之前在寫 Java 的文章的時候,如果想在本地進行某段程式碼的效能測試(通常是對比另外一段或者幾段),就會用到基準測試框架 JMH ,也的確非常好用。雖然我學習 Go 語言有一段時間了,對於基準測試還沒有涉獵,下面就分享 Go 語言的基準測試入門實踐。
什麼是基準測試
基準測試(Benchmarking)是一種透過執行預定任務來測量系統或程式碼效能的測試方法,主要目的是評估軟體在特定負載和條件下的表現,常常關注執行時間、記憶體消耗、吞吐量和延遲等指標。在開發過程中,基準測試用於量化程式碼的效能差異,幫助開發者識別效能瓶頸併為最佳化提供有力的資料支援。
它的主要用途在於評估效能、監測效能迴歸、指導最佳化以及比較不同技術或方案。透過基準測試,開發者可以準確瞭解不同實現方案的效能表現,確保最佳化過程中不會引入效能退化的問題,確保程式碼在實際場景下的表現符合預期。基準測試的結果為找到效能瓶頸提供了資料依據,避免盲目最佳化的風險。
在效能最佳化中,基準測試的作用尤為重要。它不僅能提供清晰的定量分析,讓最佳化決策基於真實的資料,而非直覺或經驗,還能幫助快速發現程式碼中的效能瓶頸,如某個函式或 I/O 操作的效率低下。此外,基準測試也可以驗證最佳化後的效果,透過對比前後的資料,判斷所做的更改是否真正提升了效能。同時,基準測試還可以融入到持續整合和持續交付的流程中,確保系統在長期演進中的效能穩定性。
Go 語言的基準測試
Go 語言的基準測試用於評估程式碼的效能表現,尤其是函式和操作的執行時間。它透過多次執行相同的程式碼片段,計算平均耗時,提供準確的測試結果。基準測試常用於比較不同實現方案、驗證最佳化效果,確保程式碼在不同場景下的效率,並能夠檢測效能迴歸。
此外,Go 的基準測試支援設定不同的輸入條件,模擬實際負載,幫助開發者全面評估程式碼效能。它還能追蹤記憶體分配情況,發現潛在的記憶體問題。基準測試生成的報告為最佳化提供了有力的資料支援,使開發者能更有效地識別瓶頸並進行改進。
testing.B 是 Go 語言中的一個結構體,用於基準測試,專門用於測量程式碼的效能。它提供了執行基準測試的必要工具,例如控制測試執行的次數、記錄執行時間、追蹤記憶體分配等。在使用 testing.B 進行基準測試時,Go 會自動多次執行測試程式碼,以確保結果的穩定性和準確性,幫助開發者準確評估程式碼的效能表現並發現最佳化機會。
基準測試實踐
編寫規範
在開始之前,我們先來看看 Go 語言的基準測試用例編寫過程中,需要遵循的一些規範。在 Go 語言中,編寫基準測試時遵循一定的規範是非常重要的,這有助於確保測試的準確性、可讀性和可維護性。以下是一些基準測試編寫的基本規範:
- 命名規範:基準測試函式應以
Benchmark
開頭,後跟被測試功能的描述。命名應清晰明瞭,以便他人理解其測試的內容。例如:BenchmarkFunctionName
。 - 使用
testing.B
:基準測試的引數必須是*testing.B
型別,利用其提供的方法來執行效能測試。 - 迴圈測試:在基準測試中,使用
b.N
來控制迴圈次數,確保基準測試執行足夠多次,以獲取穩定的效能資料。b.N
的值由 Go 的測試框架自動管理,以避免人為干預。 - 避免副作用:基準測試應避免具有副作用的操作,確保每次執行的結果不受外部因素影響。確保測試函式中的程式碼是可重複執行的。
- 資源清理:如果基準測試中使用了需要清理的資源(如檔案、網路連線等),應在測試結束後確保資源得到妥善處理,儘量使用 defer 語句來保證清理操作的執行。
程式碼實踐
下面我透過一些簡單的例子展示 Go 語言基準測試的 code,我用了兩種字串拼接的方法來演示。
package test
import (
"strings"
"testing")
// 基準測試示例:測試使用 + 運算子連線字串的效能
func BenchmarkStringConcat(b *testing.B) {
setConfig(b)
str1 := "Hello"
str2 := "FunTester"
for i := 0; i < b.N; i++ {
_ = str1 + str2
}
}
// 基準測試示例:測試使用 strings.Builder 連線字串的效能
func BenchmarkStringBuilder(b *testing.B) {
setConfig(b)
str1 := "Hello"
str2 := "FunTester"
for i := 0; i < b.N; i++ {
var builder strings.Builder
builder.WriteString(str1)
builder.WriteString(str2)
_ = builder.String()
}
}
func setConfig(b *testing.B) {
b.SetParallelism(4) // 設定並行測試的 goroutine 數量
b.ReportAllocs() // 開啟記憶體分配跟蹤
b.SetBytes(1024) // 設定每次操作處理的位元組數
}
我們可以直接在 IDE 中執行,也可以透過以下命令列執行 benchmark 基準測試。
go test -bench . -cpu 1,2,4,8,12
控制檯輸出:
goos: darwin
goarch: arm64
pkg: tempProject/test
BenchmarkStringConcat-2 100000000 11.61 ns/op 88166.09 MB/s 0 B/op 0 allocs/op
BenchmarkStringConcat-4 100000000 11.71 ns/op 87425.82 MB/s 0 B/op 0 allocs/op
BenchmarkStringConcat-8 100000000 11.62 ns/op 88098.01 MB/s 0 B/op 0 allocs/op
BenchmarkStringBuilder-2 34372545 33.96 ns/op 30155.42 MB/s 24 B/op 2 allocs/op
BenchmarkStringBuilder-4 35140291 33.75 ns/op 30341.89 MB/s 24 B/op 2 allocs/op
BenchmarkStringBuilder-8 34851384 33.99 ns/op 30129.57 MB/s 24 B/op 2 allocs/op
PASS
ok tempProject/test 7.951s
測試報告解讀
測試報告的解讀可以從以下幾個方面進行分析:
環境資訊:
- goos: darwin:表示作業系統是 macOS(Darwin 是 macOS 的核心)。
- goarch: arm64:表示執行的架構是 ARM64(常見於蘋果的 M1 和 M2 晶片)。
- pkg: tempProject/test:表示基準測試執行在 tempProject/test 包中。
基準測試結果:
每一行結果的格式如下:
BenchmarkName-N 總執行次數 平均耗時 吞吐量 記憶體使用 記憶體分配次數
基準測試:
- BenchmarkStringConcat-2:使用 + 運算子連線字串,執行 100,000,000 次,平均耗時 11.61 ns/op,吞吐量 88166.09 MB/s,記憶體使用 0 B/op,記憶體分配次數 0 allocs/op。
- BenchmarkStringConcat-4:相似的效能,平均耗時 11.71 ns/op,吞吐量稍低 87425.82 MB/s。
- BenchmarkStringConcat-8:表現相近,平均耗時 11.62 ns/op,吞吐量 88098.01 MB/s。
測試結論
- 效能對比:BenchmarkStringConcat 的效能明顯優於 BenchmarkStringBuilder,適合用於簡單字串連線的場景。
- 記憶體管理:BenchmarkStringConcat 沒有進行任何記憶體分配,而 BenchmarkStringBuilder 則有一定的記憶體使用和分配次數,顯示出在效能敏感的環境中,選擇合適的字串連線方式至關重要。
- 測試時長:整個測試執行的時間為 7.951s,這個時間在基準測試中是合理的,表明測試的複雜度和工作量適中。
透過以上測試,可以幫助我們在實際專案中選擇合適的字串操作方式,以最佳化效能和記憶體使用。
testing.B 常用 API
在 Go 語言的基準測試中,*testing.B
提供了一系列常用的 API,幫助我們進行高效的效能測試和評估。以下是一些常用的 *testing.B
方法及其功能:
-
b.N
:這是一個整數值,表示基準測試要執行的迴圈次數。開發者在基準測試中通常使用b.N
來控制程式碼的執行次數,以獲取穩定的效能資料。 -
b.ResetTimer()
:在基準測試中,可以呼叫此方法重置計時器。這在需要進行一些初始化操作後,確保不將這些操作的時間計入基準測試時非常有用。 -
b.StopTimer()
:此方法用於停止基準測試的計時。可以在測試中使用此方法來排除某些不需要計入測試時間的操作,例如初始化或清理操作。 -
b.StartTimer()
:用於重新啟動計時器。當需要停止計時並進行某些不希望計入測試的操作後,可以透過此方法恢復計時。 -
b.ReportAllocs()
:呼叫此方法可以在基準測試結束時報告記憶體分配情況。如果希望監測記憶體的使用情況,可以在基準測試開始時呼叫此方法。 -
b.Log(args ...interface{})
:該方法允許開發者在基準測試中記錄資訊,類似於fmt.Println
。它可以用於除錯或輸出測試過程中獲取的某些特定資料。 -
b.SetBytes(n int64)
:此方法用於設定測試中處理的資料大小,以便在報告中顯示吞吐量時提供更準確的資訊。通常用於計算每個操作處理的位元組數。 -
b.Run(name string, f func(b *B))
:這個方法允許在基準測試中執行子基準測試。這樣可以組織複雜的基準測試結構,同時提供更好的結果分析。
透過合理利用這些 API,開發者可以更靈活、高效地編寫基準測試,從而準確評估和最佳化程式碼效能。
FunTester 原創精華
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片