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

[インデックス 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 testcloseness におけるバグを修正。

変更の背景

Go言語の初期開発段階において、浮動小数点数のリテラル(コードに直接記述された数値)が正しく扱われ、期待される精度で比較できることを保証するためのテストが存在しました。このテストでは、2つの浮動小数点数が「非常に近い」と見なせるかどうかを判定する close というヘルパー関数が使用されていました。

しかし、この close 関数内の比較ロジックに誤りがあり、特定の条件下で本来「近い」と判定されるべき浮動小数点数が「近くない」と誤判定されるバグが存在していました。このバグは、test/golden.out ファイルに「BUG: known to fail incorrectly」というコメントが残されていることからも、開発者が認識していた既知の問題であったことが伺えます。

このコミットは、その誤った比較ロジックを修正し、浮動小数点数の近似比較が正しく機能するようにすることで、テストが期待通りにパスするようにすることを目的としています。

前提知識の解説

浮動小数点数と精度

コンピュータにおける浮動小数点数(floatdouble など)は、実数を近似的に表現するためのものです。これは、有限のビット数で無限の実数を表現するため、常に誤差を伴います。この誤差のため、0.1 + 0.2 が厳密に 0.3 にならないなど、直感に反する結果になることがあります。

浮動小数点数の比較(イプシロン比較)

浮動小数点数の特性上、a == b のように厳密な等価比較を行うことはほとんどの場合で推奨されません。代わりに、2つの浮動小数点数 ab が「十分に近似している」かどうかを判定するために、「イプシロン(epsilon)比較」と呼ばれる手法が用いられます。

イプシロン比較では、2つの数値の絶対差が、非常に小さな正の数(イプシロン、ε)よりも小さい場合に、それらの数値が近いと見なします。 |a - b| < ε

ただし、この単純なイプシロン比較では、数値の大きさに応じて相対的な誤差の許容範囲が変わるという問題があります。例えば、0.0000001 の差は 1.01.0000001 の間では非常に小さいですが、0.00000010.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 < dif e*1.0e-14 > d になったことです。

この close 関数は、2つの浮動小数点数 ab が「近い」かどうかを判定しています。

  • eab の絶対差 |a - b| を表します。
  • dab の絶対値の大きい方 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-14d を比較しています。 もし ed に比べて非常に小さい場合、e * 1.0e-14d よりもさらに小さくなるはずです。 例えば、e = 1.0e-15, d = 1.0 の場合、e * 1.0e-14 = 1.0e-29 となり、d (1.0) よりもはるかに小さいです。 この場合、e * 1.0e-14 < dtrue となります。

しかし、この close 関数のロジックは、e*1.0e-14d の関係を逆転させています。 元のコード if e*1.0e-14 < d は、ed に対して非常に小さい場合に true を返していました。これは一見正しく見えますが、この関数が「近い」を判定する関数であるとすると、e が小さいほど true を返すのが自然です。

このコミットの変更は、e*1.0e-14 > d になっています。これは、ed に対して十分に大きくない場合に true を返す、というロジックに変わったことを意味します。 つまり、ed に比べてある閾値を超えて大きい場合に 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 型の浮動小数点数 ab が「十分に近似している」かを判定します。

  1. e := a - b; if e < 0 { e = -e; }

    • eab の差の絶対値、すなわち |a - b| を計算します。これは2つの数値間の絶対的な距離を示します。
  2. d := a; if d < 0 { d = -d; } if b > d { d = b; } if d < 0 { d = -d; }

    • dab の絶対値のうち大きい方、すなわち max(|a|, |b|) を計算します。これは、相対誤差を評価する際の基準となる値です。d < 0 のチェックは、double 型が負の値を取りうるため、絶対値を取る際に考慮されていますが、max(|a|, |b|) の結果が負になることは通常ありません。
  3. if d == 0 { return true; }

    • もし d0 であれば(つまり ab の両方が 0 であれば)、それらは等しいと見なし true を返します。これは、0 での除算を避けるための特殊なケース処理です。
  4. if e*1.0e-14 > d { return true; } (変更後のロジック)

    • これがこのコミットの主要な変更点です。
    • 1.0e-14 は非常に小さな数値であり、相対誤差の許容範囲(イプシロン)として機能します。
    • この条件は e > d / 1.0e-14 と等価です。
    • このロジックは、「絶対差 e が、基準値 d1.0e-14 で割った値(つまり d10^14 倍)よりも大きい場合」に true を返します。
    • これは、ed に対して相対的に大きすぎる場合に true を返す、という逆の判定ロジックになっています。
    • この close 関数は、おそらく「ab が近いではない」場合に false を返すことを意図しており、この条件が true になる場合は「近い」と判断しているようです。
    • つまり、ed10^14 倍よりも大きい場合、それは「近い」と判断される、という一見すると直感に反するロジックです。しかし、これは ed に対して相対的に小さすぎる場合に false を返す、という元のバグを修正するために、比較演算子を反転させた結果です。
    • この変更により、ed に対して相対的にある閾値を超えて大きい場合に false を返し、そうでない場合に true を返す、という「近い」の判定が正しく行われるようになったと考えられます。
  5. return false;

    • 上記の条件が満たされない場合、つまり e*1.0e-14d 以下である場合、false を返します。

test/golden.out からの削除された行は、float_lit.go テストが以前は既知のバグのために誤って失敗していたことを示すコメントでした。このコミットでバグが修正されたため、これらのコメントは不要となり削除されました。

関連リンク

参考にした情報源リンク