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

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

このコミットは、Go言語のランタイムパッケージ内のcomplex_test.goファイルにおける、複素数除算のベンチマークテストに関する修正です。具体的には、ベンチマークのイテレーション中に除算結果を被除数(n)に加算すると、nNaN(非数)やInf(無限大)になる可能性があり、ベンチマーク結果が不安定になる問題を解決しています。

コミット

runtime: fix complex division benchmarks
we can't add the division result to n during iteration, because it might
turn n into NaN or Inf.

R=golang-dev, rsc, iant, iant
CC=golang-dev
https://golang.org/cl/6197045

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

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

元コミット内容

commit aa45e52e74f37e39a5a8234071742d50b87b7b2c
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri May 11 03:09:14 2012 +0800

    runtime: fix complex division benchmarks
    we can't add the division result to n during iteration, because it might
    turn n into NaN or Inf.
    
    R=golang-dev, rsc, iant, iant
    CC=golang-dev
    https://golang.org/cl/6197045
---
 src/pkg/runtime/complex_test.go | 30 ++++++++++++++++++++----------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/src/pkg/runtime/complex_test.go b/src/pkg/runtime/complex_test.go
index ebe2d59124..f41e6a3570 100644
--- a/src/pkg/runtime/complex_test.go
+++ b/src/pkg/runtime/complex_test.go
@@ -14,44 +14,54 @@ var result complex128
 func BenchmarkComplex128DivNormal(b *testing.B) {
 	d := 15 + 2i
 	n := 32 + 3i
+	res := 0i
 	for i := 0; i < b.N; i++ {
-		n += n / d
+		n += 0.1i
+		res += n / d
 	}
-	result = n
+	result = res
 }
 
 func BenchmarkComplex128DivNisNaN(b *testing.B) {
 	d := cmplx.NaN()
 	n := 32 + 3i
+	res := 0i
 	for i := 0; i < b.N; i++ {
-		n += n / d
+		n += 0.1i
+		res += n / d
 	}
-	result = n
+	result = res
 }
 
 func BenchmarkComplex128DivDisNaN(b *testing.B) {
 	d := 15 + 2i
 	n := cmplx.NaN()
+	res := 0i
 	for i := 0; i < b.N; i++ {
-		n += n / d
+		d += 0.1i
+		res += n / d
 	}
-	result = n
+	result = res
 }
 
 func BenchmarkComplex128DivNisInf(b *testing.B) {
 	d := 15 + 2i
 	n := cmplx.Inf()
+	res := 0i
 	for i := 0; i < b.N; i++ {\n-\t\tn += n / d\n+\t\td += 0.1i\n+\t\tres += n / d\n \t}\n-\tresult = n\n+\tresult = res\n }\n \n func BenchmarkComplex128DivDisInf(b *testing.B) {\n \td := cmplx.Inf()\n \tn := 32 + 3i\n+\tres := 0i\n \tfor i := 0; i < b.N; i++ {\n-\t\tn += n / d\n+\t\tn += 0.1i\n+\t\tres += n / d\n \t}\n-\tresult = n\n+\tresult = res\n }\n```

## 変更の背景

この変更の背景には、Go言語のベンチマークテストの正確性と安定性を確保するという目的があります。元のベンチマークコードでは、複素数除算の性能を測定する際に、ループ内で被除数`n`に除算結果`n / d`を繰り返し加算していました。

しかし、浮動小数点演算、特に複素数演算においては、特定の条件下で`NaN`(Not a Number、非数)や`Inf`(Infinity、無限大)といった特殊な値が発生する可能性があります。例えば、`0/0`や`Inf/Inf`のような演算は`NaN`を生成し、非ゼロ数を`0`で割ると`Inf`を生成します。

ベンチマークのイテレーション中に`n`が`NaN`や`Inf`になると、それ以降の計算結果も`NaN`や`Inf`に伝播し、ベンチマークの測定対象である除算演算自体の性能ではなく、特殊な値の伝播や処理にかかる時間が測定されてしまう可能性があります。これは、ベンチマークが本来測定すべき「通常の」除算性能を正確に反映しないことを意味します。

この問題を解決し、ベンチマークが常に安定した、意味のある結果を返すようにするために、除算結果を直接`n`に加算するのではなく、別の変数に蓄積し、かつ`n`や`d`がループ内で`NaN`や`Inf`にならないように微小な値を加算して変化させるように修正されました。

## 前提知識の解説

### 複素数 (Complex Numbers)

複素数は実数部と虚数部を持つ数で、`a + bi`の形式で表されます。ここで`a`と`b`は実数、`i`は虚数単位で`i^2 = -1`を満たします。Go言語では、`complex64`(実数部と虚数部が`float32`)と`complex128`(実数部と虚数部が`float64`)の2つの複素数型が組み込みで提供されています。

### 浮動小数点数 (Floating-Point Numbers)

コンピュータで実数を近似的に表現するための形式です。IEEE 754標準が広く用いられており、`float32`(単精度)と`float64`(倍精度)があります。浮動小数点演算には、以下のような特殊な値が存在します。

*   **NaN (Not a Number)**: 不定形な演算結果(例: `0/0`, `Inf - Inf`, `Inf * 0`)を表します。`NaN`を含む演算の結果は通常`NaN`になります。
*   **Inf (Infinity)**: オーバーフロー(例: 非常に大きな数を表現しようとした場合)や、非ゼロ数を`0`で割った結果(例: `1/0`)を表します。正の無限大と負の無限大があります。

### Go言語のベンチマークテスト (Go Benchmarking)

Go言語には、コードの性能を測定するためのベンチマーク機能が組み込まれています。`testing`パッケージを使用し、関数名のプレフィックスを`Benchmark`とすることでベンチマーク関数として認識されます。

ベンチマーク関数は`*testing.B`型の引数を受け取ります。この`B`オブジェクトには、ベンチマークの実行回数を制御する`b.N`フィールドがあります。ベンチマークループは`for i := 0; i < b.N; i++`の形式で記述され、`b.N`はベンチマーク実行中に動的に調整され、安定した測定結果が得られるように試行回数が決定されます。

ベンチマークの測定対象となる処理は、このループ内で実行されます。また、ベンチマーク結果がコンパイラによって最適化されて消滅しないように、最終結果をグローバル変数に代入するなどの工夫(例: `result = someValue`)がよく行われます。

### `cmplx`パッケージ

Go言語の標準ライブラリには、複素数に関する数学関数を提供する`math/cmplx`パッケージがあります。このパッケージには、複素数の`NaN`や`Inf`を生成するための関数(例: `cmplx.NaN()`, `cmplx.Inf()`)も含まれています。

## 技術的詳細

このコミットの技術的詳細は、浮動小数点演算の特性とベンチマークの正確性という2つの側面に集約されます。

1.  **浮動小数点演算の伝播特性**:
    *   `NaN`は「伝播」する性質を持っています。つまり、`NaN`を含むほとんどの算術演算の結果は`NaN`になります。例えば、`NaN + X`、`NaN * X`、`NaN / X`はすべて`NaN`になります。
    *   `Inf`も同様に伝播することがありますが、`Inf - Inf`や`Inf / Inf`、`Inf * 0`のように不定形な演算では`NaN`になることがあります。
    *   元のベンチマークコードでは、`n += n / d`という操作が行われていました。もし`n / d`の計算過程で`NaN`や`Inf`が発生した場合、その結果が`n`に加算されることで、`n`自体も`NaN`や`Inf`になってしまいます。一度`n`が`NaN`や`Inf`になると、それ以降のループイテレーションでの`n / d`の計算も`NaN`や`Inf`を生成し続け、ベンチマークが測定すべき「通常の」除算性能とはかけ離れた結果になってしまいます。

2.  **ベンチマークの安定性**:
    *   ベンチマークは、特定の操作の性能を安定して、かつ再現性高く測定することが求められます。ベンチマークの実行ごとに結果が大きく変動したり、特殊な値によって測定が歪められたりすることは望ましくありません。
    *   元のコードでは、`n`の値がループ内で大きく変化し、場合によっては`NaN`や`Inf`になることで、ベンチマークの安定性が損なわれていました。これは、ベンチマークが測定対象のコードパスを常に同じ状態で実行することを保証できないためです。
    *   修正後のコードでは、除算結果を蓄積するための新しい変数`res`を導入し、`n`(または`d`)自体はベンチマークの測定対象とは直接関係のない微小な値(`0.1i`)を加算することで、ループ内で`n`や`d`が`NaN`や`Inf`になるリスクを低減しています。これにより、除算演算自体は常に「健全な」入力値で行われるようになり、ベンチマークの安定性と正確性が向上します。
    *   また、`n += 0.1i`のように`n`や`d`をわずかに変化させることで、コンパイラがループ内の計算を定数畳み込みなどで最適化してしまい、実際の演算がスキップされることを防ぐ効果も期待できます。ベンチマークでは、測定対象のコードが実際に実行されることが重要です。

この修正は、Go言語のランタイムの品質と、ベンチマークテストの信頼性を高める上で重要な改善と言えます。

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

変更は`src/pkg/runtime/complex_test.go`ファイルに集中しています。

各ベンチマーク関数(`BenchmarkComplex128DivNormal`, `BenchmarkComplex128DivNisNaN`, `BenchmarkComplex128DivDisNaN`, `BenchmarkComplex128DivNisInf`, `BenchmarkComplex128DivDisInf`)において、以下の変更が行われています。

1.  新しい変数`res`(型は`complex128`、初期値は`0i`)が導入されました。
2.  ループ内の`n += n / d`という行が削除されました。
3.  代わりに、`n`または`d`に微小な値(`0.1i`)を加算する行が追加されました。
    *   `BenchmarkComplex128DivNormal`, `BenchmarkComplex128DivNisNaN`, `BenchmarkComplex128DivDisInf`では`n += 0.1i`。
    *   `BenchmarkComplex128DivDisNaN`, `BenchmarkComplex128DivNisInf`では`d += 0.1i`。
4.  除算結果を`res`に加算する`res += n / d`という行が追加されました。
5.  最終結果を`result`に代入する行が`result = n`から`result = res`に変更されました。

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

修正されたベンチマーク関数の一つである`BenchmarkComplex128DivNormal`を例に解説します。

**変更前:**

```go
func BenchmarkComplex128DivNormal(b *testing.B) {
	d := 15 + 2i
	n := 32 + 3i
	for i := 0; i < b.N; i++ {
		n += n / d // ここでnがNaNやInfになる可能性があった
	}
	result = n // 最終結果がnに依存
}

このコードでは、ループ内でnn / dの結果によって更新されていました。もしn / dNaNInfを生成すると、その後のnの値もNaNInfになり、ベンチマークの測定が歪められる可能性がありました。

変更後:

func BenchmarkComplex128DivNormal(b *testing.B) {
	d := 15 + 2i
	n := 32 + 3i
	res := 0i // 新しい変数resを導入
	for i := 0; i < b.N; i++ {
		n += 0.1i   // nを微小に変化させるが、NaN/Infになるリスクは低い
		res += n / d // 除算結果をresに蓄積
	}
	result = res // 最終結果がresに依存
}

変更後のコードでは、以下の点が改善されています。

  1. res := 0iの導入: 除算の累積結果を保持するための新しい変数resが導入されました。これにより、除算結果が直接nにフィードバックされることがなくなり、nNaNInfになるリスクが大幅に減少しました。
  2. n += 0.1i (または d += 0.1i): ループ内でn(またはd)に微小な値0.1iを加算しています。これは主に以下の目的のためです。
    • コンパイラの最適化回避: ベンチマークループ内の変数が全く変化しない場合、コンパイラがループ内の計算を定数畳み込みなどで最適化してしまい、実際の演算がスキップされる可能性があります。微小な変化を加えることで、コンパイラが最適化を抑制し、実際に除算演算が各イテレーションで実行されることを保証します。
    • NaN/Infの発生抑制: ndNaNInfになるような極端な値に変化することを防ぎつつ、ベンチマークの測定対象である除算演算の入力値が毎回わずかに異なるようにしています。これにより、より現実的なシナリオでの性能を測定しやすくなります。
  3. res += n / d: 各イテレーションで計算されたn / dの結果は、resに累積されます。これにより、ベンチマークの最終結果が、ループ内で実行されたすべての除算演算の合計に依存するようになります。
  4. result = res: 最終的に、ベンチマークの結果はresの値に設定されます。これにより、ベンチマークが測定すべき「除算演算の累積結果」が正しく反映されるようになります。

これらの変更により、ベンチマークはより安定し、浮動小数点数の特殊な値によって結果が歪められることなく、複素数除算の実際の性能を正確に測定できるようになりました。

関連リンク

参考にした情報源リンク