## 11.4. 基準測試
基準測試是測量一個程序在固定工作負載下的性能。在Go語言中,基準測試函數和普通測試函數寫法類似,但是以Benchmark為前綴名,并且帶有一個`*testing.B`類型的參數;`*testing.B`參數除了提供和`*testing.T`類似的方法,還有額外一些和性能測量相關的方法。它還提供了一個整數N,用于指定操作執行的循環次數。
下面是IsPalindrome函數的基準測試,其中循環將執行N次。
```Go
import "testing"
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
```
我們用下面的命令運行基準測試。和普通測試不同的是,默認情況下不運行任何基準測試。我們需要通過`-bench`命令行標志參數手工指定要運行的基準測試函數。該參數是一個正則表達式,用于匹配要執行的基準測試函數的名字,默認值是空的。其中“.”模式將可以匹配所有基準測試函數,但因為這里只有一個基準測試函數,因此和`-bench=IsPalindrome`參數是等價的效果。
```
$ cd $GOPATH/src/gopl.io/ch11/word2
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s
```
結果中基準測試名的數字后綴部分,這里是8,表示運行時對應的GOMAXPROCS的值,這對于一些與并發相關的基準測試是重要的信息。
報告顯示每次調用IsPalindrome函數花費1.035微秒,是執行1,000,000次的平均時間。因為基準測試驅動器開始時并不知道每個基準測試函數運行所花的時間,它會嘗試在真正運行基準測試前先嘗試用較小的N運行測試來估算基準測試函數所需要的時間,然后推斷一個較大的時間保證穩定的測量結果。
循環在基準測試函數內實現,而不是放在基準測試框架內實現,這樣可以讓每個基準測試函數有機會在循環啟動前執行初始化代碼,這樣并不會顯著影響每次迭代的平均運行時間。如果還是擔心初始化代碼部分對測量時間帶來干擾,那么可以通過testing.B參數提供的方法來臨時關閉或重置計時器,不過這些一般很少會用到。
現在我們有了一個基準測試和普通測試,我們可以很容易測試改進程序運行速度的想法。也許最明顯的優化是在IsPalindrome函數中第二個循環的停止檢查,這樣可以避免每個比較都做兩次:
```Go
n := len(letters)/2
for i := 0; i < n; i++ {
if letters[i] != letters[len(letters)-1-i] {
return false
}
}
return true
```
不過很多情況下,一個顯而易見的優化未必能帶來預期的效果。這個改進在基準測試中只帶來了4%的性能提升。
```
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 992 ns/op
ok gopl.io/ch11/word2 2.093s
```
另一個改進想法是在開始為每個字符預先分配一個足夠大的數組,這樣就可以避免在append調用時可能會導致內存的多次重新分配。聲明一個letters數組變量,并指定合適的大小,像下面這樣,
```Go
letters := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsLetter(r) {
letters = append(letters, unicode.ToLower(r))
}
}
```
這個改進提升性能約35%,報告結果是基于2,000,000次迭代的平均運行時間統計。
```
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 2000000 697 ns/op
ok gopl.io/ch11/word2 1.468s
```
如這個例子所示,快的程序往往是伴隨著較少的內存分配。`-benchmem`命令行標志參數將在報告中包含內存的分配數據統計。我們可以比較優化前后內存的分配情況:
```
$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
```
這是優化之后的結果:
```
$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
```
用一次內存分配代替多次的內存分配節省了75%的分配調用次數和減少近一半的內存需求。
這個基準測試告訴了我們某個具體操作所需的絕對時間,但我們往往想知道的是兩個不同的操作的時間對比。例如,如果一個函數需要1ms處理1,000個元素,那么處理10000或1百萬將需要多少時間呢?這樣的比較揭示了漸近增長函數的運行時間。另一個例子:I/O緩存該設置為多大呢?基準測試可以幫助我們選擇在性能達標情況下所需的最小內存。第三個例子:對于一個確定的工作哪種算法更好?基準測試可以評估兩種不同算法對于相同的輸入在不同的場景和負載下的優缺點。
比較型的基準測試就是普通程序代碼。它們通常是單參數的函數,由幾個不同數量級的基準測試函數調用,就像這樣:
```Go
func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B) { benchmark(b, 10) }
func Benchmark100(b *testing.B) { benchmark(b, 100) }
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
```
通過函數參數來指定輸入的大小,但是參數變量對于每個具體的基準測試都是固定的。要避免直接修改b.N來控制輸入的大小。除非你將它作為一個固定大小的迭代計算輸入,否則基準測試的結果將毫無意義。
比較型的基準測試反映出的模式在程序設計階段是很有幫助的,但是即使程序完工了也應當保留基準測試代碼。因為隨著項目的發展,或者是輸入的增加,或者是部署到新的操作系統或不同的處理器,我們可以再次用基準測試來幫助我們改進設計。
**練習 11.6:** 為2.6.2節的練習2.4和練習2.5的PopCount函數編寫基準測試。看看基于表格算法在不同情況下對提升性能會有多大幫助。
**練習 11.7:** 為`*IntSet`(§6.5)的Add、UnionWith和其他方法編寫基準測試,使用大量隨機輸入。你可以讓這些方法跑多快?選擇字的大小對于性能的影響如何?IntSet和基于內建map的實現相比有多快?
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言