go通過(guò)benchmark對(duì)代碼進(jìn)行性能測(cè)試詳解
benchmark的使用
在開(kāi)發(fā)中我們要想編寫(xiě)高性能的代碼,或者優(yōu)化代碼的性能時(shí),你首先得知道當(dāng)前代碼的性能,在go中可以使用testing包的benchmark來(lái)做基準(zhǔn)測(cè)試 ,首先我們寫(xiě)一個(gè)簡(jiǎn)單的返回隨機(jī)字符串的方法
func randomStr(length int) string { mathRand.Seed(time.Now().UnixNano()) letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, length) for i := range b { b[i] = letters[mathRand.Intn(len(letters))] } return string(b) }
要對(duì)上面的代碼做基準(zhǔn)測(cè)試,首先我們要新建一個(gè)測(cè)試文件,比如main_test.go
,然后新建一個(gè)基準(zhǔn)測(cè)試方法BenchmarkRandomStr
,與普通的測(cè)試函數(shù)Test 開(kāi)頭,參數(shù)為t *testing.T類(lèi)似,基準(zhǔn)測(cè)試函數(shù)要以Benchmark開(kāi)頭,參數(shù)為b *testing.B,代碼中的b.N
代表的是該用例的運(yùn)行次數(shù),這個(gè)值是會(huì)變的,對(duì)于每個(gè)用例都不一樣,這個(gè)值會(huì)從1開(kāi)始增加,具體的實(shí)現(xiàn)我會(huì)在下面的實(shí)現(xiàn)原理里進(jìn)行介紹。
func BenchmarkRandomStr(b *testing.B) { for i := 0; i < b.N; i++ { randomStr(10000) } }
運(yùn)行Benchmark
我們可以使用 go test -bench .
命令直接運(yùn)行當(dāng)前目錄下的所有基準(zhǔn)測(cè)試用例,-bench后面也可以跟正則或者是字符串來(lái)匹配對(duì)應(yīng)的用例
$ go test -bench='Str$' goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ? 6692 ? ? ? ? ? ?181262 ns/op PASS ok ? ? learn/learn_test ? ? ? ?2.142s ?
對(duì)上面的一些關(guān)鍵指標(biāo)我們要了解一下,首先BenchmarkRandomStr-12后面的-12
代表的是GOMAXPROCS
這個(gè)跟你機(jī)器CPU的邏輯核數(shù)有關(guān),在基準(zhǔn)測(cè)試中可以通過(guò)-cpu
參數(shù)指定需要以幾核的cpu來(lái)運(yùn)行測(cè)試用例
$ go test -bench='Str$' -cpu=2,4,8 . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-2 ? ? ? ?6715 ? ? ? ? ? ?181197 ns/op BenchmarkRandomStr-4 ? ? ? ?6471 ? ? ? ? ? ?180249 ns/op BenchmarkRandomStr-8 ? ? ? ?6616 ? ? ? ? ? ?179510 ns/op PASS ok ? ? learn/learn_test ? ? ? ?4.516s ?
6715
和181197 ns/op
代表用例執(zhí)行了6715次,每次花費(fèi)的時(shí)間約為0.0001812s,總耗時(shí)約為1.2s(ns:s的換算為1000000000:1)
指定測(cè)試時(shí)長(zhǎng)或測(cè)試次數(shù)
-benchtime=3s 指定時(shí)長(zhǎng)
-benchtime=100000x 指定次數(shù)
-coun=3 指定輪數(shù)
$ go test -bench='Str$' -benchtime=3s . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ?19988 ? ? ? ? ? ?177572 ns/op PASS ok ? ? learn/learn_test ? ? ? ?5.384s ? $ go test -bench='Str$' -benchtime=10000x . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ?10000 ? ? ? ? ? ?184832 ns/op PASS ok ? ? learn/learn_test ? ? ? ?1.870s ? $ go test -bench='Str$' -count=2 . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ? 6702 ? ? ? ? ? ?177048 ns/op BenchmarkRandomStr-12 ? ? ? ? ? ? ? 6482 ? ? ? ? ? ?177861 ns/op PASS ok ? ? learn/learn_test ? ? ? ?3.269s ? ?
重置時(shí)間和暫停計(jì)時(shí)
有時(shí)候我們的測(cè)試用例會(huì)需要一些前置準(zhǔn)備的耗時(shí)行為,這對(duì)我們的測(cè)試結(jié)果會(huì)產(chǎn)生影響,這個(gè)時(shí)候就需要在耗時(shí)操作后重置計(jì)時(shí)。下面我們用一個(gè)偽代碼來(lái)模擬一下
func BenchmarkRandomStr(b *testing.B) { time.Sleep(time.Second * 2) // 模擬耗時(shí)操作 for i := 0; i < b.N; i++ { randomStr(10000) } } ?
這時(shí)候我們?cè)賵?zhí)行一下用例
$ go test -bench='Str$' . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ? ? ?1 ? ? ? ?2001588866 ns/op PASS ok ? ? learn/learn_test ? ? ? ?2.009s ?
發(fā)現(xiàn)只執(zhí)行了一次,時(shí)間變成了2s多,這顯然不符合我們的預(yù)期,這個(gè)時(shí)候需要調(diào)用b.ResetTime()
來(lái)重置時(shí)間
func BenchmarkRandomStr(b *testing.B) { time.Sleep(time.Second * 2) // 模擬耗時(shí)操作 ?b.ResetTimer() for i := 0; i < b.N; i++ { randomStr(10000) } }
再次執(zhí)行基準(zhǔn)測(cè)試
$ go test -bench='Str$' . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkRandomStr-12 ? ? ? ? ? ? ? 6506 ? ? ? ? ? ?183098 ns/op PASS ok ? ? learn/learn_test ? ? ? ?10.030s ?
運(yùn)行次數(shù)和單次執(zhí)行時(shí)間已經(jīng)恢復(fù)到之前測(cè)試的情況了?;鶞?zhǔn)測(cè)試還有b.StopTimer()
和b.StartTimer()
方法也是同樣的道理,在影響耗時(shí)的操作之前停止計(jì)時(shí),完成之后再開(kāi)始計(jì)時(shí)。
查看內(nèi)存使用情況
我們?cè)僭u(píng)估代碼的性能時(shí),除了時(shí)間的快慢,還有一個(gè)重要的指標(biāo)就是內(nèi)存使用率,基準(zhǔn)測(cè)試中可以通過(guò) -benchmem
來(lái)顯示內(nèi)存使用情況。下面我們用一組指定cap和不指定cap的返回int切片方法來(lái)看一下內(nèi)存的使用情況
func getIntArr(n int) []int { rand.Seed(uint64(time.Now().UnixNano())) arr := make([]int, 0) for i := 0; i < n; i++ { arr = append(arr, rand.Int()) } ? return arr } ? func getIntArrWithCap(n int) []int { rand.Seed(uint64(time.Now().UnixNano())) arr := make([]int, 0, n) for i := 0; i < n; i++ { arr = append(arr, rand.Int()) } ? return arr } //------------------------------------------ // 基準(zhǔn)測(cè)試代碼 //------------------------------------------ func BenchmarkGetIntArr(b *testing.B) { for i := 0; i < b.N; i++ { getIntArr(100000) } } ? func BenchmarkGetIntArrWithCap(b *testing.B) { for i := 0; i < b.N; i++ { getIntArrWithCap(100000) } } ?
執(zhí)行基準(zhǔn)測(cè)試:
$ go test -bench='Arr' -benchmem . goos: darwin goarch: amd64 pkg: learn/learn_test cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkGetIntArr-12 ? ? ? ? ? ? ? ? ? ? ? ?598 ? ? ? ? ? 1928991 ns/op ? ? ? ? 4101389 B/op ? ? ? ? 28 allocs/op BenchmarkGetIntArrWithCap-12 ? ? ? ? ? ? ? ? 742 ? ? ? ? ? 1556204 ns/op ? ? ? ? ?802817 B/op ? ? ? ? ?1 allocs/op PASS ok ? ? learn/learn_test ? ? ? ?2.688s ?
可以看到指定了cap的方法執(zhí)行的速度大約快20%,而內(nèi)存的使用少了80%左右, 802817 B/op
代表每次的內(nèi)存使用情況,1 allocs/op
表示每次操作分配內(nèi)存的次數(shù)
testing.B的底層實(shí)現(xiàn)
在寫(xiě)基準(zhǔn)測(cè)試的時(shí)候,最讓我搞不懂的是b.N的機(jī)制,如何根據(jù)不同的用例來(lái)自動(dòng)調(diào)整執(zhí)行的次數(shù),然后我在源碼中找到了一些蛛絲馬跡。首先,先看一下基準(zhǔn)測(cè)試的底層數(shù)據(jù)結(jié)構(gòu)
type B struct { common importPath ? ? ? string context ? ? ? ? ?*benchContext N ? ? ? ? ? ? ? ?int // 這個(gè)就是要搞懂的N,代表要執(zhí)行的次數(shù) previousN ? ? ? ?int ? ? ? ? ? previousDuration time.Duration benchFunc ? ? ? ?func(b *B) // 測(cè)試函數(shù) benchTime ? ? ? ?durationOrCountFlag // 執(zhí)行時(shí)間,默認(rèn)是1s 可以通過(guò)-benchtime指定 bytes ? ? ? ? ? ?int64 missingBytes ? ? bool timerOn ? ? ? ? ?bool showAllocResult ?bool result ? ? ? ? ? BenchmarkResult parallelism ? ? ?int startAllocs uint64 startBytes ?uint64 netAllocs uint64 netBytes ?uint64 extra map[string]float64 }
通過(guò)結(jié)構(gòu)體中的N字段,可以找到幾個(gè)關(guān)鍵的方法,runN()
:每一次執(zhí)行都會(huì)調(diào)用的方法,設(shè)置N的值。run1()
:第一次迭代,根據(jù)它的結(jié)果決定是否需要運(yùn)行更多的基準(zhǔn)測(cè)試。run()
: run1()執(zhí)行的結(jié)果為true的情況會(huì)調(diào)用,這個(gè)方法里調(diào)用doBench()
函數(shù)從而調(diào)用launch()
函數(shù),這個(gè)是最終決定執(zhí)行次數(shù)的函數(shù)
// Run benchmarks f as a subbenchmark with the given name. It reports // whether there were any failures. // // A subbenchmark is like any other benchmark. A benchmark that calls Run at // least once will not be measured itself and will be called once with N=1. func (b *B) Run(name string, f func(b *B)) bool { // ...省略部分代碼 ?// Run()方法是基準(zhǔn)測(cè)試的啟動(dòng)方法,會(huì)新建一個(gè)子測(cè)試 sub := &B{ common: common{ signal: ?make(chan bool), name: ? ?benchName, parent: ?&b.common, level: ? b.level + 1, creator: pc[:n], w: ? ? ? b.w, chatty: ?b.chatty, bench: ? true, }, importPath: b.importPath, benchFunc: ?f, benchTime: ?b.benchTime, context: ? ?b.context, } // ...省略部分代碼 ?if sub.run1() { // 執(zhí)行一次子測(cè)試,如果不出錯(cuò)執(zhí)行run() ? ?sub.run() //最終調(diào)用 launch()方法,決定需要執(zhí)行多少次runN() } b.add(sub.result) return !sub.failed } ? // runN runs a single benchmark for the specified number of iterations. func (b *B) runN(n int) { // ....省略部分代碼 b.N = n //指定N // ... } // launch launches the benchmark function. It gradually increases the number // of benchmark iterations until the benchmark runs for the requested benchtime. // launch is run by the doBench function as a separate goroutine. // run1 must have been called on b. func (b *B) launch() { ?// ....省略部分代碼 d := b.benchTime.d ?// 最少執(zhí)行時(shí)間為1s,最多執(zhí)行次數(shù)為1e9次 for n := int64(1); !b.failed && b.duration < d && n < 1e9; { last := n // 預(yù)測(cè)所需要的迭代次數(shù) goalns := d.Nanoseconds() prevIters := int64(b.N) prevns := b.duration.Nanoseconds() if prevns <= 0 { //四舍五入,預(yù)防除0 prevns = 1 } n = goalns * prevIters / prevns ? ? ?// 避免增長(zhǎng)的太快,先按1.2倍增長(zhǎng),最少增加一次 n += n / 5 n = min(n, 100*last) n = max(n, last+1) // 最多執(zhí)行1e9次 n = min(n, 1e9) b.runN(int(n)) } ?
總結(jié)
1.基準(zhǔn)測(cè)試方法要以Benchmark開(kāi)頭
2.執(zhí)行基準(zhǔn)測(cè)試用go test -bench .命令執(zhí)行該目錄下所有的基準(zhǔn)測(cè)試,-bench后面可以跟正則表達(dá)式,來(lái)執(zhí)行符合條件的測(cè)試
3.-cpu參數(shù)可以指定運(yùn)行測(cè)試的cpu核心數(shù)
4.-benchtime參數(shù)可以指定運(yùn)行測(cè)試的時(shí)間和次數(shù)
5.-count參數(shù)可以指定運(yùn)行測(cè)試的輪數(shù)
6.b.ResetTimer()、b.StopTimer()、b.StartTimer()可以重置或暫停計(jì)時(shí),來(lái)消除一些耗時(shí)操作的影響
以上就是go通過(guò)benchmark對(duì)代碼進(jìn)行性能測(cè)試詳解的詳細(xì)內(nèi)容,更多關(guān)于go benchmark代碼性能測(cè)試的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語(yǔ)言實(shí)現(xiàn)請(qǐng)求超時(shí)處理的方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中實(shí)現(xiàn)請(qǐng)求的超時(shí)控制的方法,主要是通過(guò)timer和timerCtx來(lái)實(shí)現(xiàn)請(qǐng)求的超時(shí)控制,希望對(duì)大家有所幫助2023-05-05Golang?流水線設(shè)計(jì)模式實(shí)踐示例詳解
這篇文章主要為大家介紹了Golang?流水線設(shè)計(jì)模式實(shí)踐示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12golang不到30行代碼實(shí)現(xiàn)依賴注入的方法
這篇文章主要介紹了golang不到30行代碼實(shí)現(xiàn)依賴注入的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07Go語(yǔ)言基于viper實(shí)現(xiàn)apollo多實(shí)例快速
viper是適用于go應(yīng)用程序的配置解決方案,這款配置管理神器,支持多種類(lèi)型、開(kāi)箱即用、極易上手。本文主要介紹了如何基于viper實(shí)現(xiàn)apollo多實(shí)例快速接入,感興趣的可以了解一下2023-01-01go語(yǔ)言接口的定義和實(shí)現(xiàn)簡(jiǎn)單分享
這篇文章主要介紹了go語(yǔ)言接口的定義和實(shí)現(xiàn)簡(jiǎn)單分享的相關(guān)資料,需要的朋友可以參考下2023-08-08