<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          go語言最全優(yōu)化技巧總結(jié),值得收藏!

          共 9212字,需瀏覽 19分鐘

           ·

          2021-08-24 00:32


          導(dǎo)語 | 本文總結(jié)了在維護(hù)go基礎(chǔ)庫(kù)過程中,用到或者見到的一些性能優(yōu)化技巧,現(xiàn)將一些理解梳理撰寫成文,和大家探討。



          一、常規(guī)手段


          (一)sync.Pool


          臨時(shí)對(duì)象池應(yīng)該是對(duì)可讀性影響最小且優(yōu)化效果顯著的手段。基本上,業(yè)內(nèi)以高性能著稱的開源庫(kù),都會(huì)使用到。


          最典型的就是fasthttp(網(wǎng)址:https://github.com/valyala/fasthttp/)了,它幾乎把所有的對(duì)象都用sync.Pool維護(hù)。


          但這樣的復(fù)用不一定全是合理的。比如在fasthttp中,傳遞上下文相關(guān)信息的RequestCtx就是用sync.Pool維護(hù)的,這就導(dǎo)致了你不能把它傳遞給其他的goroutine。


          如果要在fasthttp中實(shí)現(xiàn)類似接受請(qǐng)求->異步處理的邏輯,必須得拷貝一份RequestCtx再傳遞。這對(duì)不熟悉fasthttp原理的使用者來講,很容易就踩坑了。


          還有一種利用sync.Pool特性,來減少鎖競(jìng)爭(zhēng)的優(yōu)化手段,也非常巧妙。另外,在優(yōu)化前要善用go逃逸檢查分析對(duì)象是否逃逸到堆上,防止負(fù)優(yōu)化。



          (二)string2bytes & bytes2string


          這也是兩個(gè)比較常規(guī)的優(yōu)化手段,核心還是復(fù)用對(duì)象,減少內(nèi)存分配。


          在go標(biāo)準(zhǔn)庫(kù)中也有類似的用法gostringnocopy。


          要注意string2bytes后,不能對(duì)其修改。


          unsafe.Pointer經(jīng)常出現(xiàn)在各種優(yōu)化方案中,使用時(shí)要非常小心。這類操作引發(fā)的異常,通常是不能recover的。



          (三)協(xié)程池


          絕大部分應(yīng)用場(chǎng)景,go是不需要協(xié)程池的。當(dāng)然,協(xié)程池還是有一些自己的優(yōu)勢(shì):


          1. 可以限制goroutine數(shù)量,避免無限制的增長(zhǎng)。

          2. 減少棧擴(kuò)容的次數(shù)。

          3. 頻繁創(chuàng)建goroutine的場(chǎng)景下,資源復(fù)用,節(jié)省內(nèi)存。(需要一定規(guī)模。一般場(chǎng)景下,效果不太明顯。)

           

          go對(duì)goroutine有一定的復(fù)用能力。所以要根據(jù)場(chǎng)景選擇是否使用協(xié)程池,不恰當(dāng)?shù)膱?chǎng)景不僅得不到收益,反而增加系統(tǒng)復(fù)雜性。



          (四)反射


          go里面的反射代碼可讀性本來就差,常見的優(yōu)化手段進(jìn)一步犧牲可讀性。而且后續(xù)馬上就有泛型的支持,所以若非必要,建議不要優(yōu)化反射部分的代碼。


          比較常見的優(yōu)化手段有:


          1. 緩存反射結(jié)果,減少不必要的反射次數(shù)。例如json-iterator

            (網(wǎng)址:https://github.com/json-iterator/go)。

          2. 直接使用unsafe.Pointer根據(jù)各個(gè)字段偏移賦值。

          3. 消除一般的struct反射內(nèi)存消耗go-reflect。

            (網(wǎng)址:https://github.com/goccy/go-reflect)

          4. 避免一些類型轉(zhuǎn)換,如interface->[]byte。



          (五)減小鎖消耗


          并發(fā)場(chǎng)景下,對(duì)臨界區(qū)加鎖比較常見。帶來的性能隱患也必須重視。常見的優(yōu)化手段有:


          • 減小鎖粒度:

            go標(biāo)準(zhǔn)庫(kù)當(dāng)中,math.rand就有這么一處隱患。當(dāng)我們直接使用rand庫(kù)生成隨機(jī)數(shù)時(shí),實(shí)際上由全局的globalRand對(duì)象負(fù)責(zé)生成。globalRand加鎖后生成隨機(jī)數(shù),會(huì)導(dǎo)致我們?cè)诟哳l使用隨機(jī)數(shù)的場(chǎng)景下效率低下


          • atomic:

            適當(dāng)場(chǎng)景下,用原子操作代替互斥鎖也是一種經(jīng)典的lock-free技巧。標(biāo)準(zhǔn)庫(kù)中sync.map針對(duì)讀操作的優(yōu)化消除了rwlock,是一個(gè)標(biāo)準(zhǔn)的案例。對(duì)它的介紹文章也比較多,不在贅述。


          prometheus里的組件histograms直方圖也是一個(gè)非常巧妙的設(shè)計(jì)。一般的開源庫(kù),比如go-metrics(網(wǎng)址:https://github.com/rcrowley/go-metrics)是直接在這里使用了互斥鎖。指標(biāo)上報(bào)作為一個(gè)高頻操作,在這里加鎖,對(duì)系統(tǒng)性能影響可想而知。


          參考sync.map里冗余map的做法,prometheus把原來histograms的計(jì)數(shù)器也分為兩個(gè):cold和hot,還有一個(gè)hotIdx用來表示哪個(gè)計(jì)數(shù)器是hot。prometheus里的組件histograms直方圖也是一個(gè)非常巧妙的設(shè)計(jì)。一般的開源庫(kù),比如go-metrics(網(wǎng)址:https://github.com/rcrowley/go-metrics)是直接在這里使用了互斥鎖。指標(biāo)上報(bào)作為一個(gè)高頻操作,在這里加鎖,對(duì)系統(tǒng)性能影響可想而知。


          業(yè)務(wù)代碼上報(bào)指標(biāo)時(shí),用atomic原子操作對(duì)hot計(jì)數(shù)器累加向prometheus服務(wù)上報(bào)數(shù)據(jù)時(shí),更改hotIdx,把原來的熱數(shù)據(jù)變?yōu)槔鋽?shù)據(jù),作為上報(bào)的數(shù)據(jù)。然后把現(xiàn)在冷數(shù)據(jù)里的值,累加到熱數(shù)據(jù)里,完成一次冷熱數(shù)據(jù)的更新替換。

          還有一些狀態(tài)等待,結(jié)構(gòu)體內(nèi)存布局的介紹,不再贅述。



          二、另類手段


          (一)golink


          golink(網(wǎng)址:https://golang.org/cmd/compile/)在官方的文檔里有介紹,使用格式:


          //go:linkname FastRand runtime.fastrandfunc FastRand() uint32

          主要功能就是讓編譯器編譯的時(shí)候,把當(dāng)前符號(hào)指向到目標(biāo)符號(hào)。上面的函數(shù)FastRand被指向到runtime.fastrand,runtime包生成的也是偽隨機(jī)數(shù),和math包不同的是,它的隨機(jī)數(shù)生成使用的上下文是來自當(dāng)前goroutine的,所以它不用加鎖。正因如此,一些開源庫(kù)選擇直接使用runtime的隨機(jī)數(shù)生成函數(shù)。性能對(duì)比如下:


          Benchmark_MathRand-12       84419976            13.98 ns/opBenchmark_Runtime-12        505765551           2.158 ns/op


          還有很多這樣的例子,比如我們要拿時(shí)間戳的話,可以標(biāo)準(zhǔn)庫(kù)中的time.Now(),這個(gè)庫(kù)在會(huì)有兩次系統(tǒng)調(diào)用runtime.walltime1和runtime.nanotime,分別獲取時(shí)間戳和程序運(yùn)行時(shí)間。大部分場(chǎng)景下,我們只需要時(shí)間戳,這時(shí)候就可以直接使用runtime.walltime1。性能對(duì)比如下:


          Benchmark_Time-12       16323418            73.30 ns/opBenchmark_Runtime-12    29912856            38.10 ns/op


          同理,如果我們需要統(tǒng)計(jì)某個(gè)函數(shù)的耗時(shí),也可以直接調(diào)用兩次runtime.nanotime然后相減,不用再調(diào)用兩次time.Now。


          //go:linkname nanotime1 runtime.nanotime1func nanotime1() int64func main() {    defer func( begin int64) {        cost := (nanotime1() - begin)/1000/1000        fmt.Printf("cost = %dms \n" ,cost)    }(nanotime1())        time.Sleep(time.Second)}
          運(yùn)行結(jié)果:cost = 1000ms


          系統(tǒng)調(diào)用在go里面相對(duì)來講是比較重的。runtime會(huì)切換到g0棧中去執(zhí)行這部分代碼,time.Now方法在go<=1.16中有兩次連續(xù)的系統(tǒng)調(diào)用。


          不過,go官方團(tuán)隊(duì)的lan大佬已經(jīng)發(fā)現(xiàn)并提交優(yōu)化pr。


          優(yōu)化后,這兩次系統(tǒng)調(diào)將會(huì)合并在一起,減少一次g0棧的切換。


          linkname為我們提供了一種方法,可以直接調(diào)用go標(biāo)準(zhǔn)庫(kù)里的未導(dǎo)出方法,可以讀取未導(dǎo)出變量。使用時(shí)要注意go版本更新后,是否有兼容問題,畢竟go團(tuán)隊(duì)并沒有保證這些未導(dǎo)出的方法變量后續(xù)不會(huì)變更。


          還有一些其他奇奇怪怪的用法:


          1. reflect2包,創(chuàng)建reflect.typelinks的引用,用來讀取所有包中struct的定義。

          2. 創(chuàng)建panic的引用后,用一些hook函數(shù)重定向panic,這樣你的程序panic后會(huì)走到你的自定義邏輯里。

          3. runtime.main_inittask保存了程序初始化時(shí),init函數(shù)的執(zhí)行順序,之前版本沒有init過程debug功能時(shí),可以用它來打印程序init調(diào)用鏈。最新版本已經(jīng)有官方的調(diào)試方案:GODEBUG=inittracing=1開啟init。

          4. runtime.asmcgocall是cgo代碼的實(shí)際調(diào)用入口。有時(shí)候我們可以直接用它來調(diào)用cgo代碼,避免goroutine切換,具體會(huì)在cgo優(yōu)化部分展開。



          (二) log-函數(shù)名稱行號(hào)的獲取


          雖然很多高性能的日志庫(kù),默認(rèn)都不開啟記錄行號(hào)。但實(shí)際業(yè)務(wù)場(chǎng)景中,我們還是覺得能打印最好。


          runtime中,函數(shù)行號(hào)和函數(shù)名稱的獲取分為兩步:


          1. runtime回溯goroutine棧,獲取上層調(diào)用方函數(shù)的的程序計(jì)數(shù)器(pc)。

          2. 根據(jù)pc,找到對(duì)應(yīng)的funcInfo,然后返回行號(hào)名稱。

          經(jīng)過pprof分析。第二步性能占比最大,約60%。針對(duì)第一步,我們經(jīng)過多次嘗試,并沒有找到有效的辦法。但是第二步很明顯,我們不需要每次都調(diào)用runtime函數(shù)去查找pc和函數(shù)信息的,我們可以把第一次的結(jié)果緩存起來,后面直接使用。這樣,第二步約60%的消耗就可以去掉。


          var(    m sync.Map)func Caller(skip int)(pc uintptr, file string, line int, ok bool){    rpc := [1]uintptr{}    n := runtime.Callers(skip+1, rpc[:])    if n < 1 {        return    }    var (        frame  runtime.Frame        )    pc  = rpc[0]    if item,ok:=m.Load(pc);ok{        frame = item.(runtime.Frame)    }else{        tmprpc := []uintptr{            pc,        }        frame, _ = runtime.CallersFrames(tmprpc).Next()        m.Store(pc,frame)    }    return frame.PC,frame.File,frame.Line,frame.PC!=0}


          壓測(cè)數(shù)據(jù)如下,優(yōu)化后稍微減輕這部分的負(fù)擔(dān),同時(shí)消除掉不必要的內(nèi)存分配。


          BenchmarkCaller-8       2765967        431.7 ns/op         0 B/op          0 allocs/opBenchmarkRuntime-8      1000000       1085 ns/op         216 B/op          2 allocs/op



          (三)cgo


          cgo的支持讓我們可以在go中調(diào)用c++和c的代碼,但cgo的代碼在運(yùn)行期間不受go調(diào)度器的管理,為了防止cgo調(diào)用引起調(diào)度阻塞,cgo調(diào)用會(huì)切換到g0棧執(zhí)行,并獨(dú)占m。由于runtime設(shè)計(jì)時(shí)沒有考慮m的回收,所以運(yùn)行時(shí)間久了之后,會(huì)發(fā)現(xiàn)有cgo代碼的程序,線程數(shù)都比較多。


          用go的編譯器轉(zhuǎn)換包含cgo的代碼:


          go tool cgo main.go


          轉(zhuǎn)換后看代碼,cgo調(diào)用實(shí)際上是由runtime.cgocall發(fā)起,而runtime.cgocall調(diào)用過程主要分為以下幾步:


          1. entersyscall(): 保存上下文,標(biāo)記當(dāng)前mincgo獨(dú)占m,跳過垃圾回收。

          2. osPreemptExtEnter:標(biāo)記異步搶占,使異步搶占邏輯失效。

          3. asmcgocall:真正的cgo call入口,切換到g0執(zhí)行c代碼。

          4. 恢復(fù)之前的上下文,清理標(biāo)記。

           

          對(duì)于一些簡(jiǎn)單的c函數(shù),我們可以直接用asmcgocall調(diào)用,避免來回切換:


          package main
          /*#include <stdio.h>#include <stdlib.h>#include <unistd.h>struct args{ int p1,p2; int r;};int add(struct args* arg) { arg->r= arg->p1 + arg->p2; return 100;}*/import "C"import ( "fmt" "unsafe")//go:linkname asmcgocall runtime.asmcgocallfunc asmcgocall(unsafe.Pointer, uintptr) int32
          func main() { arg := C.struct_args{} arg.p1 = 100 arg.p2 = 200 //C.add(&arg) asmcgocall(C.add,uintptr(unsafe.Pointer(&arg))) fmt.Println(arg.r)}

          壓測(cè)數(shù)據(jù)如下:


          BenchmarkCgo-12             16143393    73.01 ns/op     16 B/op        1 allocs/op
          BenchmarkAsmCgoCall-12 119081407 9.505 ns/op 0 B/op 0 allocs/op



          (四)epoll


          runtime對(duì)網(wǎng)絡(luò)io,以及定時(shí)器的管理,會(huì)放到自己維護(hù)的一個(gè)epoll里,具體可以參考runtime/netpool。在一些高并發(fā)的網(wǎng)絡(luò)io中,有以下幾個(gè)問題:


          1. 需要維護(hù)大量的協(xié)程去處理讀寫事件。

          2. 對(duì)連接的狀態(tài)無感知,必須要等待read或者write返回錯(cuò)誤才能知道對(duì)端狀態(tài),其余時(shí)間只能等待。

          3. 原生的netpool只維護(hù)一個(gè)epoll,沒有充分發(fā)揮多核優(yōu)勢(shì)。


          基于此,有很多項(xiàng)目用x/unix擴(kuò)展包實(shí)現(xiàn)了自己的基于epoll的網(wǎng)絡(luò)庫(kù),比如潘神的gnet(網(wǎng)址:https://github.com/panjf2000/gnet),還有字節(jié)跳動(dòng)的netpoll


          在我們的項(xiàng)目中,也有嘗試過使用。最終我們還是覺得基于標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)已經(jīng)足夠。理由如下:


          1. 用戶態(tài)的goroutine優(yōu)先級(jí)沒有g(shù)onetpool的調(diào)度優(yōu)先級(jí)高。帶來的問題就是毛刺多了。近期字節(jié)跳動(dòng)也開源了自己的netpool,并且通過優(yōu)化擴(kuò)展包內(nèi)epoll的使用方式來優(yōu)化這個(gè)問題,具體效果未知。

          2. 效果不明顯,我們絕大部分業(yè)務(wù)的QPS主要受限于其他的RPC調(diào)用,或者CPU計(jì)算。收發(fā)包的優(yōu)化效果很難體現(xiàn)。

          3. 增加了系統(tǒng)復(fù)雜性,雖然標(biāo)準(zhǔn)庫(kù)慢一點(diǎn)點(diǎn),但是足夠穩(wěn)定和簡(jiǎn)單。



          (五)包大小優(yōu)化


          我們CI是用藍(lán)盾流水線實(shí)現(xiàn)的,有一次業(yè)務(wù)反饋說藍(lán)盾編譯的二進(jìn)制會(huì)比自己開發(fā)機(jī)編譯的體積大50%左右。對(duì)比了操作系統(tǒng)和go版本都是一樣的,tlinux2.2 golang1.15。我們?cè)谟胠inux命令size—A對(duì)兩個(gè)文件各個(gè)section做對(duì)比時(shí),發(fā)現(xiàn)了debug相關(guān)的section size明顯不一致,而且section的名稱也不一樣:


          size -A test-30MBsection                  size       addr.interp                    28    4194928.note.ABI-tag              32    4194956... ... ... ....zdebug_aranges          1565          0.zdebug_pubnames        56185          0.zdebug_info          2506085          0.zdebug_abbrev          13448          0.zdebug_line          1250753          0.zdebug_frame          298110          0.zdebug_str             40806          0.zdebug_loc           1199790          0.zdebug_pubtypes       151567          0.zdebug_ranges         371590          0.debug_gdb_scripts         42          0Total                93653020
          size -A test-50MBsection size addr.interp 28 4194928.note.ABI-tag 32 4194956.note.go.buildid 100 4194988... ... ....debug_aranges 6272 0.debug_pubnames 289151 0.debug_info 8527395 0.debug_abbrev 73457 0.debug_line 4329334 0.debug_frame 1235304 0.debug_str 336499 0.debug_loc 8018952 0.debug_pubtypes 1072157 0.debug_ranges 2256576 0.debug_gdb_scripts 62 0Total 113920274


          通過查找debug和zdebug的區(qū)別了解到,zdebug是對(duì)debug段做了zip壓縮,所以壓縮后包體積會(huì)更小。查看go的源碼(網(wǎng)址:https://github.com/golang/go/blob/master/src/cmd/link/internal/ld/dwarf.go#L2210),發(fā)現(xiàn)鏈接器默認(rèn)已經(jīng)對(duì)debug段做了zip壓縮。


          看來,未壓縮的debug段不是go自己干的。我們很容易就猜到,由于代碼中引入了cgo,可能是c++的鏈接器沒有壓縮導(dǎo)致的。


          代碼引入cgo后,go代碼由go編譯器編譯,c代碼由g++編譯。后續(xù)由ld鏈接成可執(zhí)行文件


          所以包含cgo的代碼在跨平臺(tái)編譯時(shí),需要更改對(duì)應(yīng)平臺(tái)的c代碼編譯器,鏈接器。具體過程可以翻閱go編譯過程相關(guān)資料,不再贅述


          再次尋找原因,我們猜測(cè)可能跟tlinux2.2支持go 1.16有關(guān),之前我們發(fā)現(xiàn)升級(jí)go版本之后,在開發(fā)機(jī)上無法編譯。最后發(fā)現(xiàn)是因?yàn)間o1.16優(yōu)化了一部分編譯指令,導(dǎo)致我們的ld版本太低不支持。所以我們用yum install -y binutils升級(jí)了ld的版本。果然,在翻閱了ld的文檔之后,我們確認(rèn)了tlinux2.2自帶的ld不支持--compress-debug-sections=zlib-gnu這個(gè)指令,升級(jí)后ld才支持。


          總結(jié):在包含cgo的代碼編譯時(shí),將ld升級(jí)到2.27版本,編譯后的體積可以減少約50%。



          (六)simd


          首先,go鏈接器支持simd指令,但go編譯器不支持simd指令的生成。


          所以在go中使用simd一般來說有三種方式:


          1. 手寫匯編。

          2. llvm。

          3. cgo(如果用cgo的方式來調(diào)用,會(huì)受限于cgo的性能,達(dá)不到加速的目的)。


          目前比較流行的做法是llvm:


          1. 用c來寫simd相關(guān)的函數(shù),然后用llvm編譯成c匯編。

          2. 用工具把c匯編轉(zhuǎn)換成go的匯編格式,保存為.s文件。

          3. 在go中調(diào)用.s里的方法,最后用go編譯器編譯。

          以下開源庫(kù)用到了simd,可以參考:


          1. simdjson-go

            (網(wǎng)址:https://github.com/minio/simdjson-go)

          2. soni

            (網(wǎng)址:https://github.com/bytedance/sonic)

          3. sha256-simd

            (網(wǎng)址:https://github.com/minio/sha256-simd)


          合理的使用simd可以充分發(fā)揮cpu特性,但是存在以下弊端:


          1. 難以維護(hù),要么需要懂匯編的大神,要么需要引入第三方語言。

          2. 跨平臺(tái)支持不夠,需要對(duì)不同平臺(tái)匯編指令做適配。

          3. 匯編代碼很難調(diào)試,作為使用方來講,完全黑盒。



          (七)jit


          go中使用jit的方式可以參考Writing a JIT compiler in Golang,

          目前只有在字節(jié)跳動(dòng)剛開源的json解析庫(kù)中發(fā)現(xiàn)了使用場(chǎng)景sonic。

          (網(wǎng)址:https://github.com/bytedance/sonic)

          這種使用方式個(gè)人感覺在go中意義不大,僅供參考。



          三、總結(jié)


          過早的優(yōu)化是萬惡之源,千萬不要為了優(yōu)化而優(yōu)化:


          1. pprof分析,競(jìng)態(tài)分析,逃逸分析,這些基礎(chǔ)的手段是必須要學(xué)會(huì)的。

          2. 常規(guī)的優(yōu)化技巧是比較實(shí)用的,他們往往能解決大部分的性能問題并且足夠安全。

          3. 在一些著重性能的基礎(chǔ)庫(kù)中,使用一些非常規(guī)的優(yōu)化手段也是可以的,但必須要權(quán)衡利弊,不要過早放棄可讀性,兼容性和穩(wěn)定性。




           作者簡(jiǎn)介


          趙柯

          騰訊音樂后臺(tái)開發(fā)工程師

          騰訊音樂后臺(tái)開發(fā)工程師,Go Contributor。



           推薦閱讀


          首篇極客解題報(bào)告意外泄出!亞軍竟有神操作?

          消息隊(duì)列:聽我解釋,我真的不是只有Kafka!

          如何用函數(shù)式編程思想優(yōu)化業(yè)務(wù)代碼,這就給你安排上!

          拒絕代碼臃腫,這套計(jì)算引擎設(shè)計(jì)方法值得一看!





          瀏覽 65
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色一级电影视频 | 九九九亚洲视频播放 | 亚洲无码成人视频在线观看 | 日韩激情无码 | 老太色HD色老太HD-百度 无码专区一区二区三区 |