Go 語言基準測試入門

FunTester發表於2024-10-14

之前在寫 Java 的文章的時候,如果想在本地進行某段程式碼的效能測試(通常是對比另外一段或者幾段),就會用到基準測試框架 JMH ,也的確非常好用。雖然我學習 Go 語言有一段時間了,對於基準測試還沒有涉獵,下面就分享 Go 語言的基準測試入門實踐。

什麼是基準測試

基準測試(Benchmarking)是一種透過執行預定任務來測量系統或程式碼效能的測試方法,主要目的是評估軟體在特定負載和條件下的表現,常常關注執行時間、記憶體消耗、吞吐量和延遲等指標。在開發過程中,基準測試用於量化程式碼的效能差異,幫助開發者識別效能瓶頸併為最佳化提供有力的資料支援。

它的主要用途在於評估效能、監測效能迴歸、指導最佳化以及比較不同技術或方案。透過基準測試,開發者可以準確瞭解不同實現方案的效能表現,確保最佳化過程中不會引入效能退化的問題,確保程式碼在實際場景下的表現符合預期。基準測試的結果為找到效能瓶頸提供了資料依據,避免盲目最佳化的風險。

在效能最佳化中,基準測試的作用尤為重要。它不僅能提供清晰的定量分析,讓最佳化決策基於真實的資料,而非直覺或經驗,還能幫助快速發現程式碼中的效能瓶頸,如某個函式或 I/O 操作的效率低下。此外,基準測試也可以驗證最佳化後的效果,透過對比前後的資料,判斷所做的更改是否真正提升了效能。同時,基準測試還可以融入到持續整合和持續交付的流程中,確保系統在長期演進中的效能穩定性。

Go 語言的基準測試

Go 語言的基準測試用於評估程式碼的效能表現,尤其是函式和操作的執行時間。它透過多次執行相同的程式碼片段,計算平均耗時,提供準確的測試結果。基準測試常用於比較不同實現方案、驗證最佳化效果,確保程式碼在不同場景下的效率,並能夠檢測效能迴歸。

此外,Go 的基準測試支援設定不同的輸入條件,模擬實際負載,幫助開發者全面評估程式碼效能。它還能追蹤記憶體分配情況,發現潛在的記憶體問題。基準測試生成的報告為最佳化提供了有力的資料支援,使開發者能更有效地識別瓶頸並進行改進。

testing.B 是 Go 語言中的一個結構體,用於基準測試,專門用於測量程式碼的效能。它提供了執行基準測試的必要工具,例如控制測試執行的次數、記錄執行時間、追蹤記憶體分配等。在使用 testing.B 進行基準測試時,Go 會自動多次執行測試程式碼,以確保結果的穩定性和準確性,幫助開發者準確評估程式碼的效能表現並發現最佳化機會。

基準測試實踐

編寫規範

在開始之前,我們先來看看 Go 語言的基準測試用例編寫過程中,需要遵循的一些規範。在 Go 語言中,編寫基準測試時遵循一定的規範是非常重要的,這有助於確保測試的準確性、可讀性和可維護性。以下是一些基準測試編寫的基本規範:

  1. 命名規範:基準測試函式應以 Benchmark 開頭,後跟被測試功能的描述。命名應清晰明瞭,以便他人理解其測試的內容。例如:BenchmarkFunctionName
  2. 使用 testing.B:基準測試的引數必須是 *testing.B 型別,利用其提供的方法來執行效能測試。
  3. 迴圈測試:在基準測試中,使用 b.N 來控制迴圈次數,確保基準測試執行足夠多次,以獲取穩定的效能資料。b.N 的值由 Go 的測試框架自動管理,以避免人為干預。
  4. 避免副作用:基準測試應避免具有副作用的操作,確保每次執行的結果不受外部因素影響。確保測試函式中的程式碼是可重複執行的。
  5. 資源清理:如果基準測試中使用了需要清理的資源(如檔案、網路連線等),應在測試結束後確保資源得到妥善處理,儘量使用 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 方法及其功能:

  1. b.N:這是一個整數值,表示基準測試要執行的迴圈次數。開發者在基準測試中通常使用 b.N 來控制程式碼的執行次數,以獲取穩定的效能資料。
  2. b.ResetTimer():在基準測試中,可以呼叫此方法重置計時器。這在需要進行一些初始化操作後,確保不將這些操作的時間計入基準測試時非常有用。
  3. b.StopTimer():此方法用於停止基準測試的計時。可以在測試中使用此方法來排除某些不需要計入測試時間的操作,例如初始化或清理操作。
  4. b.StartTimer():用於重新啟動計時器。當需要停止計時並進行某些不希望計入測試的操作後,可以透過此方法恢復計時。
  5. b.ReportAllocs():呼叫此方法可以在基準測試結束時報告記憶體分配情況。如果希望監測記憶體的使用情況,可以在基準測試開始時呼叫此方法。
  6. b.Log(args ...interface{}):該方法允許開發者在基準測試中記錄資訊,類似於 fmt.Println。它可以用於除錯或輸出測試過程中獲取的某些特定資料。
  7. b.SetBytes(n int64):此方法用於設定測試中處理的資料大小,以便在報告中顯示吞吐量時提供更準確的資訊。通常用於計算每個操作處理的位元組數。
  8. b.Run(name string, f func(b *B)):這個方法允許在基準測試中執行子基準測試。這樣可以組織複雜的基準測試結構,同時提供更好的結果分析。

透過合理利用這些 API,開發者可以更靈活、高效地編寫基準測試,從而準確評估和最佳化程式碼效能。

FunTester 原創精華
  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章