Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19526] ファイルの概要

このコミットは、Go言語の標準ライブラリであるtestingパッケージ内のbenchmark.goファイルに対する変更です。benchmark.goは、Goのベンチマークテストの実行ロジック、特にベンチマーク関数が実行されるイテレーション数(b.N)を動的に決定するメカニズムを管理しています。このファイルは、ベンチマークの精度と実行時間のバランスを取りながら、効率的なベンチマーク実行を可能にするための重要な役割を担っています。

コミット

このコミットは、Go言語のベンチマーク実行を高速化することを目的としています。具体的には、ベンチマークのイテレーション数を決定するロジックを改善し、特に高速なベンチマークにおいて、イテレーション数の増加をより迅速に行うように変更しています。また、イテレーション数の丸め処理が二重に行われていた点を修正し、全体的なベンチマーク実行時間の短縮を実現しています。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5bb1af43214c3da52d1752e58d03ed968e6a11b

元コミット内容

commit a5bb1af43214c3da52d1752e58d03ed968e6a11b
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Thu Jun 12 07:51:32 2014 -0700

    testing: make benchmarking faster
    
    Allow the number of benchmark iterations to grow faster for fast benchmarks, and don't round up twice.
    
    Using the default benchtime, this CL reduces wall clock time to run benchmarks:
    
    net/http        49s   -> 37s   (-24%)
    runtime         8m31s -> 5m55s (-30%)
    bytes           2m37s -> 1m29s (-43%)
    encoding/json   29s   -> 21s   (-27%)
    strings         1m16s -> 53s   (-30%)
    
    LGTM=crawshaw
    R=golang-codereviews, crawshaw
    CC=golang-codereviews
    https://golang.org/cl/101970047

変更の背景

Goのベンチマークシステムは、指定されたbenchtime(デフォルトは1秒)を満たすように、ベンチマーク関数が実行されるイテレーション数(b.N)を動的に調整します。しかし、この調整ロジックには改善の余地がありました。特に、非常に高速なベンチマークの場合、イテレーション数b.Nの増加が遅く、結果としてbenchtimeを満たすまでに多くの試行(サブベンチマーク実行)が必要となり、全体のベンチマーク実行時間が長くなるという問題がありました。

このコミットの背景には、以下の具体的な課題がありました。

  1. 高速なベンチマークにおけるイテレーション数増加の遅さ: 以前のロジックでは、前回のイテレーション数lastが小さい場合でも、n(次のイテレーション数)の増加率が制限されていました。これにより、b.Nがなかなか十分な値に達せず、ベンチマークがbenchtimeに到達するまでに多くの短い実行を繰り返す必要がありました。
  2. 二重の丸め処理: イテレーション数の計算において、roundUp関数による丸め処理が実質的に二重に行われており、これが効率を低下させていました。

これらの課題を解決し、ベンチマークの実行時間を短縮することが、このコミットの主な動機となっています。コミットメッセージに記載されているように、この変更により、net/httpruntimebytesencoding/jsonstringsといった主要なパッケージのベンチマーク実行時間が大幅に短縮されています。

前提知識の解説

Goのベンチマークシステム

Go言語には、標準でベンチマークテストをサポートするtestingパッケージが用意されています。ベンチマークテストは、関数のパフォーマンスを測定するために使用され、go test -bench=.コマンドで実行できます。

  • ベンチマーク関数: func BenchmarkXxx(b *testing.B)というシグネチャを持つ関数がベンチマーク関数として認識されます。
  • *testing.B: ベンチマーク実行のコンテキストを提供する構造体です。
    • b.N: ベンチマーク関数が実行されるイテレーション数です。この値は、testingパッケージの内部ロジックによって動的に調整されます。目標は、ベンチマーク関数が合計で約1秒間(デフォルトのbenchtime)実行されるようにb.Nを決定することです。
    • b.ResetTimer(): タイマーをリセットします。セットアップコードの実行時間を測定から除外するために使用されます。
    • b.StartTimer(): タイマーを開始します。
    • b.StopTimer(): タイマーを停止します。
    • b.Run(name string, f func(b *B)): サブベンチマークを実行します。
  • イテレーション数の動的調整: testingパッケージは、ベンチマーク関数を複数回実行し、その実行時間に基づいて最適なb.Nを探索します。最初の実行ではb.Nは小さく設定され、その実行時間から1秒間の実行に必要なb.Nの概算値を計算し、次の実行でその概算値に近いb.Nを使用します。このプロセスを繰り返し、安定した測定結果が得られるまでb.Nを調整します。

b.launch() メソッド

src/pkg/testing/benchmark.go内のb.launch()メソッドは、ベンチマークのイテレーション数b.Nを決定し、ベンチマーク関数を実際に実行する中心的なロジックを含んでいます。このメソッドは、前回の実行時間とイテレーション数に基づいて、次のイテレーション数nを計算します。

b.nsPerOp() メソッド

b.nsPerOp()は、1回の操作(イテレーション)あたりのナノ秒数を計算します。これは、前回のベンチマーク実行の合計時間とイテレーション数から導き出されます。この値は、次のイテレーション数nを推定する際の重要な指標となります。

minmax 関数

GoのmathパッケージにはMinMax関数がありますが、このコミットのコンテキストでは、おそらくtestingパッケージ内部で定義されたシンプルなmin(a, b)max(a, b)ヘルパー関数を指しています。これらは2つの数値のうち小さい方または大きい方を返す基本的な関数です。

roundUp 関数

roundUp関数は、与えられた数値を読みやすい(例えば、10の倍数や100の倍数など)値に切り上げるために使用されます。ベンチマークのイテレーション数b.Nは、通常、キリの良い数値で表示されることが望ましいため、この関数が利用されます。

技術的詳細

このコミットの技術的な核心は、src/pkg/testing/benchmark.go内の(*B).launch()メソッドにおける、次のベンチマークイテレーション数nの計算ロジックの変更です。

変更前は、nの計算に以下のロジックが使用されていました。

		// Run more iterations than we think we'll need for a second (1.5x).
		// Don't grow too fast in case we had timing errors previously.
		n = max(min(n+n/2, 100*last), last+1)
		// Round up to something easy to read.
		n = roundUp(n)

変更後のロジックは以下のようになります。

		// If the last run was small, don't grow too fast.
		if last < 1000 {
			n = min(n, 100*last)
		}
		// Be sure to run at least one more than last time.
		n = max(n, last+1)
		// Round up to something easy to read.
		n = roundUp(n)

主要な変更点は以下の2つです。

  1. 高速なベンチマークにおけるイテレーション数増加の制限緩和:
    • 変更前は、n = max(min(n+n/2, 100*last), last+1)という式で、nの増加が100*lastに制限されていました。これは、前回の実行lastが非常に小さい場合(例えば、数イテレーションしか実行されなかった場合)でも、次のイテレーション数nlastの100倍までに制限されることを意味します。これにより、高速なベンチマークではb.Nがなかなか大きな値に到達せず、benchtimeを満たすまでに多くの試行が必要となっていました。
    • 変更後は、if last < 1000 { n = min(n, 100*last) }という条件が追加されました。これは、「前回のイテレーション数lastが1000未満の場合にのみ、nの増加を100*lastに制限する」というものです。lastが1000以上の場合は、この制限が適用されなくなり、nはより自由に、かつ迅速に増加できるようになります。これにより、高速なベンチマークがより早く適切なb.Nに到達し、全体の実行時間が短縮されます。
  2. 二重の丸め処理の解消:
    • 変更前は、n = max(min(n+n/2, 100*last), last+1)の後にn = roundUp(n)が適用されていました。このmax(min(...))の内部で、n+n/2という部分が実質的に1.5xの成長を意図しており、これがroundUpの前に適用されていました。
    • 変更後は、n = max(n, last+1)というシンプルな式になり、roundUp(n)が適用される前に余計な丸めや複雑な成長ロジックがなくなりました。これにより、イテレーション数の計算がより直接的になり、効率が向上します。

これらの変更により、特に高速なベンチマークにおいて、b.Nがより迅速に適切な値に収束するようになり、結果としてベンチマークの総実行時間が大幅に短縮されました。コミットメッセージに示されているように、最大で43%の実行時間短縮が実現されています。

コアとなるコードの変更箇所

--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -205,10 +205,12 @@ func (b *B) launch() {\n 		} else {\n 			n = int(d.Nanoseconds() / b.nsPerOp())\n 		}\n-\t\t// Run more iterations than we think we'll need for a second (1.5x).\n-\t\t// Don't grow too fast in case we had timing errors previously.\n+\t\t// If the last run was small, don't grow too fast.\n+\t\tif last < 1000 {\n+\t\t\tn = min(n, 100*last)\n+\t\t}\n \t\t// Be sure to run at least one more than last time.\n-\t\tn = max(min(n+n/2, 100*last), last+1)\n+\t\tn = max(n, last+1)\n \t\t// Round up to something easy to read.\n \t\tn = roundUp(n)\n \t\tb.runN(n)\n```

## コアとなるコードの解説

このコードスニペットは、`testing`パッケージの`*B`型に属する`launch()`メソッドの一部です。このメソッドは、ベンチマークの各試行において、次のイテレーション数`n`を計算する役割を担っています。

変更前のコード:

```go
		// Run more iterations than we think we'll need for a second (1.5x).
		// Don't grow too fast in case we had timing errors previously.
		n = max(min(n+n/2, 100*last), last+1)
		// Round up to something easy to read.
		n = roundUp(n)
  • n = int(d.Nanoseconds() / b.nsPerOp()) の行(変更箇所の上にある行)で、前回の実行時間dと1操作あたりのナノ秒数b.nsPerOp()から、目標のbenchtime(通常1秒)を満たすために必要なイテレーション数nの初期推定値が計算されます。
  • n = max(min(n+n/2, 100*last), last+1):
    • n+n/2: これはnを1.5倍に増やすことを意図しています。
    • min(n+n/2, 100*last): 計算されたnが前回のイテレーション数lastの100倍を超えないように制限します。これは、急激なイテレーション数の増加を防ぐための安全策でした。
    • max(..., last+1): 計算されたnが少なくとも前回のイテレーション数lastより1多くなることを保証します。これにより、ベンチマークが常に進行することが保証されます。
  • n = roundUp(n): 計算されたnを、人間が読みやすいキリの良い数値に丸めます。

変更後のコード:

		// If the last run was small, don't grow too fast.
		if last < 1000 {
			n = min(n, 100*last)
		}
		// Be sure to run at least one more than last time.
		n = max(n, last+1)
		// Round up to something easy to read.
		n = roundUp(n)
  • if last < 1000 { n = min(n, 100*last) }:
    • この新しい条件文が追加されました。前回のイテレーション数lastが1000未満の場合にのみ、nの増加を100*lastに制限します。
    • この変更の意図は、ベンチマークの初期段階(lastが小さい時)では急激なイテレーション数の増加を抑えつつ、lastが十分に大きくなった(1000以上になった)場合には、この100*lastという制限を解除し、nがより迅速に成長できるようにすることです。これにより、高速なベンチマークがより早く適切なb.Nに到達できるようになります。
  • n = max(n, last+1):
    • 以前の複雑なmax(min(...))の式が、このシンプルな式に置き換えられました。
    • これは、計算されたnが少なくとも前回のイテレーション数lastより1多くなることを保証する、という最小限の保証のみを行います。
    • 以前のn+n/2(1.5倍成長)や100*last(急成長制限)のロジックは、if last < 1000ブロックに移動または削除されたため、この行はよりシンプルになりました。
  • n = roundUp(n): この行は変更されていませんが、その適用されるタイミングと、その前のnの計算ロジックが変更されたことで、全体的な効率が向上しています。以前はroundUpの前にn+n/2という成長ロジックがありましたが、それがなくなったことで、より直接的な丸め処理が行われるようになりました。

この変更により、ベンチマークのイテレーション数b.Nの決定がより効率的になり、特に高速なベンチマークにおいて、不要な短い試行が減り、全体の実行時間が短縮される効果が期待できます。

関連リンク

参考にした情報源リンク

[インデックス 19526] ファイルの概要

このコミットは、Go言語の標準ライブラリであるtestingパッケージ内のbenchmark.goファイルに対する変更です。benchmark.goは、Goのベンチマークテストの実行ロジック、特にベンチマーク関数が実行されるイテレーション数(b.N)を動的に決定するメカニズムを管理しています。このファイルは、ベンチマークの精度と実行時間のバランスを取りながら、効率的なベンチマーク実行を可能にするための重要な役割を担っています。

コミット

このコミットは、Go言語のベンチマーク実行を高速化することを目的としています。具体的には、ベンチマークのイテレーション数を決定するロジックを改善し、特に高速なベンチマークにおいて、イテレーション数の増加をより迅速に行うように変更しています。また、イテレーション数の丸め処理が二重に行われていた点を修正し、全体的なベンチマーク実行時間の短縮を実現しています。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5bb1af43214c3da52d1752e58d03ed968e6a11b

元コミット内容

commit a5bb1af43214c3da52d1752e58d03ed968e6a11b
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Thu Jun 12 07:51:32 2014 -0700

    testing: make benchmarking faster
    
    Allow the number of benchmark iterations to grow faster for fast benchmarks, and don't round up twice.
    
    Using the default benchtime, this CL reduces wall clock time to run benchmarks:
    
    net/http        49s   -> 37s   (-24%)
    runtime         8m31s -> 5m55s (-30%)
    bytes           2m37s -> 1m29s (-43%)
    encoding/json   29s   -> 21s   (-27%)
    strings         1m16s -> 53s   (-30%)
    
    LGTM=crawshaw
    R=golang-codereviews, crawshaw
    CC=golang-codereviews
    https://golang.org/cl/101970047

変更の背景

Goのベンチマークシステムは、指定されたbenchtime(デフォルトは1秒)を満たすように、ベンチマーク関数が実行されるイテレーション数(b.N)を動的に調整します。しかし、この調整ロジックには改善の余地がありました。特に、非常に高速なベンチマークの場合、イテレーション数b.Nの増加が遅く、結果としてbenchtimeを満たすまでに多くの試行(サブベンチマーク実行)が必要となり、全体のベンチマーク実行時間が長くなるという問題がありました。

このコミットの背景には、以下の具体的な課題がありました。

  1. 高速なベンチマークにおけるイテレーション数増加の遅さ: 以前のロジックでは、前回のイテレーション数lastが小さい場合でも、n(次のイテレーション数)の増加率が制限されていました。これにより、b.Nがなかなか十分な値に達せず、ベンチマークがbenchtimeに到達するまでに多くの短い実行を繰り返す必要がありました。
  2. 二重の丸め処理: イテレーション数の計算において、roundUp関数による丸め処理が実質的に二重に行われており、これが効率を低下させていました。

これらの課題を解決し、ベンチマークの実行時間を短縮することが、このコミットの主な動機となっています。コミットメッセージに記載されているように、この変更により、net/httpruntimebytesencoding/jsonstringsといった主要なパッケージのベンチマーク実行時間が大幅に短縮されています。

前提知識の解説

Goのベンチマークシステム

Go言語には、標準でベンチマークテストをサポートするtestingパッケージが用意されています。ベンチマークテストは、関数のパフォーマンスを測定するために使用され、go test -bench=.コマンドで実行できます。

  • ベンチマーク関数: func BenchmarkXxx(b *testing.B)というシグネチャを持つ関数がベンチマーク関数として認識されます。
  • *testing.B: ベンチマーク実行のコンテキストを提供する構造体です。
    • b.N: ベンチマーク関数が実行されるイテレーション数です。この値は、testingパッケージの内部ロジックによって動的に調整されます。目標は、ベンチマーク関数が合計で約1秒間(デフォルトのbenchtime)実行されるようにb.Nを決定することです。
    • b.ResetTimer(): タイマーをリセットします。セットアップコードの実行時間を測定から除外するために使用されます。
    • b.StartTimer(): タイマーを開始します。
    • b.StopTimer(): タイマーを停止します。
    • b.Run(name string, f func(b *B)): サブベンチマークを実行します。
  • イテレーション数の動的調整: testingパッケージは、ベンチマーク関数を複数回実行し、その実行時間に基づいて最適なb.Nを探索します。最初の実行ではb.Nは小さく設定され、その実行時間から1秒間の実行に必要なb.Nの概算値を計算し、次の実行でその概算値に近いb.Nを使用します。このプロセスを繰り返し、安定した測定結果が得られるまでb.Nを調整します。

b.launch() メソッド

src/pkg/testing/benchmark.go内のb.launch()メソッドは、ベンチマークのイテレーション数b.Nを決定し、ベンチマーク関数を実際に実行する中心的なロジックを含んでいます。このメソッドは、前回の実行時間とイテレーション数に基づいて、次のイテレーション数nを計算します。

b.nsPerOp() メソッド

b.nsPerOp()は、1回の操作(イテレーション)あたりのナノ秒数を計算します。これは、前回のベンチマーク実行の合計時間とイテレーション数から導き出されます。この値は、次のイテレーション数nを推定する際の重要な指標となります。

minmax 関数

GoのmathパッケージにはMinMax関数がありますが、このコミットのコンテキストでは、おそらくtestingパッケージ内部で定義されたシンプルなmin(a, b)max(a, b)ヘルパー関数を指しています。これらは2つの数値のうち小さい方または大きい方を返す基本的な関数です。

roundUp 関数

roundUp関数は、与えられた数値を読みやすい(例えば、10の倍数や100の倍数など)値に切り上げるために使用されます。ベンチマークのイテレーション数b.Nは、通常、キリの良い数値で表示されることが望ましいため、この関数が利用されます。

技術的詳細

このコミットの技術的な核心は、src/pkg/testing/benchmark.go内の(*B).launch()メソッドにおける、次のベンチマークイテレーション数nの計算ロジックの変更です。

変更前は、nの計算に以下のロジックが使用されていました。

		// Run more iterations than we think we'll need for a second (1.5x).
		// Don't grow too fast in case we had timing errors previously.
		n = max(min(n+n/2, 100*last), last+1)
		// Round up to something easy to read.
		n = roundUp(n)

変更後のロジックは以下のようになります。

		// If the last run was small, don't grow too fast.
		if last < 1000 {
			n = min(n, 100*last)
		}
		// Be sure to run at least one more than last time.
		n = max(n, last+1)
		// Round up to something easy to read.
		n = roundUp(n)

主要な変更点は以下の2つです。

  1. 高速なベンチマークにおけるイテレーション数増加の制限緩和:
    • 変更前は、n = max(min(n+n/2, 100*last), last+1)という式で、nの増加が100*lastに制限されていました。これは、前回の実行lastが非常に小さい場合(例えば、数イテレーションしか実行されなかった場合)でも、次のイテレーション数nlastの100倍までに制限されることを意味します。これにより、高速なベンチマークではb.Nがなかなか大きな値に到達せず、benchtimeを満たすまでに多くの試行が必要となっていました。
    • 変更後は、if last < 1000 { n = min(n, 100*last) }という条件が追加されました。これは、「前回のイテレーション数lastが1000未満の場合にのみ、nの増加を100*lastに制限する」というものです。lastが1000以上の場合は、この制限が適用されなくなり、nはより自由に、かつ迅速に増加できるようになります。これにより、高速なベンチマークがより早く適切なb.Nに到達し、全体の実行時間が短縮されます。
  2. 二重の丸め処理の解消:
    • 変更前は、n = max(min(n+n/2, 100*last), last+1)の後にn = roundUp(n)が適用されていました。このmax(min(...))の内部で、n+n/2という部分が実質的に1.5xの成長を意図しており、これがroundUpの前に適用されていました。
    • 変更後は、n = max(n, last+1)というシンプルな式になり、roundUp(n)が適用される前に余計な丸めや複雑な成長ロジックがなくなりました。これにより、イテレーション数の計算がより直接的になり、効率が向上します。

これらの変更により、特に高速なベンチマークにおいて、b.Nがより迅速に適切な値に収束するようになり、結果としてベンチマークの総実行時間が大幅に短縮されました。コミットメッセージに示されているように、最大で43%の実行時間短縮が実現されています。

コアとなるコードの変更箇所

--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -205,10 +205,12 @@ func (b *B) launch() {\n 		} else {\n 			n = int(d.Nanoseconds() / b.nsPerOp())\n 		}\n-\t\t// Run more iterations than we think we'll need for a second (1.5x).\n-\t\t// Don't grow too fast in case we had timing errors previously.\n+\t\t// If the last run was small, don't grow too fast.\n+\t\tif last < 1000 {\n+\t\t\tn = min(n, 100*last)\n+\t\t}\n \t\t// Be sure to run at least one more than last time.\n-\t\tn = max(min(n+n/2, 100*last), last+1)\n+\t\tn = max(n, last+1)\n \t\t// Round up to something easy to read.\n \t\tn = roundUp(n)\n \t\tb.runN(n)\n```

## コアとなるコードの解説

このコードスニペットは、`testing`パッケージの`*B`型に属する`launch()`メソッドの一部です。このメソッドは、ベンチマークの各試行において、次のイテレーション数`n`を計算する役割を担っています。

変更前のコード:

```go
		// Run more iterations than we think we'll need for a second (1.5x).
		// Don't grow too fast in case we had timing errors previously.
		n = max(min(n+n/2, 100*last), last+1)
		// Round up to something easy to read.
		n = roundUp(n)
  • n = int(d.Nanoseconds() / b.nsPerOp()) の行(変更箇所の上にある行)で、前回の実行時間dと1操作あたりのナノ秒数b.nsPerOp()から、目標のbenchtime(通常1秒)を満たすために必要なイテレーション数nの初期推定値が計算されます。
  • n = max(min(n+n/2, 100*last), last+1):
    • n+n/2: これはnを1.5倍に増やすことを意図しています。
    • min(n+n/2, 100*last): 計算されたnが前回のイテレーション数lastの100倍を超えないように制限します。これは、急激なイテレーション数の増加を防ぐための安全策でした。
    • max(..., last+1): 計算されたnが少なくとも前回のイテレーション数lastより1多くなることを保証します。これにより、ベンチマークが常に進行することが保証されます。
  • n = roundUp(n): 計算されたnを、人間が読みやすいキリの良い数値に丸めます。

変更後のコード:

		// If the last run was small, don't grow too fast.
		if last < 1000 {
			n = min(n, 100*last)
		}
		// Be sure to run at least one more than last time.
		n = max(n, last+1)
		// Round up to something easy to read.
		n = roundUp(n)
  • if last < 1000 { n = min(n, 100*last) }:
    • この新しい条件文が追加されました。前回のイテレーション数lastが1000未満の場合にのみ、nの増加を100*lastに制限します。
    • この変更の意図は、ベンチマークの初期段階(lastが小さい時)では急激なイテレーション数の増加を抑えつつ、lastが十分に大きくなった(1000以上になった)場合には、この100*lastという制限を解除し、nがより迅速に成長できるようにすることです。これにより、高速なベンチマークがより早く適切なb.Nに到達できるようになります。
  • n = max(n, last+1):
    • 以前の複雑なmax(min(...))の式が、このシンプルな式に置き換えられました。
    • これは、計算されたnが少なくとも前回のイテレーション数lastより1多くなることを保証する、という最小限の保証のみを行います。
    • 以前のn+n/2(1.5倍成長)や100*last(急成長制限)のロジックは、if last < 1000ブロックに移動または削除されたため、この行はよりシンプルになりました。
  • n = roundUp(n): この行は変更されていませんが、その適用されるタイミングと、その前のnの計算ロジックが変更されたことで、全体的な効率が向上しています。以前はroundUpの前にn+n/2という成長ロジックがありましたが、それがなくなったことで、より直接的な丸め処理が行われるようになりました。

この変更により、ベンチマークのイテレーション数b.Nの決定がより効率的になり、特に高速なベンチマークにおいて、不要な短い試行が減り、全体の実行時間が短縮される効果が期待できます。

関連リンク

参考にした情報源リンク