美文网首页
go性能优化

go性能优化

作者: mafa1993 | 来源:发表于2023-03-26 14:38 被阅读0次

go性能优化

寄存器结构

  1. cache的最小存储单位为cache line,一个cache line 64字节,如果只从内存中读取一个字节,也会将cache line的64字节都加载到cache中
  2. perf进行性能分析 perf record -F 1999 -e cycles,cache-misses,vranch-misses --pid xx -o x
    • -F指定采样频率
    • -e 指定采集的信息,cycles为cpu周期,cache misses 缓存miss,branch misses 分支miss
  3. cpu缓存:时间局部性(频繁访问的缓存)和空间局部性(访问的空间和附近的都加载)

cache line 优化

RunParallel创建多个goroutinue然后把b.N个迭代测试分布到这些goroutine上。goroutinue的数目默认是GOMAXPROCS,如果要增加non-COU-bound的benchmark的并发个数,在执行RunParallel之前调用SetParallelism

type NoPad struct {
    a uint64
    // l uint64[7]
    b uint64
}

func (p *NoPad) add(){
    atomic.AddUint64(&p.a,1)
}

func (p *NoPad) read(){
    return atomic.LoadUint64(&p.b)
}

func BenchmarkNoPad_In(b *testing.B){
    noPad := &NoPad{}
    var r uint64
    b.RunParallel(func(pb *testing.PB){
        gor pb.Next() {
            for i:=0;i<1<<10;i++{
                noPad.add()
                r = noPad.read()
            }
        }
    })
}

上面代码,不加注释行的话,a和b会加载到同一个cache line,如果多个cpu并行执行,例如cpu0对a进行写操作,cpu1执行了对b的读操作,cpu0对a写完会将整个struct写入内存,计算机不知道程序对b没有修改,导致cpu1会重新加载b,从而导致cache miss,性能更差,这称为伪共享

运行时优化

  1. go包含工具、编译器、运行时、标准库

gc和内存申请优化

  1. 减少对象申请,增加对象复用,减少对象逃逸,减少不必要对象申请
  2. 延长gc间隔时间
  3. 减少gc时长,减少对象扫描时间、常驻内存通过cgo申请

pprof工具使用

  1. pprof进行性能分析,有轻度代码侵入
  2. pprof常用命令
    • allocs,查看所有内存分配样本
    • block,查看导致阻塞的同步堆栈跟踪
    • goroutine 查看所有运行的goroutine堆栈跟踪
    • heap,查看活动对象的内存分配情况
    • profile 默认进行30s的cpu 性能采集,生成数据文件
    • curl http://xx/debug/pprof/xx > d.data 获取数据
    • go tool pprof -http=xxx d.data 展示数据

减少对象申请

// sync.pool 使用
package main
import (
    "fmt"
    "sync"
)
var pool *sync.Pool
type Person struct {
    Name string
}
func initPool() {
    pool = &sync.Pool {
        New: func()interface{} {
            fmt.Println("Creating a new Person")
            return new(Person)
        },
    }
}
func main() {
    initPool()
    p := pool.Get().(*Person)
    fmt.Println("首次从 pool 里获取:", p)
    p.Name = "first"
    fmt.Printf("设置 p.Name = %s\n", p.Name)
    pool.Put(p)
    fmt.Println("Pool 里已有一个对象:&{first},调用 Get: ", pool.Get().(*Person))
    fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}
  1. 使用sync.pool进行对象复用
// 每次创建对象, 每次都需要malloc内存,然后通过gc释放
func process() {
    for n:=0;n<1<<10;n++ {
        stu := &Persion{}
        json.Unmarshal(buf,stu)
    }
}

// 复用对象,从池子里获取对象,池子里没有才申请内存
func process() {
    for n := 0; n< 1<<10; n++ {
        stu := pool.Get().(*Person)
        json.Unmarshal(buf,stu)
        pool.Put(stu)
    }
}

延长gc时间

  1. runtime.GC() 手工执行gc
  2. debug.SetGCPercent(150) 设置gc系数
runtime.GC()
N := 100
for {
    // 每次申请100m内存
    produceTemporaryObject(N)  
    time.Sleep(time.Millsecond * 10)
}


// 设置gc
debug.SetGCPercent(150)
runtime.GC()
N := 100
for {
    // 每次申请100m内存
    produceTemporaryObject(N)  
    time.Sleep(time.Millsecond * 10)
}
  1. 触发gc阈值的计算公式,gc_trigger = 上次标记存活内存量 * (1 + gogc/100) gogc默认100
  2. gc时间间隔不是越长越好,如果临时对象很多,程序gc时间太长反而无法及时回收内存

减少对象扫描时间

type person struct {
    age int
    sex [200]int
    name string
}

func newPerson() *person{
    obj := new(person)
    obj.name ="x"
    return obj
}


// 这样定义性能更高,定义结构体时,指针类型字段往前放
type person2 struct {
    name string 
    age int
    sex [200]int
}

在扫描结构体内存时,扫描到最后一个指针就停止扫描了,在go结构中,会存储每个指针的位置和最后一个指针的位置,后面的就不在进行扫描

协程优化

  1. 限制协程数量
// 通过带缓存channel限制
var wg sync.WaitGroup

var ch = make(chan struct{},10)
for i:=0;i<iteration;i++{
    ch <- struct{} // 池满了会阻塞
    wg.Add(1)
    go func(i int){
        defer wg.Done()
        fmt.Println(i)
        time.Sleep(time.Second)
        <-ch
    }{i}
}
wg.Wait() 
  1. 使用协程池https://blog.csdn.net/K346K346/article/details/104370501?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167627276116800213093960%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167627276116800213093960&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-4-104370501-null-null.142v73insert_down4,201v4add_ask,239v1control&utm_term=go%20%E5%8D%8F%E7%A8%8B%E6%B1%A0&spm=1018.2226.3001.4187

语言特性优化

  1. 频繁解引用
// 每次循环都是先解开引用,然后在转换成指针,效率低
func a(sum *int, s []int){
    for _,v:=range s {
        *sum += v
    }
}

// 优化
func b(sum *int,s []int) {
    var n = *num;
    for _,v :=range s {
        n += v
    }
    *num = n
}
// 结构体同样
func c(a *T){
    // n := *T
    for 1:=0;i<1000;i++{
        //n.x += i
        a.x += i
    }
}
  1. 减少值拷贝
// 每次调用都要值传递xy
func add(x,y[1000]int) int {
    return x[0]+y[0]
}
// 改成引用传值
func add1(x,y *[1000]int)int {
    return (*x)[0]+(*y)[0]
}
  1. 使用小结构体,小结构体在go源码中有优化,速度更快,建议使用值拷贝,而不使用引用传值,少于16字节为小结构体
  2. 在已有数组或者切片上reslice,其实是引用,如果原数组较大,只引用了一部分进行操作,会导致内存得不到释放,建议使用拷贝
func copy(s [512]byte)[]byte{
    newSlice := make([]byte,2)
    copy(newSlice,s[0:2])
    return newSlice
}
  1. 切片反复扩容,小于1024两倍扩容,大于1024 1/4扩容,make指定cap时,如果是变量,则会分配在堆上,是常量并且数组大小小于64k,则在栈上
// 不推荐,不指定大小 arr := make([]int,0) 
// 不推荐,指定大小为变量,会发生一次alloc arr := make([]int,0,n)
arr := make([]int,0,100)
  1. for range使用指针,如果使用非指针,会频繁发生value的值拷贝,for range的value项不使用指针会发生值拷贝,影响性能,只是用下标和for循环效率基本一致
var persons [1024]*person
var totalAge int

for _,persion := range persions {
    totalAge += person.age
}
  1. 清空一个数组时,使用for range
for i:=range arr {
    arr[i]=zeroValue
}
  1. 字符串拼接,不建议用+=和fmt.Sprintf, 使用bytes builder和string builder, strings.repeat也可以
func bufferConcat(n int, str string) string {
    buf := new(bytes.Buffer)
    buf.Grow(n* len(str)) // 字符串总长度

    for i:=0;i<n;i++ {
        buf.WriteString(str)
    }
    return buf.String()
}

func bufferConcat1(n int, str string) string {
    bar builder strings.Builder
    builder.Grow(n* len(str)) // 字符串总长度

    for i:=0;i<n;i++ {
        builder.WriteString(str)
    }
    return builder.String()
}
  1. map删除元素,如果使用delete关键字,不会回收内存,make重建会gc内存,但是慢
  2. 将map转换成一个枚举类型加一个数组,下标的映射使用枚举形成
  3. 使用接口断言成结构体或者是使用结构体进行函数调用比直接使用接口调用要快,但是不多
func (p mystruct) test()
type myin interface{
    test()
}

func BenchmarkA (b *testing.B){
    //var p1 myin = mystruct{}
    //p1.test();
    // p myin = &mystruct{}
    // p.(*mystruct).test()

    var p2 = mystruct{}
    for i:=0;i<b.N;i++ {
        p2.test()
    }
}
  1. 接口断言成结构体性能非常高
  2. 少使用闭包函数和defer

并发场景

  1. map只有对key操作才使用写锁
  2. 繁殖使用defer导致锁定的代码段增加
  3. 减少锁范围,例如可以对一个map的不同段加不同的锁
  4. 使用原子操作代替锁,原子操作指执行过程中不能被中断的操作,cpu不会执行其他对该值得操作
  5. 减少select 的case数量,越少cpu开销越小
  6. case中receive比send效率高

相关文章

  • go性能优化

    程序各种指标 是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减...

  • pprof 的使用

    基本介绍 pprof 是在做性能优化前的性能分析工具。 安装: go get -u github.com/goog...

  • go-redis 源码分析:连接池

    笔者最近在项目中基于 go-redis 实现 Redis 缓存优化性能。go-redis 是一个 Go 语言实现的...

  • Android性能优化 - 消除卡顿

    性能优化系列阅读 Android性能优化 性能优化 - 消除卡顿 性能优化 - 内存优化 性能分析工具 - Tra...

  • Android性能优化 - 内存优化

    性能优化系列阅读 Android性能优化 性能优化 - 消除卡顿 性能优化- 内存优化 性能分析工具 - Trac...

  • 前端性能优化(中)

    性能优化调研系列文章 《前端性能优化(上)》 《前端性能优化(中)》 《前端性能优化(下)》 《前端性能优化(上)...

  • 前端性能优化(下)

    性能优化调研系列文章 《前端性能优化(上)》 《前端性能优化(中)》 《前端性能优化(下)》 《前端性能优化(中)...

  • Awesome Extra

    性能优化 性能优化模式 常见性能优化策略的总结 Spark 性能优化指南——基础篇 Spark 性能优化指南——高...

  • 常用的后端性能优化六种方式:缓存化+服务化+异步化等

    性能优化专题 前端性能优化 数据库性能优化 jvm和多线程优化 架构层面优化 缓存性能优化 常用的后端性能优化六大...

  • webpack 性能优化

    webpack性能优化 开发环境性能优化 生产环境性能优化 开发环境性能优化 优化打包构建速度 优化调试功能 生产...

网友评论

      本文标题:go性能优化

      本文链接:https://www.haomeiwen.com/subject/jywordtx.html