官术网_书友最值得收藏!

46.4 排除額外干擾,讓基準測試更精確

從前面對順序執(zhí)行和并行執(zhí)行的性能基準測試原理的介紹可知,每個基準測試都可能會運行多輪,每個BenchmarkXxx函數都可能會被執(zhí)行多次。有些復雜的基準測試在真正執(zhí)行For循環(huán)之前或者在每個循環(huán)中,除了執(zhí)行真正的被測代碼之外,可能還需要做一些測試準備工作,比如建立基準測試所需的測試上下文等。如果不做特殊處理,這些測試準備工作所消耗的時間也會被算入最終結果中,這就會導致最終基準測試的數據受到干擾而不夠精確。為此,testing.B中提供了多種靈活操控基準測試計時器的方法,通過這些方法可以排除掉額外干擾,讓基準測試結果更能反映被測代碼的真實性能。來看一個例子:

// chapter8/sources/benchmark_with_expensive_context_setup_test.go

var sl = []string{
    "Rob Pike ",
    "Robert Griesemer ",
    "Ken Thompson ",
}

func concatStringByJoin(sl []string) string {
    return strings.Join(sl, "")
}

func expensiveTestContextSetup() {
    time.Sleep(200 * time.Millisecond)
}

func BenchmarkStrcatWithTestContextSetup(b *testing.B) {
    expensiveTestContextSetup()
    for n := 0; n < b.N; n++ {
        concatStringByJoin(sl)
    }
}

func BenchmarkStrcatWithTestContextSetupAndResetTimer(b *testing.B) {
    expensiveTestContextSetup()
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        concatStringByJoin(sl)
    }
}

func BenchmarkStrcatWithTestContextSetupAndRestartTimer(b *testing.B) {
    b.StopTimer()
    expensiveTestContextSetup()
    b.StartTimer()
    for n := 0; n < b.N; n++ {
        concatStringByJoin(sl)
    }
}

func BenchmarkStrcat(b *testing.B) {
    for n := 0; n < b.N; n++ {
        concatStringByJoin(sl)
    }
}

在這個例子中,我們來對比一下不建立測試上下文、建立測試上下文以及在對計時器控制下建立測試上下文等情況下的基準測試數據:

$go test -bench . benchmark_with_expensive_context_setup_test.go
goos: darwin
goarch: amd64
BenchmarkStrcatWithTestContextSetup-8                 16943037     65.9 ns/op
BenchmarkStrcatWithTestContextSetupAndResetTimer-8    21700249     52.7 ns/op
BenchmarkStrcatWithTestContextSetupAndRestartTimer-8  21628669     50.5 ns/op
BenchmarkStrcat-8                                     22915291     50.7 ns/op
PASS
ok       command-line-arguments 9.838s

我們看到,如果不通過testing.B提供的計數器控制接口對測試上下文帶來的消耗進行隔離,最終基準測試得到的數據(BenchmarkStrcatWithTestContextSetup)將偏離準確數據(BenchmarkStrcat)很遠。而通過testing.B提供的計數器控制接口對測試上下文帶來的消耗進行隔離后,得到的基準測試數據(BenchmarkStrcatWithTestContextSetupAndResetTimer和Bench-markStrcatWithTestContextSetupAndRestartTimer)則非常接近真實數據。

雖然在上面的例子中,ResetTimer和StopTimer/StartTimer組合都能實現對測試上下文帶來的消耗進行隔離的目的,但二者是有差別的:ResetTimer并不停掉計時器(無論計時器是否在工作),而是將已消耗的時間、內存分配計數器等全部清零,這樣即便計數器依然在工作,它仍然需要從零開始重新記;而StopTimer只是停掉一次基準測試運行的計時器,在調用StartTimer后,計時器即恢復正常工作。

但這樣一來,將ResetTimer或StopTimer用在每個基準測試的For循環(huán)中是有副作用的。在默認情況下,每個性能基準測試函數的執(zhí)行時間為1秒。如果執(zhí)行一輪所消耗的時間不足1秒,那么會修改b.N值并啟動新的一輪執(zhí)行。這樣一旦在For循環(huán)中使用StopTimer,那么想要真正運行1秒就要等待很長時間;而如果在For循環(huán)中使用了ResetTimer,由于其每次執(zhí)行都會將計數器數據清零,因此這輪基準測試將一直執(zhí)行下去,無法退出。綜上,盡量不要在基準測試的For循環(huán)中使用ResetTimer!但可以在限定條件下在For循環(huán)中使用StopTimer/StartTimer,就像下面的Go標準庫中這樣:

// $GOROOT/src/runtime/map_test.go
func benchmarkMapDeleteInt32(b *testing.B, n int) {
    a := make(map[int32]int, n)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        if len(a) == 0 {
            b.StopTimer()
            for j := i; j < i+n; j++ {
                a[int32(j)] = j
            }
            b.StartTimer()
        }
        delete(a, int32(i))
    }
}

上面的測試代碼雖然在基準測試的For循環(huán)中使用了StopTimer,但其是在if len(a) == 0這個限定條件下使用的,StopTimer方法并不會在每次循環(huán)中都被調用。

小結

無論你是否認為性能很重要,都請你為被測代碼(尤其是位于系統(tǒng)關鍵業(yè)務路徑上的代碼)建立性能基準。如果你編寫的是供其他人使用的軟件包,則更應如此。只有這樣,我們才能至少保證后續(xù)對代碼的修改不會帶來性能回退。已經建立的性能基準可以為后續(xù)是否進一步優(yōu)化的決策提供數據支撐,而不是靠程序員的直覺。

本條要點:

  • 性能基準測試在Go語言中是“一等公民”,在Go中我們可以很容易為被測代碼建立性能基準;
  • 了解Go的兩種性能基準測試的執(zhí)行原理;
  • 使用性能比較工具協(xié)助解讀測試結果數據,優(yōu)先使用benchstat工具;
  • 使用testing.B提供的定時器操作方法排除額外干擾,讓基準測試更精確,但不要在Run-Parallel中使用ResetTimer、StartTimer和StopTimer,因為它們具有全局副作用。
主站蜘蛛池模板: 齐齐哈尔市| 南康市| 英德市| 和静县| 茌平县| 湾仔区| 苏尼特右旗| 九龙坡区| 深水埗区| 菏泽市| 河源市| 青海省| 萍乡市| 红河县| 延边| 安泽县| 石台县| 博罗县| 武隆县| 渝中区| 察雅县| 子洲县| 定结县| 鲁山县| 海阳市| 都昌县| 卢湾区| 平利县| 平武县| 汶上县| 泰兴市| 盐山县| 察雅县| 胶州市| 龙井市| 沙雅县| 河南省| 麻栗坡县| 绥江县| 丘北县| 旌德县|