溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

go怎么通過(guò)benchmark對(duì)代碼進(jìn)行性能測(cè)試

發(fā)布時(shí)間:2023-04-26 10:35:35 來(lái)源:億速云 閱讀:76 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“go怎么通過(guò)benchmark對(duì)代碼進(jìn)行性能測(cè)試”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

benchmark的使用

在開發(fā)中我們要想編寫高性能的代碼,或者優(yōu)化代碼的性能時(shí),你首先得知道當(dāng)前代碼的性能,在go中可以使用testing包的benchmark來(lái)做基準(zhǔn)測(cè)試 ,首先我們寫一個(gè)簡(jiǎn)單的返回隨機(jī)字符串的方法

func randomStr(length int) string {
  mathRand.Seed(time.Now().UnixNano())
  letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  b := make([]byte, length)
  for i := range b {
    b[i] = letters[mathRand.Intn(len(letters))]
  }
  return string(b)
}

要對(duì)上面的代碼做基準(zhǔn)測(cè)試,首先我們要新建一個(gè)測(cè)試文件,比如main_test.go,然后新建一個(gè)基準(zhǔn)測(cè)試方法BenchmarkRandomStr,與普通的測(cè)試函數(shù)Test 開頭,參數(shù)為t *testing.T類似,基準(zhǔn)測(cè)試函數(shù)要以Benchmark開頭,參數(shù)為b *testing.B,代碼中的b.N代表的是該用例的運(yùn)行次數(shù),這個(gè)值是會(huì)變的,對(duì)于每個(gè)用例都不一樣,這個(gè)值會(huì)從1開始增加,具體的實(shí)現(xiàn)我會(huì)在下面的實(shí)現(xiàn)原理里進(jìn)行介紹。

func BenchmarkRandomStr(b *testing.B) {
  for i := 0; i < b.N; i++ {
    randomStr(10000)
  }
}

運(yùn)行Benchmark

我們可以使用 go test -bench .命令直接運(yùn)行當(dāng)前目錄下的所有基準(zhǔn)測(cè)試用例,-bench后面也可以跟正則或者是字符串來(lái)匹配對(duì)應(yīng)的用例

$  go test -bench='Str$'
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12               6692            181262 ns/op
PASS
ok      learn/learn_test        2.142s

對(duì)上面的一些關(guān)鍵指標(biāo)我們要了解一下,首先BenchmarkRandomStr-12后面的-12代表的是GOMAXPROCS這個(gè)跟你機(jī)器CPU的邏輯核數(shù)有關(guān),在基準(zhǔn)測(cè)試中可以通過(guò)-cpu參數(shù)指定需要以幾核的cpu來(lái)運(yùn)行測(cè)試用例

$  go test -bench='Str$' -cpu=2,4,8 .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-2        6715            181197 ns/op
BenchmarkRandomStr-4        6471            180249 ns/op
BenchmarkRandomStr-8        6616            179510 ns/op
PASS
ok      learn/learn_test        4.516s

6715181197 ns/op代表用例執(zhí)行了6715次,每次花費(fèi)的時(shí)間約為0.0001812s,總耗時(shí)約為1.2s(ns:s的換算為1000000000:1)

指定測(cè)試時(shí)長(zhǎng)或測(cè)試次數(shù)

-benchtime=3s 指定時(shí)長(zhǎng)

-benchtime=100000x 指定次數(shù)

-coun=3 指定輪數(shù)

$  go test -bench='Str$' -benchtime=3s .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12              19988            177572 ns/op
PASS
ok      learn/learn_test        5.384s

$ go test -bench='Str$' -benchtime=10000x .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12              10000            184832 ns/op
PASS
ok      learn/learn_test        1.870s

$ go test -bench='Str$' -count=2 . 
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12               6702            177048 ns/op
BenchmarkRandomStr-12               6482            177861 ns/op
PASS
ok      learn/learn_test        3.269s

重置時(shí)間和暫停計(jì)時(shí)

有時(shí)候我們的測(cè)試用例會(huì)需要一些前置準(zhǔn)備的耗時(shí)行為,這對(duì)我們的測(cè)試結(jié)果會(huì)產(chǎn)生影響,這個(gè)時(shí)候就需要在耗時(shí)操作后重置計(jì)時(shí)。下面我們用一個(gè)偽代碼來(lái)模擬一下

func BenchmarkRandomStr(b *testing.B) {
  time.Sleep(time.Second * 2) // 模擬耗時(shí)操作
  for i := 0; i < b.N; i++ {
    randomStr(10000)
  }
}

這時(shí)候我們?cè)賵?zhí)行一下用例

$ go test -bench='Str$' .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12                  1        2001588866 ns/op
PASS
ok      learn/learn_test        2.009s

發(fā)現(xiàn)只執(zhí)行了一次,時(shí)間變成了2s多,這顯然不符合我們的預(yù)期,這個(gè)時(shí)候需要調(diào)用b.ResetTime()來(lái)重置時(shí)間

func BenchmarkRandomStr(b *testing.B) {
  time.Sleep(time.Second * 2) // 模擬耗時(shí)操作
  b.ResetTimer() 
  for i := 0; i < b.N; i++ {
    randomStr(10000)
  }
}

再次執(zhí)行基準(zhǔn)測(cè)試

$ go test -bench='Str$' .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRandomStr-12               6506            183098 ns/op
PASS
ok      learn/learn_test        10.030s

運(yùn)行次數(shù)和單次執(zhí)行時(shí)間已經(jīng)恢復(fù)到之前測(cè)試的情況了?;鶞?zhǔn)測(cè)試還有b.StopTimer()b.StartTimer()方法也是同樣的道理,在影響耗時(shí)的操作之前停止計(jì)時(shí),完成之后再開始計(jì)時(shí)。

查看內(nèi)存使用情況

我們?cè)僭u(píng)估代碼的性能時(shí),除了時(shí)間的快慢,還有一個(gè)重要的指標(biāo)就是內(nèi)存使用率,基準(zhǔn)測(cè)試中可以通過(guò) -benchmem 來(lái)顯示內(nèi)存使用情況。下面我們用一組指定cap和不指定cap的返回int切片方法來(lái)看一下內(nèi)存的使用情況

func getIntArr(n int) []int {
  rand.Seed(uint64(time.Now().UnixNano()))
  arr := make([]int, 0)
  for i := 0; i < n; i++ {
    arr = append(arr, rand.Int())
  }

  return arr
}

func getIntArrWithCap(n int) []int {
  rand.Seed(uint64(time.Now().UnixNano()))
  arr := make([]int, 0, n)
  for i := 0; i < n; i++ {
    arr = append(arr, rand.Int())
  }

  return arr
}
//------------------------------------------
// 基準(zhǔn)測(cè)試代碼
//------------------------------------------
func BenchmarkGetIntArr(b *testing.B) {
  for i := 0; i < b.N; i++ {
    getIntArr(100000)
  }
}

func BenchmarkGetIntArrWithCap(b *testing.B) {
  for i := 0; i < b.N; i++ {
    getIntArrWithCap(100000)
  }
}

執(zhí)行基準(zhǔn)測(cè)試:

$ go test -bench='Arr' -benchmem .
goos: darwin
goarch: amd64
pkg: learn/learn_test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkGetIntArr-12                        598           1928991 ns/op         4101389 B/op         28 allocs/op
BenchmarkGetIntArrWithCap-12                 742           1556204 ns/op          802817 B/op          1 allocs/op
PASS
ok      learn/learn_test        2.688s

可以看到指定了cap的方法執(zhí)行的速度大約快20%,而內(nèi)存的使用少了80%左右, 802817 B/op 代表每次的內(nèi)存使用情況,1 allocs/op表示每次操作分配內(nèi)存的次數(shù)

testing.B的底層實(shí)現(xiàn)

在寫基準(zhǔn)測(cè)試的時(shí)候,最讓我搞不懂的是b.N的機(jī)制,如何根據(jù)不同的用例來(lái)自動(dòng)調(diào)整執(zhí)行的次數(shù),然后我在源碼中找到了一些蛛絲馬跡。首先,先看一下基準(zhǔn)測(cè)試的底層數(shù)據(jù)結(jié)構(gòu)

type B struct {
  common
  importPath       string
  context          *benchContext
  N                int // 這個(gè)就是要搞懂的N,代表要執(zhí)行的次數(shù)
  previousN        int          
  previousDuration time.Duration 
  benchFunc        func(b *B) // 測(cè)試函數(shù)
  benchTime        durationOrCountFlag // 執(zhí)行時(shí)間,默認(rèn)是1s 可以通過(guò)-benchtime指定
  bytes            int64 
  missingBytes     bool 
  timerOn          bool 
  showAllocResult  bool
  result           BenchmarkResult
  parallelism      int 
  
  startAllocs uint64 
  startBytes  uint64 
  
  netAllocs uint64 
  netBytes  uint64 
  
  extra map[string]float64
}

通過(guò)結(jié)構(gòu)體中的N字段,可以找到幾個(gè)關(guān)鍵的方法,runN():每一次執(zhí)行都會(huì)調(diào)用的方法,設(shè)置N的值。run1():第一次迭代,根據(jù)它的結(jié)果決定是否需要運(yùn)行更多的基準(zhǔn)測(cè)試。run(): run1()執(zhí)行的結(jié)果為true的情況會(huì)調(diào)用,這個(gè)方法里調(diào)用doBench()函數(shù)從而調(diào)用launch()函數(shù),這個(gè)是最終決定執(zhí)行次數(shù)的函數(shù)

// Run benchmarks f as a subbenchmark with the given name. It reports
// whether there were any failures.
//
// A subbenchmark is like any other benchmark. A benchmark that calls Run at
// least once will not be measured itself and will be called once with N=1.
func (b *B) Run(name string, f func(b *B)) bool {
  // ...省略部分代碼
  // Run()方法是基準(zhǔn)測(cè)試的啟動(dòng)方法,會(huì)新建一個(gè)子測(cè)試
  sub := &B{
    common: common{
      signal:  make(chan bool),
      name:    benchName,
      parent:  &b.common,
      level:   b.level + 1,
      creator: pc[:n],
      w:       b.w,
      chatty:  b.chatty,
      bench:   true,
    },
    importPath: b.importPath,
    benchFunc:  f,
    benchTime:  b.benchTime,
    context:    b.context,
  }
// ...省略部分代碼
  if sub.run1() { // 執(zhí)行一次子測(cè)試,如果不出錯(cuò)執(zhí)行run()
    sub.run() //最終調(diào)用 launch()方法,決定需要執(zhí)行多少次runN()
  }
  b.add(sub.result)
  return !sub.failed
}

// runN runs a single benchmark for the specified number of iterations.
func (b *B) runN(n int) {
	// ....省略部分代碼
	b.N = n //指定N
	// ...
}

// launch launches the benchmark function. It gradually increases the number
// of benchmark iterations until the benchmark runs for the requested benchtime.
// launch is run by the doBench function as a separate goroutine.
// run1 must have been called on b.
func (b *B) launch() {
  // ....省略部分代碼
    d := b.benchTime.d
  // 最少執(zhí)行時(shí)間為1s,最多執(zhí)行次數(shù)為1e9次
    for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
      last := n
      // 預(yù)測(cè)所需要的迭代次數(shù)
      goalns := d.Nanoseconds()
      prevIters := int64(b.N)
      prevns := b.duration.Nanoseconds()
      if prevns <= 0 {
        //四舍五入,預(yù)防除0
        prevns = 1
      }
      n = goalns * prevIters / prevns
      // 避免增長(zhǎng)的太快,先按1.2倍增長(zhǎng),最少增加一次
      n += n / 5
      n = min(n, 100*last)
      n = max(n, last+1)
      // 最多執(zhí)行1e9次
      n = min(n, 1e9)
      b.runN(int(n))
}

“go怎么通過(guò)benchmark對(duì)代碼進(jìn)行性能測(cè)試”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI