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

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

このコミットは、Go言語のランタイムパッケージ内のベンチマークテストに関する修正です。具体的には、append操作のベンチマークにおいて、意図せずNバイトではなくN²バイトをアペンドしてしまうというロジックの誤りを修正しています。これにより、ベンチマークが正確なパフォーマンス測定を行うように改善されました。

コミット

  • コミットハッシュ: 1f74aa21d54244ea7ee4e0cabc101eb6f0ad19cb
  • 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
  • コミット日時: 2013年3月2日 土曜日 21:11:05 +0100
  • コミットメッセージ: runtime: benchmark for appending N bytes should not append N² bytes.

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

https://github.com/golang/go/commit/1f74aa21d54244ea7ee4e0cabc101eb6f0ad19cb

元コミット内容

runtime: benchmark for appending N bytes should not append N² bytes.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7420051

変更の背景

Go言語の標準ライブラリには、コードのパフォーマンスを測定するためのベンチマーク機能が組み込まれています。append関数はGoのスライス操作において非常に基本的なものであり、そのパフォーマンスはGoプログラム全体の効率に大きく影響します。このコミット以前のベンチマークコードでは、append操作のテストにおいて、N回のループ内でさらにNバイトのデータをアペンドするという二重ループ構造になっていました。

これは、ベンチマークの目的が「Nバイトをアペンドする際のパフォーマンス」を測定することであるにもかかわらず、実際には「N * N (N²) バイトをアペンドする際のパフォーマンス」を測定してしまっているという問題を引き起こしていました。b.Nはベンチマーク関数が実行される回数を示すため、内側のループでN回アペンドを行うと、合計でb.N * N回のappend操作、かつb.N * N * (アペンドされるバイト数)のデータ処理が行われることになります。

この誤ったロジックは、ベンチマーク結果を不正確にし、append関数の実際の性能特性を誤解させる可能性がありました。特に、Nが大きくなるにつれて、の計算量は非常に急速に増加するため、ベンチマークの実行時間が不必要に長くなり、リソースを大量に消費するという実用上の問題も発生していました。このコミットは、このベンチマークのロジックの誤りを修正し、意図した通りの「Nバイトのアペンド」のパフォーマンスを正確に測定できるようにすることを目的としています。

前提知識の解説

Go言語のスライスとappend関数

Go言語のスライスは、可変長シーケンスを扱うための強力なデータ構造です。内部的には、配列へのポインタ、長さ(len)、容量(cap)の3つの要素で構成されます。

append関数は、スライスに要素を追加するために使用されます。そのシグネチャは以下のようになります。

func append(slice []Type, elems ...Type) []Type

appendの動作は以下の通りです。

  1. 容量の確認: appendはまず、既存のスライスの容量(cap)が、追加される要素を格納するのに十分であるかを確認します。
  2. 再割り当て: 容量が不足している場合、appendはより大きな新しい基底配列を割り当て、既存の要素を新しい配列にコピーし、その後で新しい要素を追加します。この再割り当ては、通常、現在の容量の2倍(またはそれ以上)の容量を持つ新しい配列を作成することで行われます。この操作はコストが高く、パフォーマンスに影響を与えます。
  3. 要素の追加: 容量が十分な場合、または新しい配列にコピーされた後、新しい要素が既存の要素の末尾に追加されます。
  4. 新しいスライスを返す: appendは、更新されたスライス(新しい基底配列へのポインタ、新しい長さ、新しい容量を持つ)を返します。元のスライスが変更される場合と、新しいスライスが返される場合があるため、常にx = append(x, y...)のように戻り値を元の変数に代入することが推奨されます。

Go言語のベンチマーク

Go言語には、testingパッケージを通じて組み込みのベンチマーク機能が提供されています。ベンチマーク関数はBenchmarkXxx(*testing.B)というシグネチャを持ちます。

  • *testing.B: ベンチマークのコンテキストを提供する構造体です。
  • b.N: ベンチマーク関数が実行される回数を示します。Goのテストフレームワークは、ベンチマークの実行時間を安定させるために、このb.Nの値を動的に調整します。つまり、b.Nは固定値ではなく、ベンチマークが十分な時間(デフォルトでは1秒以上)実行されるように自動的に増加します。
  • b.StartTimer() / b.StopTimer(): これらのメソッドは、ベンチマークの計測を開始/停止するために使用されます。通常、セットアップコードはStartTimer()の前に置き、クリーンアップコードはStopTimer()の後に置きます。
  • b.ResetTimer(): タイマーをリセットし、それまでの計測時間をクリアします。

ベンチマークは、go test -bench=.コマンドで実行されます。

計算量(オーダー記法)

計算量とは、アルゴリズムの効率性を入力サイズNの関数として表現する方法です。オーダー記法(Big O notation)は、アルゴリズムの実行時間やメモリ使用量が入力サイズの増加に伴ってどのように変化するかを示すために使われます。

  • O(N): 線形時間。入力サイズNに比例して実行時間が増加します。例えば、配列のすべての要素を一度だけ処理する場合など。
  • O(N²): 二次時間。入力サイズNの二乗に比例して実行時間が増加します。例えば、ネストされたループで各要素のペアを処理する場合など。Nが大きくなると、実行時間は非常に急速に増加します。

このコミットの文脈では、「Nバイトをアペンドする」という操作は理想的にはO(N)に近いパフォーマンス特性を持つべきですが、誤ったベンチマークではO(N²)の操作が行われていたため、実際の性能評価が歪められていました。

技術的詳細

このコミットが修正している問題は、ベンチマークの設計における根本的な誤りです。benchmarkAppendBytesbenchmarkAppendStrという2つのベンチマーク関数は、それぞれバイトスライスと文字列をアペンドする操作の性能を測定することを目的としていました。

元のコードでは、b.N回の外側ループの中に、さらにN回の内側ループが存在していました。

// 修正前 (例: benchmarkAppendBytes)
func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ { // 外側ループ: b.N回
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // 内側ループ: N回
            x = append(x, y...) // ここでアペンド
        }
    }
}

ここで、Nはベンチマーク関数に渡されるlength引数に対応する定数(またはテストケースごとに異なる値)です。

この構造の問題点は以下の通りです。

  1. 意図しない計算量: ベンチマークの目的が「Nバイトをアペンドする」ことであるにもかかわらず、内側のループがN回実行されるため、1回のb.Nイテレーションあたり合計でN回のappend操作が行われます。これにより、b.N回のベンチマーク実行全体では、b.N * N回のappend操作が実行されることになります。さらに、各append操作でlengthバイトが追加されるため、合計でb.N * N * lengthバイトがアペンドされることになります。これは、ベンチマークの意図である「Nバイトのアペンド」とはかけ離れた、に比例する作業量となります。
  2. 不正確な結果: b.Nはベンチマークの実行時間を一定に保つために自動調整されます。しかし、内側のループが存在することで、実際の作業量がに比例するため、ベンチマークが測定しているのは「Nバイトのアペンド」の性能ではなく、「N²バイトのアペンド」の性能になってしまいます。これは、append関数の真の性能特性を誤って評価する原因となります。
  3. リソース消費: Nが大きくなると、の作業量は非常に大きくなります。これにより、ベンチマークの実行に膨大な時間とメモリが必要となり、開発者のマシンやCI/CD環境に過度な負荷をかける可能性があります。

このコミットは、内側のfor j := 0; j < N; j++ループを削除することで、この問題を解決しています。これにより、1回のb.Nイテレーションあたり1回のappend操作のみが実行されるようになり、ベンチマークが意図した通りの「Nバイトのアペンド」のパフォーマンスを正確に測定できるようになります。

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

変更はsrc/pkg/runtime/append_test.goファイルにあります。

--- a/src/pkg/runtime/append_test.go
+++ b/src/pkg/runtime/append_test.go
@@ -26,9 +26,7 @@ func benchmarkAppendBytes(b *testing.B, length int) {
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 		x = x[0:0]
-		for j := 0; j < N; j++ {
-			x = append(x, y...)\n
-		}
+		x = append(x, y...)
 	}
 }
 
@@ -58,9 +56,7 @@ func benchmarkAppendStr(b *testing.B, str string) {
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 		x = x[0:0]
-		for j := 0; j < N; j++ {
-			x = append(x, str...)\n
-		}
+		x = append(x, str...)
 	}
 }

具体的には、以下の2つのベンチマーク関数から内側のループが削除されました。

  1. func benchmarkAppendBytes(b *testing.B, length int)
  2. func benchmarkAppendStr(b *testing.B, str string)

コアとなるコードの解説

benchmarkAppendBytes関数の変更

修正前:

func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // N回ループ
            x = append(x, y...) // N回アペンド
        }
    }
}

このコードでは、b.N回の外側ループの各イテレーションで、xスライスがリセットされた後、N回の内側ループが実行され、その中でappend(x, y...)N回呼び出されていました。ylengthバイトのデータを持つスライスです。したがって、1回のb.Nイテレーションで合計N * lengthバイトがアペンドされ、b.N回の実行全体ではb.N * N * lengthバイトがアペンドされることになります。これは、ベンチマークの意図である「lengthバイトのアペンド」とは異なる動作でした。

修正後:

func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        x = append(x, y...) // 1回だけアペンド
    }
}

内側のfor j := 0; j < N; j++ループが削除されました。これにより、b.N回の外側ループの各イテレーションで、xスライスがリセットされた後、append(x, y...)が1回だけ呼び出されるようになりました。これで、ベンチマークは正確に「lengthバイトを1回アペンドする」操作のパフォーマンスを測定するようになります。b.Nはベンチマークの実行回数を制御するため、これによりlengthバイトのアペンド操作がb.N回実行され、その合計時間が計測されます。

benchmarkAppendStr関数の変更

benchmarkAppendStr関数も同様の修正が施されています。この関数は文字列をアペンドする操作をベンチマークするためのものです。

修正前:

func benchmarkAppendStr(b *testing.B, str string) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // N回ループ
            x = append(x, str...) // N回アペンド
        }
    }
}

修正後:

func benchmarkAppendStr(b *testing.B, str string) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        x = append(x, str...) // 1回だけアペンド
    }
}

こちらも内側のループが削除され、1回のb.Nイテレーションあたり1回のappend操作のみが実行されるようになりました。これにより、文字列のアペンド操作のベンチマークも正確に行われるようになります。

これらの変更により、Goのappend関数のベンチマークは、その意図された目的(Nバイトのアペンド性能測定)を正確に反映するようになり、より信頼性の高いパフォーマンスデータを提供できるようになりました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語のランタイムパッケージ内のベンチマークテストに関する修正です。具体的には、`append`操作のベンチマークにおいて、意図せずNバイトではなくN²バイトをアペンドしてしまうというロジックの誤りを修正しています。これにより、ベンチマークが正確なパフォーマンス測定を行うように改善されました。

## コミット

- **コミットハッシュ**: `1f74aa21d54244ea7ee4e0cabc101eb6f0ad19cb`
- **作者**: Rémy Oudompheng <oudomphe@phare.normalesup.org>
- **コミット日時**: 2013年3月2日 土曜日 21:11:05 +0100
- **コミットメッセージ**: `runtime: benchmark for appending N bytes should not append N² bytes.`

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

[https://github.com/golang/go/commit/1f74aa21d54244ea7ee4e0cabc101eb6f0ad19cb](https://github.com/golang/go/commit/1f74aa21d54244ea7ee4e0cabc101eb6f0ad19cb)

## 元コミット内容

runtime: benchmark for appending N bytes should not append N² bytes.

R=golang-dev, r CC=golang-dev https://golang.org/cl/7420051


## 変更の背景

Go言語の標準ライブラリには、コードのパフォーマンスを測定するためのベンチマーク機能が組み込まれています。`append`関数はGoのスライス操作において非常に基本的なものであり、そのパフォーマンスはGoプログラム全体の効率に大きく影響します。このコミット以前のベンチマークコードでは、`append`操作のテストにおいて、`N`回のループ内でさらに`N`バイトのデータをアペンドするという二重ループ構造になっていました。

これは、ベンチマークの目的が「Nバイトをアペンドする際のパフォーマンス」を測定することであるにもかかわらず、実際には「N * N (N²) バイトをアペンドする際のパフォーマンス」を測定してしまっているという問題を引き起こしていました。`b.N`はベンチマーク関数が実行される回数を示すため、内側のループで`N`回アペンドを行うと、合計で`b.N * N`回の`append`操作、かつ`b.N * N * (アペンドされるバイト数)`のデータ処理が行われることになります。

この誤ったロジックは、ベンチマーク結果を不正確にし、`append`関数の実際の性能特性を誤解させる可能性がありました。特に、`N`が大きくなるにつれて、`N²`の計算量は非常に急速に増加するため、ベンチマークの実行時間が不必要に長くなり、リソースを大量に消費するという実用上の問題も発生していました。このコミットは、このベンチマークのロジックの誤りを修正し、意図した通りの「Nバイトのアペンド」のパフォーマンスを正確に測定できるようにすることを目的としています。

## 前提知識の解説

### Go言語のスライスと`append`関数

Go言語のスライスは、可変長シーケンスを扱うための強力なデータ構造です。内部的には、配列へのポインタ、長さ(`len`)、容量(`cap`)の3つの要素で構成されます。

`append`関数は、スライスに要素を追加するために使用されます。そのシグネチャは以下のようになります。

```go
func append(slice []Type, elems ...Type) []Type

appendの動作は以下の通りです。

  1. 容量の確認: appendはまず、既存のスライスの容量(cap)が、追加される要素を格納するのに十分であるかを確認します。
  2. 再割り当て: 容量が不足している場合、appendはより大きな新しい基底配列を割り当て、既存の要素を新しい配列にコピーし、その後で新しい要素を追加します。この再割り当ては、通常、現在の容量の2倍(またはそれ以上)の容量を持つ新しい配列を作成することで行われます。この操作はコストが高く、パフォーマンスに影響を与えます。
  3. 要素の追加: 容量が十分な場合、または新しい配列にコピーされた後、新しい要素が既存の要素の末尾に追加されます。
  4. 新しいスライスを返す: appendは、更新されたスライス(新しい基底配列へのポインタ、新しい長さ、新しい容量を持つ)を返します。元のスライスが変更される場合と、新しいスライスが返される場合があるため、常にx = append(x, y...)のように戻り値を元の変数に代入することが推奨されます。

Go言語のベンチマーク

Go言語には、testingパッケージを通じて組み込みのベンチマーク機能が提供されています。ベンチマーク関数はBenchmarkXxx(*testing.B)というシグネチャを持ちます。

  • *testing.B: ベンチマークのコンテキストを提供する構造体です。
  • b.N: ベンチマーク関数が実行される回数を示します。Goのテストフレームワークは、ベンチマークの実行時間を安定させるために、このb.Nの値を動的に調整します。つまり、b.Nは固定値ではなく、ベンチマークが十分な時間(デフォルトでは1秒以上)実行されるように自動的に増加します。
  • b.StartTimer() / b.StopTimer(): これらのメソッドは、ベンチマークの計測を開始/停止するために使用されます。通常、セットアップコードはStartTimer()の前に置き、クリーンアップコードはStopTimer()の後に置きます。
  • b.ResetTimer(): タイマーをリセットし、それまでの計測時間をクリアします。

ベンチマークは、go test -bench=.コマンドで実行されます。

計算量(オーダー記法)

計算量とは、アルゴリズムの効率性を入力サイズNの関数として表現する方法です。オーダー記法(Big O notation)は、アルゴリズムの実行時間やメモリ使用量が入力サイズの増加に伴ってどのように変化するかを示すために使われます。

  • O(N): 線形時間。入力サイズNに比例して実行時間が増加します。例えば、配列のすべての要素を一度だけ処理する場合など。
  • O(N²): 二次時間。入力サイズNの二乗に比例して実行時間が増加します。例えば、ネストされたループで各要素のペアを処理する場合など。Nが大きくなると、実行時間は非常に急速に増加します。

このコミットの文脈では、「Nバイトをアペンドする」という操作は理想的にはO(N)に近いパフォーマンス特性を持つべきですが、誤ったベンチマークではO(N²)の操作が行われていたため、実際の性能評価が歪められていました。

技術的詳細

このコミットが修正している問題は、ベンチマークの設計における根本的な誤りです。benchmarkAppendBytesbenchmarkAppendStrという2つのベンチマーク関数は、それぞれバイトスライスと文字列をアペンドする操作の性能を測定することを目的としていました。

元のコードでは、b.N回の外側ループの中に、さらにN回の内側ループが存在していました。

// 修正前 (例: benchmarkAppendBytes)
func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ { // 外側ループ: b.N回
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // 内側ループ: N回
            x = append(x, y...) // ここでアペンド
        }
    }
}

ここで、Nはベンチマーク関数に渡されるlength引数に対応する定数(またはテストケースごとに異なる値)です。

この構造の問題点は以下の通りです。

  1. 意図しない計算量: ベンチマークの目的が「Nバイトをアペンドする」ことであるにもかかわらず、内側のループがN回実行されるため、1回のb.Nイテレーションあたり合計でN回のappend操作が行われます。これにより、b.N回のベンチマーク実行全体では、b.N * N回のappend操作が実行されることになります。さらに、各append操作でlengthバイトが追加されるため、合計でb.N * N * lengthバイトがアペンドされることになります。これは、ベンチマークの意図である「Nバイトのアペンド」とはかけ離れた、に比例する作業量となります。
  2. 不正確な結果: b.Nはベンチマークの実行時間を一定に保つために自動調整されます。しかし、内側のループが存在することで、実際の作業量がに比例するため、ベンチマークが測定しているのは「Nバイトのアペンド」の性能ではなく、「N²バイトのアペンド」の性能になってしまいます。これは、append関数の真の性能特性を誤って評価する原因となります。
  3. リソース消費: Nが大きくなると、の作業量は非常に大きくなります。これにより、ベンチマークの実行に膨大な時間とメモリが必要となり、開発者のマシンやCI/CD環境に過度な負荷をかける可能性があります。

このコミットは、内側のfor j := 0; j < N; j++ループを削除することで、この問題を解決しています。これにより、1回のb.Nイテレーションあたり1回のappend操作のみが実行されるようになり、ベンチマークが意図した通りの「Nバイトのアペンド」のパフォーマンスを正確に測定できるようになります。

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

変更はsrc/pkg/runtime/append_test.goファイルにあります。

--- a/src/pkg/runtime/append_test.go
+++ b/src/pkg/runtime/append_test.go
@@ -26,9 +26,7 @@ func benchmarkAppendBytes(b *testing.B, length int) {
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 		x = x[0:0]
-		for j := 0; j < N; j++ {
-			x = append(x, y...)
-		}
+		x = append(x, y...)
 	}
 }
 
@@ -58,9 +56,7 @@ func benchmarkAppendStr(b *testing.B, str string) {
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
 		x = x[0:0]
-		for j := 0; j < N; j++ {
-			x = append(x, str...)
-		}
+		x = append(x, str...)
 	}
 }

具体的には、以下の2つのベンチマーク関数から内側のループが削除されました。

  1. func benchmarkAppendBytes(b *testing.B, length int)
  2. func benchmarkAppendStr(b *testing.B, str string)

コアとなるコードの解説

benchmarkAppendBytes関数の変更

修正前:

func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // N回ループ
            x = append(x, y...) // N回アペンド
        }
    }
}

このコードでは、b.N回の外側ループの各イテレーションで、xスライスがリセットされた後、N回の内側ループが実行され、その中でappend(x, y...)N回呼び出されていました。ylengthバイトのデータを持つスライスです。したがって、1回のb.Nイテレーションで合計N * lengthバイトがアペンドされ、b.N回の実行全体ではb.N * N * lengthバイトがアペンドされることになります。これは、ベンチマークの意図である「lengthバイトのアペンド」とは異なる動作でした。

修正後:

func benchmarkAppendBytes(b *testing.B, length int) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        x = append(x, y...) // 1回だけアペンド
    }
}

内側のfor j := 0; j < N; j++ループが削除されました。これにより、b.N回の外側ループの各イテレーションで、xスライスがリセットされた後、append(x, y...)が1回だけ呼び出されるようになりました。これで、ベンチマークは正確に「lengthバイトを1回アペンドする」操作のパフォーマンスを測定するようになります。b.Nはベンチマークの実行回数を制御するため、これによりlengthバイトのアペンド操作がb.N回実行され、その合計時間が計測されます。

benchmarkAppendStr関数の変更

benchmarkAppendStr関数も同様の修正が施されています。この関数は文字列をアペンドする操作をベンチマークするためのものです。

修正前:

func benchmarkAppendStr(b *testing.B, str string) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        for j := 0; j < N; j++ { // N回ループ
            x = append(x, str...)
        }
    }
}

修正後:

func benchmarkAppendStr(b *testing.B, str string) {
    // ... 初期化 ...
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        x = x[0:0] // スライスをリセット
        x = append(x, str...) // 1回だけアペンド
    }
}

こちらも内側のループが削除され、1回のb.Nイテレーションあたり1回のappend操作のみが実行されるようになりました。これにより、文字列のアペンド操作のベンチマークも正確に行われるようになります。

これらの変更により、Goのappend関数のベンチマークは、その意図された目的(Nバイトのアペンド性能測定)を正確に反映するようになり、より信頼性の高いパフォーマンスデータを提供できるようになりました。

関連リンク

参考にした情報源リンク