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 報告出錯繼續Fatel / Fatelf 報告出錯終止Skip / Skipf 報告並跳過
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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。