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

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

このコミットは、Go言語の標準ライブラリmath/randパッケージにおけるFloat32およびFloat64関数の挙動を修正し、Go 1.2で確立された乱数ストリームの再現性を維持することを目的としています。以前の修正で導入された、ドキュメントに反して1.0を返す可能性のあるバグを修正しつつ、その修正が意図せず乱数ストリーム自体を変更してしまった問題を解決します。これにより、特定のシード値(特にrand.Seed(0))やカスタムのSourceを使用しているプログラムが、予期せぬ乱数シーケンスの変更によって影響を受けることを防ぎます。

コミット

commit 5aca0514941ce7dd0f3cea8d8ffe627dbcd542ca
Author: Russ Cox <rsc@golang.org>
Date:   Mon May 19 12:30:25 2014 -0400

    math/rand: restore Go 1.2 value stream for Float32, Float64
    
    CL 22730043 fixed a bug in these functions: they could
    return 1.0 despite documentation saying otherwise.
    But the fix changed the values returned in the non-buggy case too,
    which might invalidate programs depending on a particular
    stream when using rand.Seed(0) or when passing their own
    Source to rand.New.
    
    The example test says:
            // These tests serve as an example but also make sure we don't change
            // the output of the random number generator when given a fixed seed.
    so I think there is some justification for thinking we have
    promised not to change the values. In any case, there's no point in
    changing the values gratuitously: we can easily fix this bug without
    changing the values, and so we should.
    
    That CL just changed the test values too, which defeats the
    stated purpose, but it was just a comment.
    Add an explicit regression test, which might be
    a clearer signal next time that we don't want to change
    the values.
    
    Fixes #6721. (again)
    Fixes #8013.
    
    LGTM=r
    R=iant, r
    CC=golang-codereviews
    https://golang.org/cl/95460049

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

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

元コミット内容

このコミットの目的は、math/randパッケージのFloat32およびFloat64関数が、Go 1.2で確立された乱数ストリームの値を維持しつつ、以前のバグ(1.0を返す可能性があった問題)を修正することです。

以前の変更(CL 22730043)は、これらの関数がドキュメントに反して1.0を返す可能性があったバグを修正しました。しかし、この修正はバグのないケースでも返される値を変更してしまい、rand.Seed(0)を使用しているプログラムや、独自のSourcerand.Newに渡しているプログラムが、特定の乱数ストリームに依存している場合に無効になる可能性がありました。

math/randの例のテストには、「これらのテストは例として機能するだけでなく、固定シードが与えられた場合に乱数ジェネレータの出力を変更しないことを確認する」というコメントがあります。これは、Goチームが乱数ストリームの安定性を約束していると解釈できる根拠となります。乱数の値を不必要に変更する意味はなく、値を変更せずにバグを修正できるのであれば、そうすべきであるという考えに基づいています。

以前のCLでは、テスト値も変更されていましたが、これは単なるコメントであり、本来の目的を損なっていました。このコミットでは、明示的な回帰テストを追加することで、今後乱数ストリームの値を変更しないという意図をより明確に示しています。

このコミットは、Issue #6721(再発)とIssue #8013を修正します。

変更の背景

このコミットの背景には、Go言語のmath/randパッケージにおける乱数生成の「再現性」と「安定性」という重要な設計原則があります。

  1. Float32およびFloat64関数のバグ: math/randパッケージのFloat32およびFloat64関数は、ドキュメント上は[0.0, 1.0)の範囲(0.0以上1.0未満)の擬似乱数を返すことになっています。しかし、以前の実装では、浮動小数点演算の特性上、ごく稀に1.0を返してしまうバグが存在しました。これはドキュメントとの乖離であり、予期せぬ挙動を引き起こす可能性がありました。

  2. 以前の修正とその副作用: この1.0を返すバグは、CL 22730043(Go 1.2のリリースサイクル中に行われた修正)によって一度修正されました。しかし、この修正はバグを直す一方で、乱数生成の内部ロジックを変更してしまい、結果としてrand.Seed(0)などの固定シード値を用いた場合に生成される乱数シーケンスが、Go 1.2の時点での期待値と異なってしまうという副作用を生じさせました。

  3. 乱数ストリームの安定性の重要性: math/randパッケージは、暗号学的に安全な乱数ではなく、再現可能な擬似乱数を生成することを目的としています。特に、rand.Seed(0)のように固定のシード値を与えた場合、常に同じ乱数シーケンスが生成されることが期待されます。これは、テストの再現性、シミュレーション、あるいは特定のアルゴリズムの挙動をデバッグする際に非常に重要です。Goの標準ライブラリのテストコードにも、固定シードでの乱数出力が変更されないことを保証する意図が明記されていました。以前の修正がこの「約束」を破ってしまったため、既存のプログラムが予期せぬ動作をする可能性がありました。

  4. 回帰テストの必要性: 以前の修正では、乱数ストリームの変更に伴い、関連するテストの期待値も変更されていました。これは、乱数ストリームの安定性を保証するというテスト本来の目的を損なうものでした。そのため、乱数ストリームの意図しない変更を将来的に防ぐための、より堅牢な回帰テストの導入が必要とされました。

このコミットは、上記の背景を踏まえ、1.0を返すバグを再修正しつつ、Go 1.2で確立された乱数ストリームの再現性を回復し、さらにその安定性を保証するための回帰テストを追加することで、math/randパッケージの信頼性と予測可能性を高めることを目指しています。

前提知識の解説

このコミットを理解するためには、以下の前提知識が役立ちます。

  1. 擬似乱数生成器 (PRNG):

    • コンピュータで生成される乱数は、実際には「擬似乱数」であり、ある初期値(シード)から決定論的なアルゴリズムによって生成されます。
    • 同じシードを与えれば、常に同じ乱数シーケンスが生成されます。これが「再現性」の基盤となります。
    • Goのmath/randパッケージは、この擬似乱数生成器を提供します。
  2. math/randパッケージ:

    • Goの標準ライブラリの一部で、擬似乱数を生成するための機能を提供します。
    • rand.Seed(seed int64): 乱数生成器のシードを設定します。同じシード値を与えると、以降の乱数シーケンスは常に同じになります。rand.Seed(0)は、デフォルトのシード値(通常は1)を設定する特殊なケースとして扱われることがあります。
    • rand.New(source Source): カスタムの乱数生成器のソース(Sourceインターフェースを実装したもの)を使用して、新しいRandインスタンスを作成します。
    • Float64(): [0.0, 1.0)の範囲のfloat64型の擬似乱数を返します。
    • Float32(): [0.0, 1.0)の範囲のfloat32型の擬似乱数を返します。
  3. 浮動小数点数 (Floating-Point Numbers):

    • コンピュータが実数を表現する方法です。float32は単精度浮動小数点数、float64は倍精度浮動小数点数です。
    • 浮動小数点数は、その性質上、正確な値を表現できない場合があります(丸め誤差)。特に、整数演算の結果を浮動小数点数に変換する際や、除算を行う際に、意図しない丸めが発生することがあります。
    • [0.0, 1.0)という範囲は、0.0は含むが1.0は含まないことを意味します。これは、乱数生成において一般的な慣習です。
  4. 乱数ストリームの安定性 (Value Stream Stability):

    • 特定のシード値から生成される乱数シーケンスが、ライブラリのバージョンアップや修正によって変更されないことを指します。
    • Goのような安定性を重視する言語では、互換性を保つために、このような決定論的な挙動が維持されることが非常に重要視されます。もし乱数ストリームが変更されると、その乱数に依存する既存のプログラム(例えば、シミュレーション、ゲーム、テストケースなど)が予期せぬ結果を生み出す可能性があります。
  5. GoのCL (Change List) とIssue:

    • Goプロジェクトでは、コード変更はGerritというコードレビューシステムを通じて提案され、各変更は「Change List (CL)」として管理されます。コミットメッセージに記載されるCL XXXXXXXXはそのCLのIDを指します。
    • Issueは、バグ報告や機能要望などを追跡するためのものです。Fixes #XXXXという記述は、そのコミットが特定のIssueを解決することを示します。

技術的詳細

このコミットの技術的詳細は、math/randパッケージにおけるFloat64およびFloat32関数の実装における浮動小数点演算の丸め誤差と、乱数ストリームの再現性維持のバランスにあります。

Float64()関数の修正

元のFloat64()関数の実装は、float64(r.Int63n(1<<53)) / (1 << 53)という形式でした。これは、Int63nで生成された整数をfloat64にキャストし、それを1 << 53で割ることで[0.0, 1.0)の範囲の浮動小数点数を得ようとするものです。しかし、Go 1の出荷時にはfloat64(r.Int63()) / (1 << 63)という実装が採用されていました。

このコミットでは、Go 1の乱数ストリームを維持するために、後者の実装をベースに修正が加えられています。

変更後のFloat64()のロジックは以下の通りです。

func (r *Rand) Float64() float64 {
	// A clearer, simpler implementation would be:
	//	return float64(r.Int63n(1<<53)) / (1<<53)
	// However, Go 1 shipped with
	//	return float64(r.Int63()) / (1 << 63)
	// and we want to preserve that value stream.
	//
	// There is one bug in the value stream: r.Int63() may be so close
	// to 1<<63 that the division rounds up to 1.0, and we've guaranteed
	// that the result is always less than 1.0. To fix that, we treat the
	// range as cyclic and map 1 back to 0. This is justified by observing
	// that while some of the values rounded down to 0, nothing was
	// rounding up to 0, so 0 was underrepresented in the results.
	// Mapping 1 back to zero restores some balance.
	// (The balance is not perfect because the implementation
	// returns denormalized numbers for very small r.Int63(),
	// and those steal from what would normally be 0 results.)
	// The remapping only happens 1/2⁵³ of the time, so most clients
	// will not observe it anyway.
	f := float64(r.Int63()) / (1 << 63)
	if f == 1 {
		f = 0
	}
	return f
}
  • Go 1のストリーム維持: float64(r.Int63()) / (1 << 63)という計算式は、Go 1で採用されたものであり、このコミットではこの計算式を維持することで、Go 1.2の乱数ストリームを再現しています。
  • 1.0問題の修正: r.Int63()1 << 63に近い値を返した場合、除算の結果が浮動小数点数の丸めによって1.0になってしまう可能性がありました。これを修正するため、計算結果f1.0と厳密に等しい場合にf0に再マッピングしています。
  • 0の過少表現の是正: この1.0から0へのマッピングは、0が乱数ストリーム中で過少に表現されていた問題を部分的に是正するという側面も持っています。これは、0に丸められる値はあったものの、0に丸め上げられる値はなかったため、0の出現頻度が低かったという観察に基づいています。
  • 影響の限定性: この再マッピングは、1/2^53という非常に低い確率でしか発生しないため、ほとんどのクライアントには影響がないと説明されています。

Float32()関数の修正

Float32()関数も同様の理由で修正されています。

func (r *Rand) Float32() float32 {
	// Same rationale as in Float64: we want to preserve the Go 1 value
	// stream except we want to fix it not to return 1.0
	// There is a double rounding going on here, but the argument for
	// mapping 1 to 0 still applies: 0 was underrepresented before,
	// so mapping 1 to 0 doesn't cause too many 0s.
	// This only happens 1/2²⁴ of the time (plus the 1/2⁵³ of the time in Float64).
	f := float32(r.Float64())
	if f == 1 {
		f = 0
	}
	return f
}
  • Float64()の利用: Float32()は、まずFloat64()を呼び出してfloat64の乱数を生成し、それをfloat32にキャストしています。
  • 1.0問題の修正: Float64()と同様に、float32にキャストした結果が1.0になった場合に0に再マッピングすることで、1.0が返されるバグを修正しています。
  • 二重丸め: Float64()からfloat32へのキャストで二重丸めが発生する可能性が指摘されていますが、1.0から0へのマッピングの正当性はFloat64()の場合と同様に適用されると説明されています。

回帰テストの追加 (regress_test.go)

このコミットの重要な変更点の一つは、src/pkg/math/rand/regress_test.goという新しいテストファイルが追加されたことです。

  • 目的: 特定のシード値(rand.Seed(0))で生成される乱数シーケンスが、Goのバージョンアップによって変更されないことを保証するための回帰テストです。
  • ゴールデンデータ: このテストは、過去のバージョンで生成された乱数シーケンスの「ゴールデンデータ」(期待される出力値のリスト)と比較することで、現在の実装が過去の挙動と一致しているかを確認します。
  • 変更禁止: テストファイルのコメントには、「ゴールデン出力を変更してはならない。もし基盤となるコードにバグが見つかった場合でも、出力に影響を与えない方法で修正を見つけること」と明記されており、乱数ストリームの安定性に対する強いコミットメントが示されています。
  • テスト対象: ExpFloat64, Float32, Float64, Int, Int31, Int31n, Int63, Int63n, Intn, NormFloat64, Perm, Uint32など、math/randパッケージの主要な乱数生成関数が網羅的にテストされています。

この回帰テストの導入により、将来的にmath/randパッケージの内部実装が変更された場合でも、意図しない乱数ストリームの変更が自動的に検出されるようになり、ライブラリの互換性と安定性が大幅に向上しました。

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

このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルです。

  1. src/pkg/math/rand/example_test.go:

    • Example_rand()関数の出力コメントが変更されています。これは、以前のCLで乱数ストリームが変更された際に、テストの期待値も更新されてしまったため、このコミットで乱数ストリームがGo 1.2の値に戻されたことに合わせて、正しい期待値に修正されたものです。
    --- a/src/pkg/math/rand/example_test.go
    +++ b/src/pkg/math/rand/example_test.go
    @@ -83,8 +83,8 @@ func Example_rand() {
     	// Perm generates a random permutation of the numbers [0, n).
     	show("Perm", r.Perm(5), r.Perm(5), r.Perm(5))
     	// Output:
    -	// Float32     0.73793465          0.38461488          0.9940225
    -	// Float64     0.6919607852308565  0.29140004584133117 0.2262092163027547
    +	// Float32     0.2635776           0.6358173           0.6718283
    +	// Float64     0.628605430454327   0.4504798828572669  0.9562755949377957
     	// ExpFloat64  0.3362240648200941  1.4256072328483647  0.24354758816173044
     	// NormFloat64 0.17233959114940064 1.577014951434847   0.04259129641113857
     	// Int31       1501292890          1486668269          182840835
    
  2. src/pkg/math/rand/rand.go:

    • Float64()関数とFloat32()関数の実装が変更されています。
    • Float64()は、Go 1の乱数ストリームを維持するためにfloat64(r.Int63()) / (1 << 63)という計算式に戻され、結果が1.0になった場合に0に再マッピングするロジックが追加されました。
    • Float32()も同様に、float32(r.Float64())の結果が1.0になった場合に0に再マッピングするロジックが追加されました。
    --- a/src/pkg/math/rand/rand.go
    +++ b/src/pkg/math/rand/rand.go
    @@ -101,10 +101,46 @@ func (r *Rand) Intn(n int) int {
     }
     
     // Float64 returns, as a float64, a pseudo-random number in [0.0,1.0).
    -func (r *Rand) Float64() float64 { return float64(r.Int63n(1<<53)) / (1 << 53) }
    +func (r *Rand) Float64() float64 {
    +	// A clearer, simpler implementation would be:
    +	//	return float64(r.Int63n(1<<53)) / (1<<53)
    +	// However, Go 1 shipped with
    +	//	return float64(r.Int63()) / (1 << 63)
    +	// and we want to preserve that value stream.
    +	//
    +	// There is one bug in the value stream: r.Int63() may be so close
    +	// to 1<<63 that the division rounds up to 1.0, and we've guaranteed
    // that the result is always less than 1.0. To fix that, we treat the
    // range as cyclic and map 1 back to 0. This is justified by observing
    // that while some of the values rounded down to 0, nothing was
    // rounding up to 0, so 0 was underrepresented in the results.
    // Mapping 1 back to zero restores some balance.
    // (The balance is not perfect because the implementation
    // returns denormalized numbers for very small r.Int63(),
    // and those steal from what would normally be 0 results.)
    // The remapping only happens 1/2⁵³ of the time, so most clients
    // will not observe it anyway.
    +	f := float64(r.Int63()) / (1 << 63)
    +	if f == 1 {
    +		f = 0
    +	}
    +	return f
    +}
     
     // Float32 returns, as a float32, a pseudo-random number in [0.0,1.0).
    -func (r *Rand) Float32() float32 { return float32(r.Int31n(1<<24)) / (1 << 24) }
    +func (r *Rand) Float32() float32 {
    +	// Same rationale as in Float64: we want to preserve the Go 1 value
    +	// stream except we want to fix it not to return 1.0
    +	// There is a double rounding going on here, but the argument for
    +	// mapping 1 to 0 still applies: 0 was underrepresented before,
    +	// so mapping 1 to 0 doesn't cause too many 0s.
    +	// This only happens 1/2²⁴ of the time (plus the 1/2⁵³ of the time in Float64).
    +	f := float32(r.Float64())
    +	if f == 1 {
    +		f = 0
    +	}
    +	return f
    +}
     
     // Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n).
     func (r *Rand) Perm(n int) []int {
    
  3. src/pkg/math/rand/regress_test.go:

    • 新しいファイルとして追加されました。
    • TestRegress関数が含まれており、rand.Seed(0)で初期化されたRandインスタンスの様々な乱数生成関数の出力が、ハードコードされた「ゴールデンデータ」と比較されます。
    • このテストは、乱数ストリームの意図しない変更を検出するためのものです。
    --- /dev/null
    +++ b/src/pkg/math/rand/regress_test.go
    @@ -0,0 +1,355 @@
    +// Copyright 2014 The Go Authors.  All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +// Test that random number sequences generated by a specific seed
    +// do not change from version to version.
    +//
    +// Do NOT make changes to the golden outputs. If bugs need to be fixed
    +// in the underlying code, find ways to fix them that do not affect the
    +// outputs.
    +
    // ... (テストコードの詳細は省略) ...
    

コアとなるコードの解説

このコミットの核心は、math/randパッケージのFloat64()Float32()関数が、[0.0, 1.0)という定義された範囲を厳密に守りつつ、Go 1.2で確立された乱数ストリームの再現性を維持するように修正された点にあります。

Float64()の修正ロジック

以前のFloat64()の実装は、r.Int63()(63ビットの非負整数を返す)の結果をfloat64にキャストし、それを1 << 63で割ることで[0.0, 1.0)の範囲の浮動小数点数を得ようとしていました。しかし、この計算において、r.Int63()が非常に大きな値(1 << 63に近い値)を返した場合、浮動小数点数の丸め誤差によって結果が1.0になってしまう可能性がありました。これは、[0.0, 1.0)という関数の契約に違反します。

このコミットでは、この問題を解決するために、計算結果f1.0と厳密に等しい場合に、f0に再マッピングするというシンプルな条件分岐が追加されました。

	f := float64(r.Int63()) / (1 << 63)
	if f == 1 {
		f = 0
	}
	return f

この修正の背後にある考え方は、1.0が返されるのはバグであり、そのバグを修正するために乱数ストリーム全体を変更するのは不適切であるというものです。1.0が返される確率は非常に低い(1/2^53)ため、この再マッピングが乱数ストリームの統計的特性に与える影響はごくわずかです。また、コミットメッセージで述べられているように、0が乱数ストリーム中で過少に表現されていたという観察に基づき、1.00にマッピングすることで、ある程度のバランスが回復されるとされています。

Float32()の修正ロジック

Float32()関数は、Float64()を呼び出してfloat64の乱数を生成し、それをfloat32にキャストするという実装になっています。

	f := float32(r.Float64())
	if f == 1 {
		f = 0
	}
	return f

ここでも同様に、float32へのキャスト後に結果が1.0になった場合に0に再マッピングするロジックが追加されています。Float64()の修正が適用されているため、Float32()1.0を返す可能性はさらに低くなりますが、念のためこのチェックが追加されています。

回帰テストの重要性

regress_test.goの追加は、このコミットの最も重要な側面の一つです。このテストは、math/randパッケージの乱数生成関数の出力が、特定のシード値(0)に対して常に同じであることを保証します。

// Test that random number sequences generated by a specific seed
// do not change from version to version.
//
// Do NOT make changes to the golden outputs. If bugs need to be fixed
// in the underlying code, find ways to fix them that do not affect the
// outputs.

このコメントは、Goチームが乱数ストリームの安定性を非常に重視していることを明確に示しています。将来的にmath/randパッケージの内部実装が変更された場合でも、この回帰テストが失敗することで、乱数ストリームの意図しない変更が即座に検出されるようになります。これにより、Goのバージョンアップが既存のプログラムに与える影響を最小限に抑え、ライブラリの互換性と信頼性が維持されます。

関連リンク

  • Go Issue #6721: このコミットで修正された問題の一つですが、公開されているGoのIssueトラッカーでは直接的な情報が見つかりませんでした。コミットメッセージに「(again)」とあることから、過去に同様の問題が報告され、再発した可能性が示唆されます。
  • Go Issue #8013: math/rand: Read should return consistent results, regardless of buffer size。このIssueはmath/randパッケージの再現性と安定性に関する広範な議論の一部であり、このコミットの精神と一致しています。
  • Go CL 95460049: このコミットに対応するGerritのChange List。
  • Go CL 22730043: このコミットで言及されている、以前の1.0バグを修正したが乱数ストリームを変更してしまったCL。このCLの直接的な公開情報は見つかりませんでしたが、CL 95460049のレビューコメントでその内容が説明されています。

参考にした情報源リンク