[インデックス 139] ファイルの概要
このコミットは、Go言語のテストスイートにおける浮動小数点リテラルの比較テスト (float_lit.go) に存在するバグを修正するものです。具体的には、2つの浮動小数点数が「十分に近似している」と判断するための close 関数内の比較ロジックが誤っていた点を修正し、それに伴いテストの期待値が記述された golden.out ファイルから不要なバグ報告のコメントを削除しています。
コミット
fix bug in closeness for float literal test
SVN=121628
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3086910f179b5e9dcbd728117a1a6f8682cedf85
元コミット内容
float literal test の closeness におけるバグを修正。
変更の背景
Go言語の初期開発段階において、浮動小数点数のリテラル(コードに直接記述された数値)が正しく扱われ、期待される精度で比較できることを保証するためのテストが存在しました。このテストでは、2つの浮動小数点数が「非常に近い」と見なせるかどうかを判定する close というヘルパー関数が使用されていました。
しかし、この close 関数内の比較ロジックに誤りがあり、特定の条件下で本来「近い」と判定されるべき浮動小数点数が「近くない」と誤判定されるバグが存在していました。このバグは、test/golden.out ファイルに「BUG: known to fail incorrectly」というコメントが残されていることからも、開発者が認識していた既知の問題であったことが伺えます。
このコミットは、その誤った比較ロジックを修正し、浮動小数点数の近似比較が正しく機能するようにすることで、テストが期待通りにパスするようにすることを目的としています。
前提知識の解説
浮動小数点数と精度
コンピュータにおける浮動小数点数(float や double など)は、実数を近似的に表現するためのものです。これは、有限のビット数で無限の実数を表現するため、常に誤差を伴います。この誤差のため、0.1 + 0.2 が厳密に 0.3 にならないなど、直感に反する結果になることがあります。
浮動小数点数の比較(イプシロン比較)
浮動小数点数の特性上、a == b のように厳密な等価比較を行うことはほとんどの場合で推奨されません。代わりに、2つの浮動小数点数 a と b が「十分に近似している」かどうかを判定するために、「イプシロン(epsilon)比較」と呼ばれる手法が用いられます。
イプシロン比較では、2つの数値の絶対差が、非常に小さな正の数(イプシロン、ε)よりも小さい場合に、それらの数値が近いと見なします。
|a - b| < ε
ただし、この単純なイプシロン比較では、数値の大きさに応じて相対的な誤差の許容範囲が変わるという問題があります。例えば、0.0000001 の差は 1.0 と 1.0000001 の間では非常に小さいですが、0.0000001 と 0.0000002 の間では非常に大きな差となります。
より堅牢な比較のためには、相対誤差を考慮したイプシロン比較が用いられることがあります。これは、差を一方の数値(または両方の数値の最大値)で割って相対的な差を評価するものです。
golden.out ファイル
golden.out のような「ゴールデンファイル」は、ソフトウェアテストにおいて、プログラムの出力が期待される正確な出力と一致することを検証するために使用される一般的なパターンです。テスト実行時に生成された出力が、事前に用意された「ゴールデン」または「参照」ファイルの内容とバイト単位で比較されます。もし両者が一致しない場合、テストは失敗と見なされます。
このコミットでは、golden.out から「BUG: known to fail incorrectly」というコメントが削除されています。これは、float_lit.go のテストが以前は既知のバグのために誤って失敗していたが、今回の修正によってそのバグが解消され、テストが正しくパスするようになったことを示唆しています。
技術的詳細
このコミットの核心は、test/float_lit.go 内の close 関数のロジック変更です。
元のコード:
func close(a, b double) bool {
e := a - b;
if e < 0 {
e = -e;
}
d := a;
if d < 0 {
d = -d;
}
if b > d {
d = b;
}
if d < 0 { // should not happen
d = -d;
}
if d == 0 { // a and b are both zero
return true;
}
if e*1.0e-14 < d { // ここが変更点
return true;
}
return false;
}
変更後のコード:
func close(a, b double) bool {
e := a - b;
if e < 0 {
e = -e;
}
d := a;
if d < 0 {
d = -d;
}
if b > d {
d = b;
}
if d < 0 { // should not happen
d = -d;
}
if d == 0 { // a and b are both zero
return true;
}
if e*1.0e-14 > d { // ここが変更点
return true;
}
return false;
}
変更点は if e*1.0e-14 < d が if e*1.0e-14 > d になったことです。
この close 関数は、2つの浮動小数点数 a と b が「近い」かどうかを判定しています。
eはaとbの絶対差|a - b|を表します。dはaとbの絶対値の大きい方max(|a|, |b|)を表します。これは、相対誤差を計算する際の基準値となります。1.0e-14は、許容される相対誤差の閾値(イプシロン)です。
元の if e*1.0e-14 < d という条件は、e < d / 1.0e-14 と等価です。これは、「絶対差 e が、基準値 d を非常に大きな数で割った値よりも小さい場合」に true を返していました。しかし、これは「差が非常に小さい場合に真」という意図とは逆のロジックになっていました。
正しいイプシロン比較の考え方では、e(絶対差)が d * 許容誤差 よりも小さい場合に「近い」と判断します。つまり、e < d * 1.0e-14 が正しい条件です。
コミットで変更された if e*1.0e-14 > d は、e > d / 1.0e-14 と等価です。これは、元の条件 e < d / 1.0e-14 を反転させたものです。
なぜ e*1.0e-14 > d が正しいのか?
このコードの意図は、e (絶対差) が d (基準値) に比べて十分に小さいかどうかを判定することです。
e / d < 1.0e-14 という条件が、相対誤差が 1.0e-14 未満である場合に true を返す、という一般的なイプシロン比較の形式です。
しかし、このコードでは e * 1.0e-14 と d を比較しています。
もし e が d に比べて非常に小さい場合、e * 1.0e-14 は d よりもさらに小さくなるはずです。
例えば、e = 1.0e-15, d = 1.0 の場合、e * 1.0e-14 = 1.0e-29 となり、d (1.0) よりもはるかに小さいです。
この場合、e * 1.0e-14 < d は true となります。
しかし、この close 関数のロジックは、e*1.0e-14 と d の関係を逆転させています。
元のコード if e*1.0e-14 < d は、e が d に対して非常に小さい場合に true を返していました。これは一見正しく見えますが、この関数が「近い」を判定する関数であるとすると、e が小さいほど true を返すのが自然です。
このコミットの変更は、e*1.0e-14 > d になっています。これは、e が d に対して十分に大きくない場合に true を返す、というロジックに変わったことを意味します。
つまり、e が d に比べてある閾値を超えて大きい場合に false を返し、そうでない場合に true を返す、という意図だったと考えられます。
この close 関数の実装は、一般的なイプシロン比較とは少し異なる形式を取っていますが、変更によって「差 e が、基準値 d に対して相対的に大きすぎる場合に false を返す」という意図が明確になったと解釈できます。
test/golden.out からの行削除は、この float_lit.go テストが以前はバグのために誤って失敗していたことを示すコメントが不要になったことを意味します。修正が適用されたことで、テストは期待通りにパスするようになったため、このコメントは削除されました。
コアとなるコードの変更箇所
diff --git a/test/float_lit.go b/test/float_lit.go
index c45de9b4c1..a7ef12390a 100644
--- a/test/float_lit.go
+++ b/test/float_lit.go
@@ -23,7 +23,7 @@ close(a, b double) bool
if e < 0 {
e = -e;
}
- if e*1.0e-14 < d {
+ if e*1.0e-14 > d {
return true;
}
return false;
diff --git a/test/golden.out b/test/golden.out
index 68be563f43..ef00c14da4 100644
--- a/test/golden.out
+++ b/test/golden.out
@@ -2,8 +2,6 @@
=========== ./char_lit.go
=========== ./float_lit.go
-+10. is printfloat should be printfloat
-BUG: known to fail incorrectly
=========== ./for.go
コアとなるコードの解説
test/float_lit.go 内の close 関数は、2つの double 型の浮動小数点数 a と b が「十分に近似している」かを判定します。
-
e := a - b; if e < 0 { e = -e; }eはaとbの差の絶対値、すなわち|a - b|を計算します。これは2つの数値間の絶対的な距離を示します。
-
d := a; if d < 0 { d = -d; } if b > d { d = b; } if d < 0 { d = -d; }dはaとbの絶対値のうち大きい方、すなわちmax(|a|, |b|)を計算します。これは、相対誤差を評価する際の基準となる値です。d < 0のチェックは、double型が負の値を取りうるため、絶対値を取る際に考慮されていますが、max(|a|, |b|)の結果が負になることは通常ありません。
-
if d == 0 { return true; }- もし
dが0であれば(つまりaとbの両方が0であれば)、それらは等しいと見なしtrueを返します。これは、0での除算を避けるための特殊なケース処理です。
- もし
-
if e*1.0e-14 > d { return true; }(変更後のロジック)- これがこのコミットの主要な変更点です。
1.0e-14は非常に小さな数値であり、相対誤差の許容範囲(イプシロン)として機能します。- この条件は
e > d / 1.0e-14と等価です。 - このロジックは、「絶対差
eが、基準値dを1.0e-14で割った値(つまりdの10^14倍)よりも大きい場合」にtrueを返します。 - これは、
eがdに対して相対的に大きすぎる場合にtrueを返す、という逆の判定ロジックになっています。 - この
close関数は、おそらく「aとbが近いではない」場合にfalseを返すことを意図しており、この条件がtrueになる場合は「近い」と判断しているようです。 - つまり、
eがdの10^14倍よりも大きい場合、それは「近い」と判断される、という一見すると直感に反するロジックです。しかし、これはeがdに対して相対的に小さすぎる場合にfalseを返す、という元のバグを修正するために、比較演算子を反転させた結果です。 - この変更により、
eがdに対して相対的にある閾値を超えて大きい場合にfalseを返し、そうでない場合にtrueを返す、という「近い」の判定が正しく行われるようになったと考えられます。
-
return false;- 上記の条件が満たされない場合、つまり
e*1.0e-14がd以下である場合、falseを返します。
- 上記の条件が満たされない場合、つまり
test/golden.out からの削除された行は、float_lit.go テストが以前は既知のバグのために誤って失敗していたことを示すコメントでした。このコミットでバグが修正されたため、これらのコメントは不要となり削除されました。
関連リンク
- Go言語の初期開発に関する情報: https://go.dev/doc/history
- 浮動小数点数の比較に関する一般的な情報: https://floating-point-gui.de/errors/comparison/
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/139.txt - GitHubコミットページ: https://github.com/golang/go/commit/3086910f179b5e9dcbd728117a1a6f8682cedf85
- 浮動小数点数の比較に関する一般的な知識 (イプシロン比較など)
- テストにおけるゴールデンファイルの概念