go性能優(yōu)化
寄存器結(jié)構(gòu)
- cache的最小存儲(chǔ)單位為cache line,一個(gè)cache line 64字節(jié),如果只從內(nèi)存中讀取一個(gè)字節(jié),也會(huì)將cache line的64字節(jié)都加載到cache中
- perf進(jìn)行性能分析 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緩存:時(shí)間局部性(頻繁訪問(wèn)的緩存)和空間局部性(訪問(wèn)的空間和附近的都加載)
cache line 優(yōu)化
RunParallel創(chuàng)建多個(gè)goroutinue然后把b.N個(gè)迭代測(cè)試分布到這些goroutine上。goroutinue的數(shù)目默認(rèn)是GOMAXPROCS,如果要增加non-COU-bound的benchmark的并發(fā)個(gè)數(shù),在執(zhí)行RunParallel之前調(diào)用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會(huì)加載到同一個(gè)cache line,如果多個(gè)cpu并行執(zhí)行,例如cpu0對(duì)a進(jìn)行寫(xiě)操作,cpu1執(zhí)行了對(duì)b的讀操作,cpu0對(duì)a寫(xiě)完會(huì)將整個(gè)struct寫(xiě)入內(nèi)存,計(jì)算機(jī)不知道程序?qū)沒(méi)有修改,導(dǎo)致cpu1會(huì)重新加載b,從而導(dǎo)致cache miss,性能更差,這稱為偽共享
運(yùn)行時(shí)優(yōu)化
- go包含工具、編譯器、運(yùn)行時(shí)、標(biāo)準(zhǔn)庫(kù)
gc和內(nèi)存申請(qǐng)優(yōu)化
- 減少對(duì)象申請(qǐng),增加對(duì)象復(fù)用,減少對(duì)象逃逸,減少不必要對(duì)象申請(qǐng)
- 延長(zhǎng)gc間隔時(shí)間
- 減少gc時(shí)長(zhǎng),減少對(duì)象掃描時(shí)間、常駐內(nèi)存通過(guò)cgo申請(qǐng)
pprof工具使用
- pprof進(jìn)行性能分析,有輕度代碼侵入
- pprof常用命令
- allocs,查看所有內(nèi)存分配樣本
- block,查看導(dǎo)致阻塞的同步堆棧跟蹤
- goroutine 查看所有運(yùn)行的goroutine堆棧跟蹤
- heap,查看活動(dòng)對(duì)象的內(nèi)存分配情況
- profile 默認(rèn)進(jìn)行30s的cpu 性能采集,生成數(shù)據(jù)文件
- curl http://xx/debug/pprof/xx > d.data 獲取數(shù)據(jù)
- go tool pprof -http=xxx d.data 展示數(shù)據(jù)
減少對(duì)象申請(qǐng)
// 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("設(shè)置 p.Name = %s\n", p.Name)
pool.Put(p)
fmt.Println("Pool 里已有一個(gè)對(duì)象:&{first},調(diào)用 Get: ", pool.Get().(*Person))
fmt.Println("Pool 沒(méi)有對(duì)象了,調(diào)用 Get: ", pool.Get().(*Person))
}
- 使用sync.pool進(jìn)行對(duì)象復(fù)用
// 每次創(chuàng)建對(duì)象, 每次都需要malloc內(nèi)存,然后通過(guò)gc釋放
func process() {
for n:=0;n<1<<10;n++ {
stu := &Persion{}
json.Unmarshal(buf,stu)
}
}
// 復(fù)用對(duì)象,從池子里獲取對(duì)象,池子里沒(méi)有才申請(qǐng)內(nèi)存
func process() {
for n := 0; n< 1<<10; n++ {
stu := pool.Get().(*Person)
json.Unmarshal(buf,stu)
pool.Put(stu)
}
}
延長(zhǎng)gc時(shí)間
- runtime.GC() 手工執(zhí)行g(shù)c
- debug.SetGCPercent(150) 設(shè)置gc系數(shù)
runtime.GC()
N := 100
for {
// 每次申請(qǐng)100m內(nèi)存
produceTemporaryObject(N)
time.Sleep(time.Millsecond * 10)
}
// 設(shè)置gc
debug.SetGCPercent(150)
runtime.GC()
N := 100
for {
// 每次申請(qǐng)100m內(nèi)存
produceTemporaryObject(N)
time.Sleep(time.Millsecond * 10)
}
- 觸發(fā)gc閾值的計(jì)算公式,gc_trigger = 上次標(biāo)記存活內(nèi)存量 * (1 + gogc/100) gogc默認(rèn)100
- gc時(shí)間間隔不是越長(zhǎng)越好,如果臨時(shí)對(duì)象很多,程序gc時(shí)間太長(zhǎng)反而無(wú)法及時(shí)回收內(nèi)存
減少對(duì)象掃描時(shí)間
type person struct {
age int
sex [200]int
name string
}
func newPerson() *person{
obj := new(person)
obj.name ="x"
return obj
}
// 這樣定義性能更高,定義結(jié)構(gòu)體時(shí),指針類(lèi)型字段往前放
type person2 struct {
name string
age int
sex [200]int
}
在掃描結(jié)構(gòu)體內(nèi)存時(shí),掃描到最后一個(gè)指針就停止掃描了,在go結(jié)構(gòu)中,會(huì)存儲(chǔ)每個(gè)指針的位置和最后一個(gè)指針的位置,后面的就不在進(jìn)行掃描
協(xié)程優(yōu)化
- 限制協(xié)程數(shù)量
// 通過(guò)帶緩存channel限制
var wg sync.WaitGroup
var ch = make(chan struct{},10)
for i:=0;i<iteration;i++{
ch <- struct{} // 池滿了會(huì)阻塞
wg.Add(1)
go func(i int){
defer wg.Done()
fmt.Println(i)
time.Sleep(time.Second)
<-ch
}{i}
}
wg.Wait()
- 使用協(xié)程池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
語(yǔ)言特性優(yōu)化
- 頻繁解引用
// 每次循環(huán)都是先解開(kāi)引用,然后在轉(zhuǎn)換成指針,效率低
func a(sum *int, s []int){
for _,v:=range s {
*sum += v
}
}
// 優(yōu)化
func b(sum *int,s []int) {
var n = *num;
for _,v :=range s {
n += v
}
*num = n
}
// 結(jié)構(gòu)體同樣
func c(a *T){
// n := *T
for 1:=0;i<1000;i++{
//n.x += i
a.x += i
}
}
- 減少值拷貝
// 每次調(diào)用都要值傳遞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]
}
- 使用小結(jié)構(gòu)體,小結(jié)構(gòu)體在go源碼中有優(yōu)化,速度更快,建議使用值拷貝,而不使用引用傳值,少于16字節(jié)為小結(jié)構(gòu)體
- 在已有數(shù)組或者切片上reslice,其實(shí)是引用,如果原數(shù)組較大,只引用了一部分進(jìn)行操作,會(huì)導(dǎo)致內(nèi)存得不到釋放,建議使用拷貝
func copy(s [512]byte)[]byte{
newSlice := make([]byte,2)
copy(newSlice,s[0:2])
return newSlice
}
- 切片反復(fù)擴(kuò)容,小于1024兩倍擴(kuò)容,大于1024 1/4擴(kuò)容,make指定cap時(shí),如果是變量,則會(huì)分配在堆上,是常量并且數(shù)組大小小于64k,則在棧上
// 不推薦,不指定大小 arr := make([]int,0)
// 不推薦,指定大小為變量,會(huì)發(fā)生一次alloc arr := make([]int,0,n)
arr := make([]int,0,100)
- for range使用指針,如果使用非指針,會(huì)頻繁發(fā)生value的值拷貝,for range的value項(xiàng)不使用指針會(huì)發(fā)生值拷貝,影響性能,只是用下標(biāo)和for循環(huán)效率基本一致
var persons [1024]*person
var totalAge int
for _,persion := range persions {
totalAge += person.age
}
- 清空一個(gè)數(shù)組時(shí),使用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)) // 字符串總長(zhǎng)度
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)) // 字符串總長(zhǎng)度
for i:=0;i<n;i++ {
builder.WriteString(str)
}
return builder.String()
}
- map刪除元素,如果使用delete關(guān)鍵字,不會(huì)回收內(nèi)存,make重建會(huì)gc內(nèi)存,但是慢
- 將map轉(zhuǎn)換成一個(gè)枚舉類(lèi)型加一個(gè)數(shù)組,下標(biāo)的映射使用枚舉形成
- 使用接口斷言成結(jié)構(gòu)體或者是使用結(jié)構(gòu)體進(jìn)行函數(shù)調(diào)用比直接使用接口調(diào)用要快,但是不多
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()
}
}
- 接口斷言成結(jié)構(gòu)體性能非常高
- 少使用閉包函數(shù)和defer
并發(fā)場(chǎng)景
- map只有對(duì)key操作才使用寫(xiě)鎖
- 繁殖使用defer導(dǎo)致鎖定的代碼段增加
- 減少鎖范圍,例如可以對(duì)一個(gè)map的不同段加不同的鎖
- 使用原子操作代替鎖,原子操作指執(zhí)行過(guò)程中不能被中斷的操作,cpu不會(huì)執(zhí)行其他對(duì)該值得操作
- 減少select 的case數(shù)量,越少cpu開(kāi)銷(xiāo)越小
- case中receive比send效率高