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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージ内のベンチマーク機能における丸め処理のバグ修正と、関連するテストの改善を目的としています。具体的には、roundDown10関数の丸め誤差を修正し、その主要な利用元であるroundUp関数に対するテストカバレッジを追加しています。

コミット

commit a28609d66f74383ad23015aa810e3c76444057f5
Author: Dave Cheney <dave@cheney.net>
Date:   Sun Jun 2 09:13:12 2013 +1000

    testing: fix rounding error in roundDown10
    
    Fixes #5599.
    
    Thanks to minux.ma for the suggested fix.
    
    As we now have a harness to test testing internal functions I added some coverage for testing.roundUp, as it is the main consumer of roundDown10.
    
    R=minux.ma, kr, r
    CC=golang-dev
    https://golang.org/cl/9926043

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

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

元コミット内容

testing: fix rounding error in roundDown10

このコミットは、testingパッケージ内のroundDown10関数における丸め誤差を修正します。 これはIssue #5599を修正するものです。 minux.ma氏の提案された修正に感謝します。 内部のtesting関数をテストするためのハーネスができたため、roundDown10の主要な利用者であるtesting.roundUpのテストカバレッジを追加しました。

変更の背景

Go言語のtestingパッケージは、ベンチマークテストを実行する際に、テストの実行回数を動的に調整するロジックを持っています。この調整には、特定の数値を10の累乗に丸めるroundDown10関数や、特定の形式(1eX, 2eX, 5eX)に丸めるroundUp関数が使用されます。

このコミットが行われる前、roundDown10関数には、入力値がちょうど10の累乗である場合に誤った丸めを行うバグが存在していました。例えば、roundDown10(10)が期待される10ではなく1を返してしまうといった問題です。これは、ベンチマークの実行回数計算に影響を与え、テストの信頼性や効率に問題を引き起こす可能性がありました。

コミットメッセージにある「Fixes #5599」は、このバグがGoのIssueトラッカーで報告されていたことを示唆しています。また、「As we now have a harness to test testing internal functions」という記述から、この修正と同時に、testingパッケージの内部関数(特にroundUp)をテストするためのフレームワークが整備されたことが伺えます。これにより、roundDown10の修正だけでなく、その影響を受けるroundUp関数も適切に動作することを保証するためのテストが追加されました。

前提知識の解説

Go言語のtestingパッケージとベンチマーク

Go言語には、標準でテストとベンチマークをサポートするtestingパッケージが組み込まれています。

  • テスト (Tests): func TestXxx(*testing.T)という形式の関数で記述され、コードの正確性を検証します。
  • ベンチマーク (Benchmarks): func BenchmarkXxx(*testing.B)という形式の関数で記述され、コードのパフォーマンスを測定します。

ベンチマーク実行時、testingパッケージはb.Nという変数を通じて、ベンチマーク対象のコードが実行される回数を自動的に調整します。これは、測定の精度を確保しつつ、適切な時間でベンチマークが完了するようにするためです。このb.Nの値を決定する内部ロジックで、数値を丸める処理が使用されます。

roundDown10関数とroundUp関数

Goのベンチマークシステムでは、実行回数を決定する際に、特定の基準値に基づいて数値を丸める処理が行われます。

  • roundDown10(n int) int: 入力nを10の最も近い累乗(1, 10, 100, ...)に切り捨てる関数です。例えば、roundDown10(123)100を返します。
  • roundUp(n int) int: 入力n1eX, 2eX, 5eXの形式の数に切り上げる関数です。これは、ベンチマークの実行回数を決定する際に、10, 20, 50, 100, 200, 500, ...といった系列の数に丸めるために使用されます。

これらの関数は、ベンチマークの実行回数を効率的かつ適切に決定するために不可欠な内部ヘルパー関数です。

丸め誤差

コンピュータにおける数値の丸め処理は、特に境界値において予期せぬ結果を生むことがあります。このコミットで修正されたroundDown10のバグは、まさにこのような境界値(例: n=10)での処理の不備に起因していました。

技術的詳細

このコミットは、主に以下の3つのファイルにわたる変更を含んでいます。

  1. src/pkg/testing/benchmark.go:

    • roundDown10関数の修正: 変更前: for n > 10 変更後: for n >= 10 この変更は、nがちょうど10である場合にループが実行されないというバグを修正します。変更前はn=10の場合、10 > 10falseとなりループに入らず、tens0のまま1 * 10^0 = 1を返していました。変更後は10 >= 10trueとなり、ループが1回実行され、n1になりtens1になります。結果として1 * 10^1 = 10が返され、期待通りの動作になります。
    • roundUp関数の修正: 変更前は複数のif文で条件分岐していましたが、これをswitch文に置き換えています。 変更前:
      if n < (2 * base) {
          return 2 * base
      }
      if n < (5 * base) {
          return 5 * base
      }
      return 10 * base
      
      変更後:
      switch {
      case n <= base:
          return base
      case n <= (2 * base):
          return 2 * base
      case n <= (5 * base):
          return 5 * base
      default:
          return 10 * base
      }
      
      この変更により、nbase2 * base5 * baseといった境界値と等しい場合も適切に処理されるようになります。特にn <= baseのケースが追加されたことで、nbase以下の場合にbaseを返すという明確なロジックが追加されました。これにより、roundUpのロジックがより堅牢になり、roundDown10の修正と合わせて、ベンチマークの実行回数計算がより正確になります。
  2. src/pkg/testing/benchmark_test.go:

    • roundDownTestsのテストケースの拡充: {10, 1}{10, 10}に修正され、{100, 10}{100, 100}に修正されました。これはroundDown10の修正を反映したものです。 また、{101, 100}, {999, 100}, {1000, 1000}, {1001, 1000}といった、より多くの境界値や一般的なケースが追加され、テストカバレッジが向上しています。
    • TestRoundDown10のエラーメッセージの改善: t.Errorfのフォーマット文字列が変更され、入力値tt.vも表示されるようになり、デバッグが容易になりました。
    • roundUpTestsTestRoundUpの新規追加: roundUp関数に対する専用のテストケースとテスト関数が追加されました。これにより、roundUp関数の動作が独立して検証できるようになり、roundDown10の修正がroundUpに与える影響も確認できるようになりました。テストケースには0, 1, 2, 5, 9, 999, 1000, 1400, 1700, 4999, 5000, 5001など、様々な入力値が含まれており、roundUpのロジックが広範囲にわたってテストされています。
  3. src/pkg/testing/export_test.go:

    • RoundUpのテスト用エクスポート: testingパッケージの内部関数であるroundUpを、テストコードからアクセスできるようにRoundUpとしてエクスポートしています。これにより、benchmark_test.goからtesting.RoundUpとしてroundUp関数を呼び出し、テストを実行することが可能になります。

これらの変更は、Goのベンチマーク機能の正確性と堅牢性を向上させる上で重要な役割を果たしています。

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

src/pkg/testing/benchmark.go

--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -138,7 +138,7 @@ func max(x, y int) int {
 func roundDown10(n int) int {
 	var tens = 0
 	// tens = floor(log_10(n))
-	for n > 10 {
+	for n >= 10 {
 		n = n / 10
 		tens++
 	}
@@ -153,13 +153,16 @@ func roundDown10(n int) int {
 // roundUp rounds x up to a number of the form [1eX, 2eX, 5eX].
 func roundUp(n int) int {
 	base := roundDown10(n)
-	if n < (2 * base) {
+	switch {
+	case n <= base:
+		return base
+	case n <= (2 * base):
 		return 2 * base
-	}
-	if n < (5 * base) {
+	case n <= (5 * base):
 		return 5 * base
+	default:
+		return 10 * base
 	}
-	return 10 * base
 }
 
 // run times the benchmark function in a separate goroutine.

src/pkg/testing/benchmark_test.go

--- a/src/pkg/testing/benchmark_test.go
+++ b/src/pkg/testing/benchmark_test.go
@@ -13,19 +13,46 @@ var roundDownTests = []struct {
 }{\n \t{1, 1},\n \t{9, 1},\n-\t{10, 1},\n+\t{10, 10},\n \t{11, 10},\n-\t{100, 10},\n-\t//\t{101, 100}, // issue 5599\n-\t{1000, 100},\n-\t//\t{1001, 1000}, // issue 5599\n+\t{100, 100},\n+\t{101, 100},\n+\t{999, 100},\n+\t{1000, 1000},\n+\t{1001, 1000},\n }\n \n func TestRoundDown10(t *testing.T) {\n \tfor _, tt := range roundDownTests {\n \t\tactual := testing.RoundDown10(tt.v)\n \t\tif tt.expected != actual {\n-\t\t\tt.Errorf(\"roundDown10: expected %v, actual %v\", tt.expected, actual)\n+\t\t\tt.Errorf(\"roundDown10(%d): expected %d, actual %d\", tt.v, tt.expected, actual)\n+\t\t}\n+\t}\n+}\n+\n+var roundUpTests = []struct {\n+\tv, expected int\n+}{\n+\t{0, 1},\n+\t{1, 1},\n+\t{2, 2},\n+\t{5, 5},\n+\t{9, 10},\n+\t{999, 1000},\n+\t{1000, 1000},\n+\t{1400, 2000},\n+\t{1700, 2000},\n+\t{4999, 5000},\n+\t{5000, 5000},\n+\t{5001, 10000},\n+}\n+\n+func TestRoundUp(t *testing.T) {\n+\tfor _, tt := range roundUpTests {\n+\t\tactual := testing.RoundUp(tt.v)\n+\t\tif tt.expected != actual {\n+\t\t\tt.Errorf(\"roundUp(%d): expected %d, actual %d\", tt.v, tt.expected, actual)\n \t\t}\n \t}\n }\n```

### `src/pkg/testing/export_test.go`

```diff
--- a/src/pkg/testing/export_test.go
+++ b/src/pkg/testing/export_test.go
@@ -4,4 +4,7 @@
 
 package testing
 
-var RoundDown10 = roundDown10
+var (
+	RoundDown10 = roundDown10
+	RoundUp     = roundUp
+)

コアとなるコードの解説

roundDown10関数の修正 (src/pkg/testing/benchmark.go)

元のroundDown10関数は、入力nを10の累乗に切り捨てることを意図していました。例えば、123100に、91に丸められます。しかし、for n > 10という条件では、nがちょうど10の場合にループが実行されませんでした。

  • 変更前: for n > 10
    • n = 10の場合: 10 > 10falseなのでループに入らない。tens0のまま。結果、1 * 10^0 = 1が返される。これは誤り。
  • 変更後: for n >= 10
    • n = 10の場合: 10 >= 10trueなのでループに入る。n1になり、tens1になる。ループ条件1 >= 10falseなのでループを抜ける。結果、1 * 10^1 = 10が返される。これは正しい動作。

この修正により、10100といった10の累乗が正しくそれ自身に丸められるようになり、ベンチマークの実行回数計算の基礎がより正確になりました。

roundUp関数の修正 (src/pkg/testing/benchmark.go)

roundUp関数は、roundDown10の結果であるbaseを基に、数値を1eX, 2eX, 5eXの形式に切り上げる役割を担っています。元の実装は複数のif文を使用していましたが、境界値の処理が曖昧になる可能性がありました。

  • 変更前: 複数のif
    • n < (2 * base)n < (5 * base)という条件は、nがちょうど2 * base5 * baseである場合に、次の条件にフォールスルーしてしまう可能性がありました。
  • 変更後: switch文とn <=条件
    • switch文を使用し、case n <= base:case n <= (2 * base):case n <= (5 * base):という明確な条件を導入しました。
    • n <= baseのケースが追加されたことで、nbase以下の場合にbaseを返すというロジックが明示されました。
    • 各ケースでn <= ...とすることで、境界値(例: nがちょうど2 * baseの場合)が正しくその範囲に収まるように処理されます。
    • defaultケースで10 * baseを返すことで、それ以外の全てのケースが適切に処理されます。

この変更により、roundUp関数のロジックがより明確になり、境界値を含むあらゆる入力に対して期待通りの丸めが行われることが保証されます。

テストの追加と改善 (src/pkg/testing/benchmark_test.go)

  • roundDownTestsの更新: roundDown10の修正に合わせて、10100のテストケースの期待値が更新されました。また、101, 999, 1001などの新しいテストケースが追加され、roundDown10の堅牢性がより広範囲で検証されるようになりました。
  • roundUpTestsTestRoundUpの新規追加: roundUp関数はroundDown10の主要な利用者であるため、その動作を独立して検証することが重要です。このコミットでは、roundUp専用のテストケースとテスト関数が追加され、0から5001までの様々な入力値に対してroundUpが正しく機能するかを確認できるようになりました。これにより、roundDown10の修正がroundUpの動作に悪影響を与えないことを保証し、全体的なベンチマークロジックの信頼性を高めています。

テスト用エクスポート (src/pkg/testing/export_test.go)

Goでは、通常、パッケージ内部の関数は外部から直接アクセスできません。しかし、テストのために内部関数を公開したい場合があります。export_test.goファイルは、このような目的のために使用されます。

  • RoundUpのエクスポート: var RoundUp = roundUpという行を追加することで、testingパッケージの外部にあるbenchmark_test.goからtesting.RoundUpとして内部のroundUp関数を呼び出せるようになりました。これにより、roundUp関数に対する単体テストが可能になり、その正確性を保証できるようになります。

これらのコード変更は、Goのベンチマーク機能の基盤となる数値丸め処理の正確性を大幅に向上させ、より信頼性の高いパフォーマンス測定を可能にしました。

関連リンク

  • Go言語のtestingパッケージに関する公式ドキュメント: https://pkg.go.dev/testing
  • Go言語のベンチマークに関する公式ブログ記事 (例: "Go's work-stealing scheduler"): https://go.dev/blog/go1.14-performance (これは一般的なベンチマークの例であり、直接このコミットに関連するものではありませんが、Goのベンチマークの文脈を理解するのに役立ちます)

参考にした情報源リンク

  • GitHub上のコミットページ: https://github.com/golang/go/commit/a28609d66f74383ad23015aa810e3c76444057f5
  • GoのIssue #5599 (検索しましたが、Go言語の公式リポジトリでは見つかりませんでした。古いIssueであるか、別のリポジトリのIssueである可能性があります。)
  • Go Gerrit Change-ID 9926043 (検索しましたが、Go Gerritシステムでは見つかりませんでした。古い変更リストであるか、番号が正しくない可能性があります。)
  • Go言語のtestingパッケージのソースコード (src/pkg/testing/): https://github.com/golang/go/tree/master/src/testing (コミット当時のパスはsrc/pkg/testingでしたが、現在はsrc/testingです。)