go性能优化

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效率高
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容