在日常開(kāi)發(fā)中,基準(zhǔn)測(cè)試是必不可少的逆趣,基準(zhǔn)測(cè)試主要是通過(guò)測(cè)試CPU和內(nèi)存的效率問(wèn)題苹支,來(lái)評(píng)估被測(cè)試代碼的性能,進(jìn)而找到更好的解決方案俱济。
而Go語(yǔ)言中自帶的benchmark則是一件非常神奇的測(cè)試?yán)魉皇恰S辛怂_(kāi)發(fā)者可以方便快捷地在測(cè)試一個(gè)函數(shù)方法在串行或并行環(huán)境下的基準(zhǔn)表現(xiàn)蛛碌。指定一個(gè)時(shí)間(默認(rèn)是1秒)聂喇,看測(cè)試對(duì)象在達(dá)到或超過(guò)時(shí)間上限時(shí),最多能被執(zhí)行多少次和在此期間測(cè)試對(duì)象內(nèi)存分配情況。
1 benchmark的常見(jiàn)用法
1.1 如何寫一個(gè)benchmark的基準(zhǔn)測(cè)試
import (
"fmt"
"testing"
)
func BenchmarkSprint(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprint(i)
}
}
對(duì)以上代碼做如下說(shuō)明:
- 基準(zhǔn)測(cè)試代碼文件必須是_test.go結(jié)尾希太,和單元測(cè)試一樣克饶;
- 基準(zhǔn)測(cè)試的函數(shù)以Benchmark開(kāi)頭;
- 參數(shù)須為 *testing.B誊辉;
- 基準(zhǔn)測(cè)試函數(shù)不能有返回值矾湃;
- b.ResetTimer是重置計(jì)時(shí)器,這樣可以避免for循環(huán)之前的初始化代碼的干擾堕澄;
- b.N是基準(zhǔn)測(cè)試框架提供的邀跃,Go會(huì)根據(jù)系統(tǒng)情況生成,不用用戶設(shè)定蛙紫,表示循環(huán)的次數(shù)拍屑,因?yàn)樾枰磸?fù)調(diào)用測(cè)試的代碼,才可以評(píng)估性能坑傅;
運(yùn)行:go test -bench=. -run=none 命令得到以下結(jié)果
運(yùn)行benchmark基準(zhǔn)測(cè)試也要用到 go test 命令僵驰,不過(guò)我們后面需要加上-bench=參數(shù),接受一個(gè)表達(dá)式作為參數(shù)唁毒,匹配基準(zhǔn)測(cè)試的函數(shù)蒜茴,"."一個(gè)點(diǎn)表示運(yùn)行所有的基準(zhǔn)測(cè)試。
因?yàn)槟J(rèn)情況下 go test 會(huì)運(yùn)行單元測(cè)試浆西,為了防止單元測(cè)試的輸出影響我們查看基準(zhǔn)測(cè)試的結(jié)果矮男,可以使用-run=匹配一個(gè)從來(lái)沒(méi)有的單元測(cè)試方法,過(guò)濾掉單元測(cè)試的輸出室谚,我們這里使用none,因?yàn)槲覀兓旧喜粫?huì)創(chuàng)建這個(gè)名字的單元測(cè)試方法崔泵。
接下來(lái)再解釋下輸出的結(jié)果:
- 函數(shù)名后面的-8秒赤,表示運(yùn)行時(shí)對(duì)應(yīng)的 GOMAXPROCS 的值;
- 接著的 1230048 表示運(yùn)行 for 循環(huán)的次數(shù)憎瘸,也就是調(diào)用被測(cè)試代碼的次數(shù)入篮,也就是在b.N的范圍內(nèi)執(zhí)行的次數(shù);
- 最后的 112.9 ns/op表示每次需要花費(fèi) 112.9 納秒幌甘;
以上是測(cè)試時(shí)間默認(rèn)是1秒潮售,也就是1秒的時(shí)間,調(diào)用 1230048 次锅风,每次調(diào)用花費(fèi) 112.9 納秒酥诽。如果想讓測(cè)試運(yùn)行的時(shí)間更長(zhǎng),可以通過(guò) -benchtime= 指定皱埠,比如-benchtime=3s肮帐,表示執(zhí)行3秒。
但是我們經(jīng)過(guò)測(cè)試發(fā)現(xiàn),測(cè)試1s和3s好像沒(méi)啥明顯區(qū)別训枢,實(shí)際上最終性能并沒(méi)有多大變化托修。一般來(lái)說(shuō)不需要太長(zhǎng),常用1s恒界、3s睦刃、5s即可,也可忙根據(jù)業(yè)務(wù)場(chǎng)景來(lái)判斷十酣。
1.2 并行用法
func BenchmarkSprints(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// do something
fmt.Sprint("代碼軼事")
}
})
}
RunParallel并發(fā)的執(zhí)行benchmark涩拙。RunParallel創(chuàng)建p個(gè)goroutine然后把b.N個(gè)迭代測(cè)試分布到這些goroutine上。
goroutine的數(shù)目默認(rèn)是GOMAXPROCS婆誓。如果要增加non-CPU-bound的benchmark的并個(gè)數(shù)吃环,在執(zhí)行RunParallel之前那就使用
b.SetParallelism(p int)
來(lái)設(shè)置,最終goroutine個(gè)數(shù)就等于p * runtime.GOMAXPROCS(0)洋幻,郁轻。
numProcs := b.parallelism * runtime.GOMAXPROCS(0)
- 所以并行的用法比較適合IO密集型的測(cè)試對(duì)象。
1.3 性能對(duì)比
上面是簡(jiǎn)單寫的幾個(gè)示例文留,下面使用我前面的文章Go語(yǔ)言幾種字符串的拼接方式比較里面關(guān)于字符串拼接的例子進(jìn)行示例:
// 文中全局有一個(gè)StrData變量好唯,是一個(gè)200長(zhǎng)度的字符串slice
// 直接使用“+”號(hào)拼接
func BenchmarkStringsAdd(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var s string
for _, v := range StrData {
s += v
}
}
b.StopTimer()
}
// fmt.Sprint拼接
func BenchmarkStringsFmt(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var _ string = fmt.Sprint(StrData)
}
b.StopTimer()
}
// strings.Join拼接
func BenchmarkStringsJoin(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Join(StrData, "")
}
b.StopTimer()
}
// StringsBuffer拼接
func BenchmarkStringsBuffer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
n := len("") * (len(StrData) - 1)
for i := 0; i < len(StrData); i++ {
n += len(StrData[i])
}
var s bytes.Buffer
s.Grow(n)
for _, v := range StrData {
s.WriteString(v)
}
_ = s.String()
}
b.StopTimer()
}
// 使用strings包里的builder類型拼接
func BenchmarkStringsBuilder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
n := len("") * (len(StrData) - 1)
for i := 0; i < len(StrData); i++ {
n += len(StrData[i])
}
var b strings.Builder
b.Grow(n)
b.WriteString(StrData[0])
for _, s := range StrData[1:] {
b.WriteString("")
b.WriteString(s)
}
_ = b.String()
}
b.StopTimer()
}
這次我們添加-benchmem參數(shù),go test -bench=. -benchmem -run=none燥翅,來(lái)查看每次操作分配內(nèi)存的次數(shù)骑篙,運(yùn)行后的結(jié)果如下:
從測(cè)試結(jié)果來(lái)看,strings包的Builder類型的效率是最高的森书,單次耗時(shí)最低靶端,內(nèi)存分配的次數(shù)最少,每次分配的內(nèi)存最低凛膏。這樣我們就能從測(cè)試結(jié)果看出來(lái)是那部分代碼最慢杨名、內(nèi)存分配占用太高,進(jìn)而想辦法對(duì)相應(yīng)的代碼進(jìn)行優(yōu)化處理猖毫。
在代碼開(kāi)發(fā)中台谍,我們很多時(shí)候是不需要太在乎性能的,但絕大部分時(shí)候是需要要求性能很高的吁断,因此編寫基準(zhǔn)測(cè)試就變得非常重要趁蕊。這能幫助我們開(kāi)發(fā)出高性能、高效率的代碼仔役。
1.4 結(jié)合pprof和火焰圖查看代碼性能
我們還是以1.3節(jié)的例子掷伙,以及Go語(yǔ)言幾種字符串的拼接方式比較里的例子來(lái)說(shuō)明一下benchmark結(jié)合pprof和火焰圖查看代碼性能的問(wèn)題。
需要先采集數(shù)據(jù)骂因,生成文件炎咖,然后用pprof打開(kāi)文件并已http的方式查看,可以分別采集內(nèi)存維度和CPU維度的數(shù)據(jù),具體命令如下:
# 使用benchmark采集3秒的內(nèi)存維度的數(shù)據(jù)乘盼,并生成文件
go test -bench=. -benchmem -benchtime=3s -memprofile=mem_profile.out
# 采集CPU維度的數(shù)據(jù)
go test -bench=. -benchmem -benchtime=3s -cpuprofile=cpu_profile.out
# 查看pprof文件升熊,指定http方式查看
go tool pprof -http="127.0.0.1:8080" mem_profile.out
go tool pprof -http="127.0.0.1:8080" cpu_profile.out
# 查看pprof文件,直接在命令行查看
go tool pprof mem_profile.out
我們執(zhí)行go tool pprof -http="127.0.0.1:8080" cpu_profile.out命令后绸栅,會(huì)自動(dòng)使用我們電腦的默認(rèn)瀏覽器打開(kāi):http://127.0.0.1:8080/ui/地址级野,會(huì)顯示默認(rèn)的Graph選項(xiàng)卡,顯示各方法間的調(diào)用關(guān)系:
圖片不清楚粹胯,主要表達(dá)意思蓖柔,具體內(nèi)容可根據(jù)自己的測(cè)試情況進(jìn)行查看分析。
然后我們選擇左上角的菜單 VIEW->Flame Graph即可顯示火焰圖:
這里如果有的小伙伴沒(méi)有提前安裝好gvedit风纠,可能會(huì)報(bào)錯(cuò)提示需要安裝graphviz况鸣。Mac或Linux用戶可直接使用brew進(jìn)行安裝:
# Mac 安裝
brew install graphviz
# Ubuntu apt-get 安裝
sudo apt-get install graphviz
# yum 安裝
sudo yum install graphviz
Windows用戶去官網(wǎng)下載http://www.graphviz.org/download/
我們也可以直接在命令行使用go tool pprof cpu_profile.out命令進(jìn)行查看,
比如上圖就是用命令行打開(kāi)竹观,然后輸入top3命令來(lái)返回消耗資源最多的3個(gè)函數(shù)镐捧,然后你也可以輸入help命令來(lái)查看支持的功能。
還有其它各種維度的指標(biāo)和命令就不在此處多說(shuō)了臭增,后面也會(huì)出pprof的文章懂酱。
上面介紹了使用benchmark進(jìn)行一個(gè)基準(zhǔn)測(cè)試的一些基礎(chǔ)用法, 當(dāng)然了誊抛,如果你比較卷列牺,還是可以繼續(xù)往下看,我們來(lái)介紹一些進(jìn)階的用法拗窃。
2 深入研究benchmark
下面的內(nèi)容瞎领,將會(huì)對(duì)一些不常用但是很深入的內(nèi)容做一些說(shuō)明,有很多方法我也幾乎用不到随夸,如有不對(duì)的地方還請(qǐng)留言指正默刚,感謝!
2.1 Start/Stop/ResetTimer()
這三個(gè)方法都是針對(duì)計(jì)時(shí)統(tǒng)計(jì)器和內(nèi)存統(tǒng)計(jì)器操作的逃魄。
因?yàn)橛行┣闆r我們?cè)谧鯾enchmark測(cè)試的時(shí)候,是不想將一些不關(guān)心的工作耗時(shí)計(jì)算進(jìn)benchmark結(jié)果中的澜搅。
比如我在1.3節(jié)中做出的示例伍俘,其實(shí)我在最開(kāi)始的init()函數(shù)里設(shè)置了一個(gè)較大的slice:StrData。以便在全局使用同一個(gè)slice進(jìn)行測(cè)試勉躺,但是我在設(shè)置這個(gè)較大的slice的時(shí)候也會(huì)內(nèi)存的消耗和工作耗時(shí)癌瘾,但是我并不關(guān)心它的資源消耗,因此我也不希望會(huì)對(duì)benchmark的測(cè)試結(jié)果產(chǎn)生影響饵溅,所以在每個(gè)被測(cè)單元里都執(zhí)行了b.ResetTimer()妨退。
而且需要注意的是,在并行的情況下,b.ResetTimer()需要在b.RunParallel()之前調(diào)用咬荷,如:
func BenchmarkSprints(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// do something
fmt.Sprint("代碼軼事")
}
})
}
StopTimer()和StartTimer()的用法如下:
init();
b.ResetTimer()
for i := 0; i < b.N; i++ {
flag := func1()
if flag {
// 需要計(jì)時(shí)
b.StartTimer()
}else {
// 不需要計(jì)時(shí)
b.StopTimer()
}
}
總結(jié)來(lái)說(shuō)
- StartTimer:開(kāi)始計(jì)時(shí)測(cè)試冠句,該函數(shù)會(huì)被自動(dòng)調(diào)用,也可用于在調(diào)用了StopTimer后恢復(fù)計(jì)時(shí)幸乒;
- StopTimer:停止對(duì)測(cè)試計(jì)時(shí)懦底,也可用于在執(zhí)行復(fù)雜的初始化時(shí)暫停計(jì)時(shí);
- ResetTimer:將已用的基準(zhǔn)時(shí)間和內(nèi)存分配計(jì)數(shù)器置零罕扎,并刪除相關(guān)指標(biāo)聚唐,但不影響計(jì)時(shí)器是否在運(yùn)行;
2.2 benchmark的輸出項(xiàng)目含義解釋
我們先嘗試執(zhí)行go test -bench=. -benchmem得到下圖的輸出結(jié)果:
接下來(lái)分別介紹每一項(xiàng)的含義腔召;
- 第一項(xiàng)是現(xiàn)實(shí)的被測(cè)試的方法名杆查,后面跟的“-8”表示P的個(gè)數(shù),通過(guò)在命令后面追加參數(shù)“-cpu 4,8” 來(lái)指定臀蛛;
- 第二項(xiàng)是指在b.N周期內(nèi)迭代的總次數(shù)亲桦,即b.N的執(zhí)行上限,通常程序執(zhí)行效率越高掺栅,耗時(shí)越低烙肺,內(nèi)存分配和消耗越小,迭代次數(shù)就越大氧卧;
- b.N每次迭代耗時(shí)桃笙,單位是ns,即每次迭代消耗多少ns沙绝,是一個(gè)被平均后的均值搏明;
- b.N每次迭代的內(nèi)存分配,即在每次迭代中分配了多少字節(jié)的內(nèi)存闪檬;
- b.N每次迭代所觸發(fā)的內(nèi)存分配次數(shù)星著,觸發(fā)的內(nèi)存分配次數(shù)越大,耗時(shí)多大粗悯,效率也就越低虚循;
2.3 進(jìn)階參數(shù)
2.3.1 -benchtime t
我們?cè)跍y(cè)試某個(gè)函數(shù)性能的時(shí)候,并不是每次執(zhí)行都會(huì)得到一模一樣的效果样傍,我連續(xù)執(zhí)行10次横缔,可能會(huì)有10次不一樣的結(jié)果,這時(shí)候我們可能會(huì)選擇添加一個(gè)指定的采樣時(shí)間衫哥,來(lái)得出一個(gè)平均值茎刚,在上文中我們討論benchmark結(jié)合pprof使用的時(shí)候就用到了這個(gè)參數(shù),但也不是盲目的無(wú)限增加采樣時(shí)間就是好的撤逢,通常采用3秒5秒即可膛锭。
該參數(shù)還可支持特殊形式Nx粮坞,用來(lái)指定被測(cè)函數(shù)的迭代次數(shù),如:
從上圖可以看出初狰,指定了迭代100次莫杈,則每個(gè)函數(shù)都會(huì)只迭代100次。
2.3.2 -count n
為了我們測(cè)試的準(zhǔn)確性跷究,可以添加-count來(lái)指定測(cè)試:
2.3.3 -cpu n
還可以指定cpu的核數(shù)姓迅,比如我下面的這個(gè)例子,使用遞歸實(shí)現(xiàn)一個(gè)計(jì)算斐波那契數(shù)列的方法俊马,然后每次迭代都開(kāi)啟10個(gè)goroutine丁存,并且要等這10個(gè)goroutine都執(zhí)行結(jié)束后才會(huì)進(jìn)行下一次迭代,代碼如下:
func BenchmarkFibonacci(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(wg1 *sync.WaitGroup) {
defer wg1.Done()
arr := [20]int{}
for i := 0; i < 20; i++ {
arr[i] = fibonacci(i)
}
}(&wg)
}
}
}
func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
然后分別指定-cpu=1,2,4,6,8,10來(lái)查看測(cè)試結(jié)果:
從運(yùn)行結(jié)果來(lái)看柴我,CPU核心數(shù)提高對(duì)性能有一定影響解寝,但也無(wú)法一直實(shí)現(xiàn)正相關(guān),而且超過(guò)一定閾值后反而性能下降了艘儒,因?yàn)镃PU核心的切換也需要成本聋伦。因此也不能盲目提高CPU核心數(shù)。
2.3.4 -benchmark
除了速度界睁,內(nèi)存分配也是一個(gè)很重要的指標(biāo)觉增,我在Go語(yǔ)言幾種字符串的拼接方式比較一文中做個(gè)比較,在使用strings包的builder類型去做字符串拼接的時(shí)候翻斟,是否合理的預(yù)分配內(nèi)存逾礁,測(cè)試的結(jié)果是不同的,如果我們能合理的預(yù)分配內(nèi)存访惜,那么性能也會(huì)有較大的提升嘹履。下面我再貼出一個(gè)例子來(lái)看實(shí)際的效果:
// 根據(jù)slice的長(zhǎng)度,對(duì)strings.Builder進(jìn)行預(yù)分配內(nèi)存
func BenchmarkStringsBuilder1(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
n := len("") * (len(StrData) - 1)
for i := 0; i < len(StrData); i++ {
n += len(StrData[i])
}
var b strings.Builder
b.Grow(n)
b.WriteString(StrData[0])
for _, s := range StrData[1:] {
b.WriteString("")
b.WriteString(s)
}
_ = b.String()
}
b.StopTimer()
}
// 不進(jìn)行預(yù)分配內(nèi)存
func BenchmarkStringsBuilder2(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
var b strings.Builder
b.WriteString(StrData[0])
for _, s := range StrData[1:] {
b.WriteString("")
b.WriteString(s)
}
_ = b.String()
}
b.StopTimer()
}
然后使用benchmark測(cè)試债热,查看結(jié)果:
BenchmarkStringsBuilder1是進(jìn)行了合理的預(yù)分配內(nèi)存砾嫉,BenchmarkStringsBuilder2沒(méi)有進(jìn)行預(yù)分配內(nèi)存,從測(cè)試的結(jié)果可以看出窒篱,BenchmarkStringsBuilder1的執(zhí)行效率比BenchmarkStringsBuilder2的執(zhí)行效率高了特別多焕刮。
3 benchmark原理
要討論benchmark基準(zhǔn)測(cè)試的原理,就要討論testing.B的數(shù)據(jù)結(jié)構(gòu)墙杯,還要分析b.N的值济锄,雖然官方資料中說(shuō)b.N的值會(huì)自動(dòng)調(diào)整,以保證可靠的計(jì)時(shí)霍转,但仍需分析其實(shí)現(xiàn)的機(jī)制。
那么我們拋出以下問(wèn)題:
- b.N是如何自動(dòng)調(diào)整的一汽?
- 內(nèi)存統(tǒng)計(jì)是如何實(shí)現(xiàn)的?
- SetBytes()其使用場(chǎng)景是什么醋安?
原理部分的討論參考了【Go專家編程】的一些文章采驻,可以點(diǎn)擊關(guān)鍵詞去看在線版本。
3.1 testing.B的數(shù)據(jù)結(jié)構(gòu)
源碼包src/testing/benchmark.go:B
定義了性能測(cè)試的數(shù)據(jù)結(jié)構(gòu)恕沫,我們提取其比較重要的一些成員進(jìn)行分析:
type B struct {
common // 與testing.T共享的testing.common,負(fù)責(zé)記錄日志纱意、狀態(tài)等婶溯,詳情可見(jiàn)src/testing/testing.go文件,在大概385行
importPath string // import path of the package containing the benchmark
context *benchContext
N int // 目標(biāo)代碼執(zhí)行次數(shù)偷霉,會(huì)自動(dòng)調(diào)整
previousN int // number of iterations in the previous run
previousDuration time.Duration // total duration of the previous run
benchFunc func(b *B) // 性能測(cè)試函數(shù)
benchTime time.Duration // 性能測(cè)試函數(shù)最少執(zhí)行的時(shí)間迄委,默認(rèn)為1s,可以通過(guò)參數(shù)'-benchtime 10s'指定
bytes int64 // 每次迭代處理的字節(jié)數(shù)
missingBytes bool // one of the subbenchmarks does not have bytes set.
timerOn bool // 是否已開(kāi)始計(jì)時(shí)
showAllocResult bool
result BenchmarkResult // 測(cè)試結(jié)果
parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines
// The initial states of memStats.Mallocs and memStats.TotalAlloc.
startAllocs uint64 // 計(jì)時(shí)開(kāi)始時(shí)堆中分配的對(duì)象總數(shù)
startBytes uint64 // 計(jì)時(shí)開(kāi)始時(shí)時(shí)堆中分配的字節(jié)總數(shù)
// The net total of this test after being run.
netAllocs uint64 // 計(jì)時(shí)結(jié)束時(shí)类少,堆中增加的對(duì)象總數(shù)
netBytes uint64 // 計(jì)時(shí)結(jié)束時(shí)叙身,堆中增加的字節(jié)總數(shù)
extra map[string]float64 // 額外收集的指標(biāo)
}
其主要成員如下:
- common: 與testing.T共享的testing.common,管理著日志硫狞、狀態(tài)等信轿;
- N:每個(gè)測(cè)試中用戶代碼執(zhí)行次數(shù)
- benchFunc:測(cè)試函數(shù)
- benchTime:性能測(cè)試最少執(zhí)行時(shí)間,默認(rèn)為1s残吩,可以通過(guò)能數(shù)-benchtime 2s指定
- bytes:每次迭代處理的字節(jié)數(shù)
- timerOn:計(jì)時(shí)啟動(dòng)標(biāo)志财忽,默認(rèn)為false,啟動(dòng)計(jì)時(shí)為true
- startAllocs:測(cè)試啟動(dòng)時(shí)記錄堆中分配的對(duì)象數(shù)
- startBytes:測(cè)試啟動(dòng)時(shí)記錄堆中分配的字節(jié)數(shù)
- netAllocs:測(cè)試結(jié)束后記錄堆中新增加的對(duì)象數(shù)泣侮,公式:結(jié)束時(shí)堆中分配的對(duì)象數(shù)-
- netBytes:測(cè)試對(duì)事后記錄堆中新增加的字節(jié)數(shù)
流程示意圖如下
5 參考文獻(xiàn)
https://books.studygolang.com/GoExpertProgramming/chapter07/7.3.4-benchmark.html