go性能优化
寄存器结构
- cache的最小存储单位为cache line,一个cache line 64字节,如果只从内存中读取一个字节,也会将cache line的64字节都加载到cache中
- 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
- 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,性能更差,这称为伪共享
运行时优化
- go包含工具、编译器、运行时、标准库
gc和内存申请优化
- 减少对象申请,增加对象复用,减少对象逃逸,减少不必要对象申请
- 延长gc间隔时间
- 减少gc时长,减少对象扫描时间、常驻内存通过cgo申请
pprof工具使用
- pprof进行性能分析,有轻度代码侵入
- 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))
}
- 使用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时间
- runtime.GC() 手工执行gc
- 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)
}
- 触发gc阈值的计算公式,gc_trigger = 上次标记存活内存量 * (1 + gogc/100) gogc默认100
- 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结构中,会存储每个指针的位置和最后一个指针的位置,后面的就不在进行扫描
协程优化
- 限制协程数量
// 通过带缓存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()
- 使用协程池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
语言特性优化
- 频繁解引用
// 每次循环都是先解开引用,然后在转换成指针,效率低
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
}
}
- 减少值拷贝
// 每次调用都要值传递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]
}
- 使用小结构体,小结构体在go源码中有优化,速度更快,建议使用值拷贝,而不使用引用传值,少于16字节为小结构体
- 在已有数组或者切片上reslice,其实是引用,如果原数组较大,只引用了一部分进行操作,会导致内存得不到释放,建议使用拷贝
func copy(s [512]byte)[]byte{
newSlice := make([]byte,2)
copy(newSlice,s[0:2])
return newSlice
}
- 切片反复扩容,小于1024两倍扩容,大于1024 1/4扩容,make指定cap时,如果是变量,则会分配在堆上,是常量并且数组大小小于64k,则在栈上
// 不推荐,不指定大小 arr := make([]int,0)
// 不推荐,指定大小为变量,会发生一次alloc arr := make([]int,0,n)
arr := make([]int,0,100)
- for range使用指针,如果使用非指针,会频繁发生value的值拷贝,for range的value项不使用指针会发生值拷贝,影响性能,只是用下标和for循环效率基本一致
var persons [1024]*person
var totalAge int
for _,persion := range persions {
totalAge += person.age
}
- 清空一个数组时,使用for range
for i:=range arr {
arr[i]=zeroValue
}
- 字符串拼接,不建议用+=和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()
}
- map删除元素,如果使用delete关键字,不会回收内存,make重建会gc内存,但是慢
- 将map转换成一个枚举类型加一个数组,下标的映射使用枚举形成
- 使用接口断言成结构体或者是使用结构体进行函数调用比直接使用接口调用要快,但是不多
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()
}
}
- 接口断言成结构体性能非常高
- 少使用闭包函数和defer
并发场景
- map只有对key操作才使用写锁
- 繁殖使用defer导致锁定的代码段增加
- 减少锁范围,例如可以对一个map的不同段加不同的锁
- 使用原子操作代替锁,原子操作指执行过程中不能被中断的操作,cpu不会执行其他对该值得操作
- 减少select 的case数量,越少cpu开销越小
- case中receive比send效率高
网友评论