Go語言之基準測試

什麼是基準測試


基準測試,是一種測試代碼性能的方法,比如你有多種不同的方案,都可以解決問題,那麼到底是那種方案性能更好呢?這時候基準測試就派上用場了。


基準測試主要是通過測試CPU和內存的效率問題,來評估被測試代碼的性能,進而找到更好的解決方案。比如鏈接池的數量不是越多越好,那麼哪個值纔是最優值呢,這就需要配合基準測試不斷調優了。


如何編寫基準測試


基準測試代碼的編寫和單元測試非常相似,它也有一定的規則,我們先看一個示例。


itoa_test.go


func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}


這是一個基準測試的例子,從中我們可以看出以下規則:


  • 基準測試的代碼文件必須以_test.go結尾。


  • 基準測試的函數必須以Benchmark開頭,必須是可導出的。


  • 基準測試函數必須接受一個指向Benchmark類型的指針作爲唯一參數。


  • 基準測試函數不能有返回值。


  • b.ResetTimer是重置計時器,這樣可以避免for循環之前的初始化代碼的干擾。


  • 最後的for循環很重要,被測試的代碼要放到循環裏。


  • b.N是基準測試框架提供的,表示循環的次數,因爲需要反覆調用測試的代碼,纔可以評估性能。


下面我們運行下基準測試,看看效果。


  hello go test -bench=. -run=none
BenchmarkSprintf-8      20000000               117 ns/op PASS ok      flysnow.org/hello       2.474s


運行基準測試也要使用go test命令,不過我們要加上-bench=標記,它接受一個表達式作爲參數,匹配基準測試的函數,.表示運行所有基準測試。


因爲默認情況下go test會運行單元測試,爲了防止單元測試的輸出影響我們查看基準測試的結果,可以使用-run=匹配一個從來沒有的單元測試方法,過濾掉單元測試的輸出,我們這裏使用none,因爲我們基本上不會創建這個名字的單元測試方法。


下面着重解釋下說出的結果,看到函數後面的-8了嗎?這個表示運行時對應的GOMAXPROCS的值。接着的20000000表示運行for循環的次數,也就是調用被測試代碼的次數,最後的117 ns/op表示每次需要話費 117 納秒。


以上是測試時間默認是 1 秒,也就是 1 秒的時間,調用兩千萬次,每次調用花費 117 納秒。如果想讓測試運行的時間更長,可以通過-benchtime指定,比如 3 秒。


  hello go test -bench=. -benchtime=3s -run=none
BenchmarkSprintf-8      50000000               109 ns/op PASS ok      flysnow.org/hello       5.628s


可以發現,我們加長了測試時間,測試的次數變多了,但是最終的性能結果:每次執行的時間,並沒有太大變化。一般來說這個值最好不要超過3秒,意義不大。


性能對比


上面那個基準測試的例子,其實是一個int類型轉爲string類型的例子,標準庫裏還有幾種方法,我們看下哪種性能更加。


func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }}func BenchmarkFormat(b *testing.B){
    num:=int64(10)
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.FormatInt(num,10)
    }}func BenchmarkItoa(b *testing.B){
    num:=10
    b.ResetTimer()    
    for i:=0;i<b.N;i++{
        strconv.Itoa(num)
    }
}


運行基準測試,看看結果:


  hello go test -bench=. -run=none              BenchmarkSprintf-8      20000000               117 ns/op
BenchmarkFormat-8       50000000                33.3 ns/op
BenchmarkItoa-8         50000000                34.9 ns/op PASS ok      flysnow.org/hello       5.951s


從結果上看strconv.FormatInt函數是最快的,其次是strconv.Itoa,然後是fmt.Sprintf最慢,前兩個函數性能達到了最後一個的 3 倍多。那麼最後一個爲什麼這麼慢的,我們再通過-benchmem找到根本原因。


  hello go test -bench=. -benchmem -run=none
BenchmarkSprintf-8      20000000       110 ns/op      16 B/op      2 allocs/op
BenchmarkFormat-8       50000000       31.0 ns/op     2 B/op       1 allocs/op
BenchmarkItoa-8         50000000       33.1 ns/op     2 B/op       1 allocs/op PASS ok      flysnow.org/hello       5.610s


-benchmem可以提供每次操作分配內存的次數,以及每次操作分配的字節數。從結果我們可以看到,性能高的兩個函數,每次操作都是進行 1 次內存分配,而最慢的那個要分配 2 次;性能高的每次操作分配 2 個字節內存,而慢的那個函數每次需要分配 16 字節的內存。從這個數據我們就知道它爲什麼這麼慢了,內存分配都佔用都太高。


在代碼開發中,對於我們要求性能的地方,編寫基準測試非常重要,這有助於我們開發出性能更好的代碼。不過性能、可用性、複用性等也要有一個相對的取捨,不能爲了追求性能而過度優化。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章