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

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

このコミットは、Go言語の標準ライブラリ testing パッケージにおける RunParallel 関数の潜在的な誤用に対する診断メッセージを改善するものです。具体的には、ベンチマークテストにおいて RunParallel のボディ関数が PB.Next() を適切に呼び出さなかった場合に、より分かりやすいエラーメッセージを提供するように変更されています。

コミット

commit 5b6aaba1ce869b3744f5f44bf218ef521bbd7940
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 24 21:08:37 2014 +0400

    testing: improve diagnosis of a potential misuse of RunParallel

    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/68230045

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

https://github.com/golang/go/commit/5b6aaba1ce869b3744f5f44bf218ef521bbd7940

元コミット内容

testing: improve diagnosis of a potential misuse of RunParallel

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/68230045

変更の背景

Go言語の testing パッケージには、並列ベンチマークを実行するための (*B).RunParallel メソッドが提供されています。このメソッドは、複数のゴルーチンを起動し、それぞれが PB (Parallel Benchmark) オブジェクトの Next() メソッドを呼び出すことで、ベンチマークのイテレーションを協調的に実行するように設計されています。

RunParallel の典型的な使用パターンは以下のようになります。

func BenchmarkSomething(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // ここでベンチマーク対象のコードを実行
        }
    })
}

ここで重要なのは、for pb.Next() ループがベンチマークのイテレーションを制御している点です。pb.Next() は、次のイテレーションに進むべきかどうかを返し、イテレーションが終了すると false を返します。

このコミットが行われる以前は、RunParallel のボディ関数内で pb.Next() が一度も呼び出されなかった場合、またはループが途中で終了してしまい、期待されるイテレーション数に達しなかった場合に、診断メッセージが不明瞭でした。元のコードでは、n == 0 (イテレーション数が0) の場合にのみ b.Fatal("RunParallel body did not not call PB.Next") というメッセージを出力していました。しかし、pb.Next() が呼び出されたものの、期待される回数に満たなかった場合(例えば、ループが途中でbreakされたり、条件が誤っていたりした場合)には、このエラーメッセージは表示されず、ユーザーは問題の原因を特定しにくい状況でした。

この変更の背景には、ユーザーが RunParallel を誤用した際に、より迅速かつ正確に問題を発見できるようにするという意図があります。特に、ベンチマークが期待通りに実行されない、あるいは結果が不正確であるといった場合に、デバッグの手間を削減することが目的です。

前提知識の解説

Go言語のベンチマークテスト

Go言語には、標準でベンチマークテストを記述・実行するための機能が testing パッケージに組み込まれています。ベンチマーク関数は BenchmarkXxx(*testing.B) というシグネチャを持ち、go test -bench=. コマンドで実行されます。

  • *testing.B: ベンチマークのコンテキストを提供する構造体です。イテレーション数 (b.N) や時間計測などの機能を提供します。
  • b.N: ベンチマーク対象のコードが実行されるイテレーション数です。Goのテストフレームワークが自動的に調整し、信頼性の高い結果が得られるようにします。
  • b.ResetTimer(): タイマーをリセットし、その後のコードの実行時間を計測対象とします。
  • b.StopTimer(): タイマーを停止します。セットアップコードなど、計測対象外の処理がある場合に使用します。
  • b.StartTimer(): タイマーを再開します。

(*B).RunParallel*testing.PB

(*B).RunParallel は、並列ベンチマークを実行するためのメソッドです。これは、複数のゴルーチンを起動し、それぞれがベンチマークのイテレーションを並行して実行できるようにします。

  • func (b *B) RunParallel(body func(*PB)): RunParallel メソッドのシグネチャです。引数として *PB を受け取る関数 body をとります。
  • *testing.PB: RunParallelbody 関数に渡される構造体で、並列ベンチマークのイテレーションを制御します。
  • pb.Next() bool: *testing.PB のメソッドで、次のベンチマークイテレーションに進むべきかどうかを返します。このメソッドは、ベンチマークのイテレーションが完了したか、またはエラーが発生した場合には false を返します。RunParallel を使用する際には、for pb.Next() { ... } の形でループを回すのが慣例です。これにより、複数のゴルーチンが協調して b.N 回のイテレーションを分担して実行します。

b.Fatal

*testing.B (および *testing.T) のメソッドで、テストまたはベンチマークを失敗としてマークし、現在のゴルーチンを終了させます。これは、致命的なエラーが発生した場合に、それ以上のテスト実行を停止するために使用されます。

技術的詳細

このコミットの技術的な変更点は、testing パッケージの benchmark.go ファイル内の (*B).RunParallel メソッドにおけるエラー診断ロジックの改善にあります。

元のコードでは、RunParallel のボディ関数が PB.Next() を一度も呼び出さなかった場合(つまり、n が0のままだった場合)にのみ、b.Fatal("RunParallel body did not not call PB.Next") というエラーメッセージを出力していました。ここで n は、pb.Next()true を返した回数をカウントする変数です。

変更後のコードでは、エラーチェックの条件が if n <= uint64(b.N) && !b.Failed() に変更されました。

  • n <= uint64(b.N): これは、pb.Next()true を返した回数 n が、ベンチマークの総イテレーション数 b.N 以下であるかどうかをチェックします。RunParallelb.N 回のイテレーションを並列に実行するように設計されているため、nb.N より少ない場合は、ボディ関数が期待通りに全てのイテレーションを処理しなかったことを意味します。
  • !b.Failed(): これは、ベンチマーク自体が既に失敗状態になっていないことを確認します。もしベンチマークが既に b.Fatal などによって失敗としてマークされている場合、重複してエラーメッセージを出力する必要がないためです。

この新しい条件が真の場合、つまり pb.Next() の呼び出し回数が期待値に満たず、かつベンチマークがまだ失敗していない場合に、b.Fatal("RunParallel: body exited without pb.Next() == false") という新しいエラーメッセージが出力されます。

この新しいエラーメッセージ「RunParallel: body exited without pb.Next() == false」は、元のメッセージよりも具体的で、問題の原因をより正確に示唆しています。これは、pb.Next() が最終的に false を返すまでループが実行されなかったことを意味し、ユーザーが for pb.Next() { ... } のループを正しく実装していない可能性が高いことを示唆します。例えば、ループが途中で break されたり、pb.Next() の呼び出しが条件分岐の内側に隠れてしまったりするようなケースが考えられます。

この変更により、RunParallel の誤用に対する診断がより堅牢になり、開発者が並列ベンチマークのロジックの誤りを早期に発見しやすくなりました。

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

変更は src/pkg/testing/benchmark.go ファイルの (*B).RunParallel メソッド内で行われています。

--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -417,8 +417,8 @@ func (b *B) RunParallel(body func(*PB)) {
 		}()
 	}\n \twg.Wait()\n-\tif n == 0 {\n-\t\tb.Fatal(\"RunParallel body did not not call PB.Next\")\n+\tif n <= uint64(b.N) && !b.Failed() {\n+\t\tb.Fatal(\"RunParallel: body exited without pb.Next() == false\")\n \t}\n }\n \n```

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

変更されたコードブロックは、`RunParallel` メソッドの終盤に位置し、並列ベンチマークの実行が完了した後に、`pb.Next()` の呼び出しが適切に行われたかどうかを検証する役割を担っています。

- **変更前**:
  ```go
  if n == 0 {
      b.Fatal("RunParallel body did not not call PB.Next")
  }

この条件は、pb.Next() が一度も true を返さなかった場合にのみエラーを報告していました。つまり、n (イテレーションカウント) が0の場合です。これは、RunParallel のボディ関数が for pb.Next() { ... } のループを全く実行しなかった、あるいは pb.Next() を呼び出すコードパスに到達しなかった場合にのみ捕捉できるものでした。

  • 変更後:
    if n <= uint64(b.N) && !b.Failed() {
        b.Fatal("RunParallel: body exited without pb.Next() == false")
    }
    
    この新しい条件は、より広範な誤用ケースを捕捉します。
    • n <= uint64(b.N): npb.Next()true を返した回数、つまりベンチマークのイテレーションが実際に実行された回数です。b.N はベンチマークが目標とする総イテレーション数です。RunParallel は、複数のゴルーチンが協力して b.N 回のイテレーションを完了することを期待しています。したがって、nb.N よりも少ない場合、それはボディ関数が全てのイテレーションを適切に処理しなかったことを意味します。これは、pb.Next() が途中で false を返してループが早期に終了したか、あるいは何らかのロジックエラーでイテレーションがスキップされた可能性を示唆します。
    • !b.Failed(): この追加条件は、ベンチマークが既に他の理由で失敗としてマークされていないことを確認します。これにより、冗長なエラーメッセージの出力を防ぎます。

新しいエラーメッセージ「RunParallel: body exited without pb.Next() == false」は、pb.Next() が最終的に false を返すことでループが正常に終了するという RunParallel の期待される動作を明確に示しています。このメッセージは、ユーザーが pb.Next() を使用したループの終了条件を誤って実装している可能性が高いことを示唆し、デバッグの方向性を明確にします。

この変更は、RunParallel の堅牢性を高め、開発者が並列ベンチマークをより正確に記述し、誤用を早期に発見できるようにするための重要な改善です。

関連リンク

参考にした情報源リンク

このコミットは、Go言語の標準ライブラリ testing パッケージにおける RunParallel 関数の潜在的な誤用に対する診断メッセージを改善するものです。具体的には、ベンチマークテストにおいて RunParallel のボディ関数が PB.Next() を適切に呼び出さなかった場合に、より分かりやすいエラーメッセージを提供するように変更されています。

コミット

commit 5b6aaba1ce869b3744f5f44bf218ef521bbd7940
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 24 21:08:37 2014 +0400

    testing: improve diagnosis of a potential misuse of RunParallel

    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/68230045

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

https://github.com/golang/go/commit/5b6aaba1ce869b3744f5f44bf218ef521bbd7940

元コミット内容

testing: improve diagnosis of a potential misuse of RunParallel

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/68230045

変更の背景

Go言語の testing パッケージには、並列ベンチマークを実行するための (*B).RunParallel メソッドが提供されています。このメソッドは、複数のゴルーチンを起動し、それぞれが PB (Parallel Benchmark) オブジェクトの Next() メソッドを呼び出すことで、ベンチマークのイテレーションを協調的に実行するように設計されています。

RunParallel の典型的な使用パターンは以下のようになります。

func BenchmarkSomething(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // ここでベンチマーク対象のコードを実行
        }
    })
}

ここで重要なのは、for pb.Next() ループがベンチマークのイテレーションを制御している点です。pb.Next() は、次のイテレーションに進むべきかどうかを返し、イテレーションが終了すると false を返します。

このコミットが行われる以前は、RunParallel のボディ関数内で pb.Next() が一度も呼び出されなかった場合、またはループが途中で終了してしまい、期待されるイテレーション数に達しなかった場合に、診断メッセージが不明瞭でした。元のコードでは、n == 0 (イテレーション数が0) の場合にのみ b.Fatal("RunParallel body did not not call PB.Next") というメッセージを出力していました。しかし、pb.Next() が呼び出されたものの、期待される回数に満たなかった場合(例えば、ループが途中でbreakされたり、条件が誤っていたりした場合)には、このエラーメッセージは表示されず、ユーザーは問題の原因を特定しにくい状況でした。

この変更の背景には、ユーザーが RunParallel を誤用した際に、より迅速かつ正確に問題を発見できるようにするという意図があります。特に、ベンチマークが期待通りに実行されない、あるいは結果が不正確であるといった場合に、デバッグの手間を削減することが目的です。

前提知識の解説

Go言語のベンチマークテスト

Go言語には、標準でベンチマークテストを記述・実行するための機能が testing パッケージに組み込まれています。ベンチマーク関数は BenchmarkXxx(*testing.B) というシグネチャを持ち、go test -bench=. コマンドで実行されます。

  • *testing.B: ベンチマークのコンテキストを提供する構造体です。イテレーション数 (b.N) や時間計測などの機能を提供します。
  • b.N: ベンチマーク対象のコードが実行されるイテレーション数です。Goのテストフレームワークが自動的に調整し、信頼性の高い結果が得られるようにします。
  • b.ResetTimer(): タイマーをリセットし、その後のコードの実行時間を計測対象とします。
  • b.StopTimer(): タイマーを停止します。セットアップコードなど、計測対象外の処理がある場合に使用します。
  • b.StartTimer(): タイマーを再開します。

(*B).RunParallel*testing.PB

(*B).RunParallel は、並列ベンチマークを実行するためのメソッドです。これは、複数のゴルーチンを起動し、それぞれがベンチマークのイテレーションを並行して実行できるようにします。

  • func (b *B) RunParallel(body func(*PB)): RunParallel メソッドのシグネチャです。引数として *PB を受け取る関数 body をとります。
  • *testing.PB: RunParallelbody 関数に渡される構造体で、並列ベンチマークのイテレーションを制御します。
  • pb.Next() bool: *testing.PB のメソッドで、次のベンチマークイテレーションに進むべきかどうかを返します。このメソッドは、ベンチマークのイテレーションが完了したか、またはエラーが発生した場合には false を返します。RunParallel を使用する際には、for pb.Next() { ... } の形でループを回すのが慣例です。これにより、複数のゴルーチンが協調して b.N 回のイテレーションを分担して実行します。

b.Fatal

*testing.B (および *testing.T) のメソッドで、テストまたはベンチマークを失敗としてマークし、現在のゴルーチンを終了させます。これは、致命的なエラーが発生した場合に、それ以上のテスト実行を停止するために使用されます。

技術的詳細

このコミットの技術的な変更点は、testing パッケージの benchmark.go ファイル内の (*B).RunParallel メソッドにおけるエラー診断ロジックの改善にあります。

元のコードでは、RunParallel のボディ関数が PB.Next() を一度も呼び出さなかった場合(つまり、n が0のままだった場合)にのみ、b.Fatal("RunParallel body did not not call PB.Next") というエラーメッセージを出力していました。ここで n は、pb.Next()true を返した回数をカウントする変数です。

変更後のコードでは、エラーチェックの条件が if n <= uint64(b.N) && !b.Failed() に変更されました。

  • n <= uint64(b.N): これは、pb.Next()true を返した回数 n が、ベンチマークの総イテレーション数 b.N 以下であるかどうかをチェックします。RunParallelb.N 回のイテレーションを並列に実行するように設計されているため、nb.N より少ない場合は、ボディ関数が期待通りに全てのイテレーションを処理しなかったことを意味します。
  • !b.Failed(): これは、ベンチマーク自体が既に失敗状態になっていないことを確認します。もしベンチマークが既に b.Fatal などによって失敗としてマークされている場合、重複してエラーメッセージを出力する必要がないためです。

この新しい条件が真の場合、つまり pb.Next() の呼び出し回数が期待値に満たず、かつベンチマークがまだ失敗していない場合に、b.Fatal("RunParallel: body exited without pb.Next() == false") という新しいエラーメッセージが出力されます。

この新しいエラーメッセージ「RunParallel: body exited without pb.Next() == false」は、元のメッセージよりも具体的で、問題の原因をより正確に示唆しています。これは、pb.Next() が最終的に false を返すまでループが実行されなかったことを意味し、ユーザーが for pb.Next() { ... } のループを正しく実装していない可能性が高いことを示唆します。例えば、ループが途中で break されたり、pb.Next() の呼び出しが条件分岐の内側に隠れてしまったりするようなケースが考えられます。

この変更により、RunParallel の誤用に対する診断がより堅牢になり、開発者が並列ベンチマークのロジックの誤りを早期に発見しやすくなりました。

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

変更は src/pkg/testing/benchmark.go ファイルの (*B).RunParallel メソッド内で行われています。

--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -417,8 +417,8 @@ func (b *B) RunParallel(body func(*PB)) {
 		}()
 	}\n \twg.Wait()\n-\tif n == 0 {\n-\t\tb.Fatal(\"RunParallel body did not not call PB.Next\")\n+\tif n <= uint64(b.N) && !b.Failed() {\n+\t\tb.Fatal(\"RunParallel: body exited without pb.Next() == false\")\n \t}\n }\n \n```

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

変更されたコードブロックは、`RunParallel` メソッドの終盤に位置し、並列ベンチマークの実行が完了した後に、`pb.Next()` の呼び出しが適切に行われたかどうかを検証する役割を担っています。

- **変更前**:
  ```go
  if n == 0 {
      b.Fatal("RunParallel body did not not call PB.Next")
  }

この条件は、pb.Next() が一度も true を返さなかった場合にのみエラーを報告していました。つまり、n (イテレーションカウント) が0の場合です。これは、RunParallel のボディ関数が for pb.Next() { ... } のループを全く実行しなかった、あるいは pb.Next() を呼び出すコードパスに到達しなかった場合にのみ捕捉できるものでした。

  • 変更後:
    if n <= uint64(b.N) && !b.Failed() {
        b.Fatal("RunParallel: body exited without pb.Next() == false")
    }
    
    この新しい条件は、より広範な誤用ケースを捕捉します。
    • n <= uint64(b.N): npb.Next()true を返した回数、つまりベンチマークのイテレーションが実際に実行された回数です。b.N はベンチマークが目標とする総イテレーション数です。RunParallel は、複数のゴルーチンが協力して b.N 回のイテレーションを完了することを期待しています。したがって、nb.N よりも少ない場合、それはボディ関数が全てのイテレーションを適切に処理しなかったことを意味します。これは、pb.Next() が途中で false を返してループが早期に終了したか、あるいは何らかのロジックエラーでイテレーションがスキップされた可能性を示唆します。
    • !b.Failed(): この追加条件は、ベンチマークが既に他の理由で失敗としてマークされていないことを確認します。これにより、冗長なエラーメッセージの出力を防ぎます。

新しいエラーメッセージ「RunParallel: body exited without pb.Next() == false」は、pb.Next() が最終的に false を返すことでループが正常に終了するという RunParallel の期待される動作を明確に示しています。このメッセージは、ユーザーが pb.Next() を使用したループの終了条件を誤って実装している可能性が高いことを示唆し、デバッグの方向性を明確にします。

この変更は、RunParallel の堅牢性を高め、開発者が並列ベンチマークをより正確に記述し、誤用を早期に発見できるようにするための重要な改善です。

関連リンク

参考にした情報源リンク