golang benchmark源碼分析

testing包提供了對Go包的自動測試支持。這是和go test 命令相呼應的功能, go test 命令會自動執行因此符合格式php

func TestXXX(t *testing.T) 

當帶着 -bench=「.」 ( 參數必須有!)來執行*go test命令的時候性能測試程序就會被順序執行。符合下面格式的函數被認爲是一個性能測試程序,css

func BenchmarkXxx(b *testing.B)

執行 go test -bench=」.」 後 結果 :golang

// 表示測試所有經過算法

>PASS // Benchmark 名字 - CPU 循環次數 平均每次執行時間 BenchmarkLoops-2 100000 20628 ns/op BenchmarkLoopsParallel-2 100000 10412 ns/op // 哪一個目錄下執行go test 累計耗時ok swap/lib 2.279s

源碼包位置:src/testing/benchmark.goapache

testing目錄下的文件有
swift

allocs.go               helper_test.go          quickallocs_test.go helperfuncs_test.go run_example.gobenchmark.go internal run_example_js.gobenchmark_test.go iotest sub_test.gocover.go match.go testing.goexample.go match_test.go testing_test.goexport_test.go panic_test.go

testing.T微信

斷定失敗接口數據結構

Fail 失敗繼續FailNow 失敗終止

打印信息接口架構

Log 數據流 (cout 相似)Logf format (printf 相似)SkipNow 跳過當前測試Skiped 檢測是否跳過

綜合接口產生:函數


Error / Errorf 報告出錯繼續 [ Log / Logf + Fail ]Fatel / Fatelf 報告出錯終止 [ Log / Logf + FailNow ]Skip / Skipf 報告並跳過 [ Log / Logf + SkipNow ]

testing.B

首先 , testing.B 擁有testing.T 的所有接口。

SetBytes( i uint64) 統計內存消耗, 若是你須要的話。SetParallelism(p int) 制定並行數目。StartTimer / StopTimer / ResertTimer 操做計時器testing.PBNext() 接口 。判斷是否繼續循環

下面帶着三個問題去閱讀源碼:

  • b.N是如何自動調整的?

  • 內存統計是如何實現的?

  • SetBytes()其使用場景是什麼?


B定義了性能測試的數據結構,咱們提取其比較重要的一些成員進行分析:

type B struct { common // 與testing.T共享的testing.common,負責記錄日誌、狀態等 importPath string // import path of the package containing the benchmark context *benchContext N int // 目標代碼執行次數,不須要用戶瞭解具體值,會自動調整 previousN int // number of iterations in the previous run previousDuration time.Duration // total duration of the previous run benchFunc func(b *B) // 性能測試函數 benchTime time.Duration // 性能測試函數最少執行的時間,默認爲1s,能夠經過參數'-benchtime 10s'指定 bytes int64 // 每次迭代處理的字節數 missingBytes bool // one of the subbenchmarks does not have bytes set. timerOn bool // 是否已開始計時 showAllocResult bool result BenchmarkResult // 測試結果 parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines // The initial states of memStats.Mallocs and memStats.TotalAlloc. startAllocs uint64 // 計時開始時堆中分配的對象總數 startBytes uint64 // 計時開始時時堆中分配的字節總數 // The net total of this test after being run. netAllocs uint64 // 計時結束時,堆中增長的對象總數 netBytes uint64 // 計時結束時,堆中增長的字節總數}

啓動計時:B.StartTimer()

StartTimer()負責啓動計時並初始化內存相關計數,測試執行時會自動調用,通常不須要用戶啓動。

func (b *B) StartTimer() { if !b.timerOn { runtime.ReadMemStats(&memStats) // 讀取當前堆內存分配信息 b.startAllocs = memStats.Mallocs // 記錄當前堆內存分配的對象數 b.startBytes = memStats.TotalAlloc // 記錄當前堆內存分配的字節數 b.start = time.Now() // 記錄測試啓動時間 b.timerOn = true // 標記計時標誌 }}

StartTimer()負責啓動計時,並記錄當前內存分配狀況,不論是否有「-benchmem」參數,內存都會被統計,參數只決定是否要在結果中輸出。


中止計時:B.StopTimer()

StopTimer()負責中止計時,並累加相應的統計值。

func (b *B) StopTimer() { if b.timerOn { b.duration += time.Since(b.start) // 累加測試耗時 runtime.ReadMemStats(&memStats) // 讀取當前堆內存分配信息 b.netAllocs += memStats.Mallocs - b.startAllocs // 累加堆內存分配的對象數 b.netBytes += memStats.TotalAlloc - b.startBytes // 累加堆內存分配的字節數 b.timerOn = false // 標記計時標誌 }}

須要注意的是,StopTimer()並不必定是測試結束,一個測試中有可能有多個統計階段,因此其統計值是累加的。


重置計時:B.ResetTimer()

ResetTimer()用於重置計時器,相應的也會把其餘統計值也重置。

func (b *B) ResetTimer() { if b.timerOn { runtime.ReadMemStats(&memStats) // 讀取當前堆內存分配信息 b.startAllocs = memStats.Mallocs // 記錄當前堆內存分配的對象數 b.startBytes = memStats.TotalAlloc // 記錄當前堆內存分配的字節數 b.start = time.Now() // 記錄測試啓動時間 } b.duration = 0 // 清空耗時 b.netAllocs = 0 // 清空內存分配對象數 b.netBytes = 0 // 清空內存分配字節數}

ResetTimer()比較經常使用,典型使用場景是一個測試中,初始化部分耗時較長,初始化後再開始計時。


設置處理字節數:B.SetBytes(n int64)

// SetBytes records the number of bytes processed in a single operation.

// If this is called, the benchmark will report ns/op and MB/s.

func (b *B) SetBytes(n int64) { b.bytes = n}

這是一個比較含糊的函數,經過其函數說明很難明白其做用。

其實它是用來設置單次迭代處理的字節數,一旦設置了這個字節數,那麼輸出報告中將會呈現「xxx MB/s」的信息,用來表示待測函數處理字節的性能。待測函數每次處理多少字節數只有用戶清楚,因此須要用戶設置。


報告內存信息:

func (b *B) ReportAllocs() { b.showAllocResult = true}

ReportAllocs() 用於設置是否打印內存統計信息,與命令行參數「-benchmem」一致,但本方法只做用於單個測試函數。

性能測試是如何啓動的

性能測試要通過屢次迭代,每次迭代可能會有不一樣的b.N值,每次迭代執行測試函數一次,跟據這次迭代的測試結果來分析要不要繼續下一次迭代。


咱們先看一下每次迭代時所用到的方法,runN():

func (b *B) runN(n int) { b.N = n // 指定B.N b.ResetTimer() // 清空統計數據 b.StartTimer() // 開始計時 b.benchFunc(b) // 執行測試 b.StopTimer() // 中止計時}

該方法指定b.N的值,執行一次測試函數。


與T.Run()相似,B.Run()也用於啓動一個子測試,實際上用戶編寫的任何一個測試都是使用Run()方法啓動的,咱們看下B.Run()的僞代碼:

func (b *B) Run(name string, f func(b *B)) bool { sub := &B{ // 新建子測試數據結構 common: common{ signal: make(chan bool), name: name, parent: &b.common, }, benchFunc: f, } if sub.run1() { // 先執行一次子測試,若是子測試不出錯且子測試沒有子測試的話繼續執行sub.run() sub.run() // run()裏決定要執行多少次runN() } b.add(sub.result) // 累加統計結果到父測試中 return !sub.failed}

全部的測試都是先使用run1()方法執行一次測試,run1()方法中實際上調用了runN(1),執行一次後再決定要不要繼續迭代。


測試結果實際上以最後一次迭代的數據爲準,固然,最後一次迭代每每意味着b.N更大,測試準確性相對更高。


B.N是如何調整的?

B.launch()方法裏最終決定B.N的值。咱們看下僞代碼:

func (b *B) launch() { // 此方法自動測算執行次數,但調用前必須調用run1以便自動計算次數 d := b.benchTime for n := 1; !b.failed && b.duration < d && n < 1e9; { // 最少執行b.benchTime(默認爲1s)時間,最多執行1e9次 last := n n = int(d.Nanoseconds()) // 預測接下來要執行多少次,b.benchTime/每一個操做耗時 if nsop := b.nsPerOp(); nsop != 0 { n /= int(nsop) } n = max(min(n+n/5, 100*last), last+1) // 避免增加較快,先增加20%,至少增加1次 n = roundUp(n) // 下次迭代次數向上取整到10的指數,方便閱讀 b.runN(n) }}

不考慮程序出錯,並且用戶沒有主動中止測試的場景下,每一個性能測試至少要執行b.benchTime長的秒數,默認爲1s。先執行一遍的意義在於看用戶代碼執行一次要花費多長時間,若是時間較短,那麼b.N值要足夠大才能夠測得更精確,若是時間較長,b.N值相應的會減小,不然會影響測試效率。


最終的b.N會被定格在某個10的指數級,是爲了方便閱讀測試報告。


內存是如何統計的?

咱們知道在測試開始時,會把當前內存值記入到b.startAllocs和b.startBytes中,測試結束時,會用最終內存值與開始時的內存值相減,獲得淨增長的內存值,並記入到b.netAllocs和b.netBytes中。


每一個測試結束,會把結果保存到BenchmarkResult對象裏,該對象裏保存了輸出報告所必需的統計信息:

type BenchmarkResult struct { N int // 用戶代碼執行的次數 T time.Duration // 測試耗時 Bytes int64 // 用戶代碼每次處理的字節數,SetBytes()設置的值 MemAllocs uint64 // 內存對象淨增長值 MemBytes uint64 // 內存字節淨增長值}

其中MemAllocs和MemBytes分別對應b.netAllocs和b.netBytes。

那麼最終統計時只須要把淨增長值除以b.N便可獲得每次新增多少內存了。

每一個操做內存對象新增值:

func (r BenchmarkResult) AllocsPerOp() int64 { return int64(r.MemAllocs) / int64(r.N)}

每一個操做內存字節數新增值:

func (r BenchmarkResult) AllocedBytesPerOp() int64 { if r.N <= 0 { return 0 } return int64(r.MemBytes) / int64(r.N)}


本文分享自微信公衆號 - golang算法架構leetcode技術php(golangLeetcode)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索