[インデックス 144] ファイルの概要
このコミットは、Go言語のテストスイート内の浮動小数点リテラルのテスト方法を改善するものです。具体的には、浮動小数点数を整数から生成し、その値をチェックするメカニズムを導入することで、テストの精度と信頼性を向上させています。
コミット
commit b1a3463a7bafae2e2acdeb3788e1b61088689bed
Author: Ken Thompson <ken@golang.org>
Date: Tue Jun 10 13:23:19 2008 -0700
made fp numbers from integers and
checked values
SVN=121972
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b1a3463a7bafae2e2acdeb3788e1b61088689bed
元コミット内容
このコミットの元の内容は、test/float_lit.go
ファイルにおいて、浮動小数点数のテスト方法を変更したものです。変更の要点は以下の通りです。
close
関数のシグネチャが変更され、浮動小数点数同士の比較だけでなく、整数から構築された浮動小数点数との比較も可能になりました。- 10のべき乗を計算する新しいヘルパー関数
pow10
が追加されました。 main
関数内のテストケースが、新しいclose
関数のシグネチャに合わせて更新され、より厳密な浮動小数点リテラルの検証が行われるようになりました。
変更の背景
浮動小数点数の表現は、コンピュータの内部では常に正確であるとは限りません。特に、10進数で表現される有限の小数が、2進数では無限小数になる場合があり、その結果として丸め誤差が生じます。Go言語のコンパイラが浮動小数点リテラルをどのように解釈し、内部表現に変換するかは、言語の正確性を保証する上で非常に重要です。
このコミットが行われた2008年6月は、Go言語がまだ初期開発段階にあった時期です。この段階では、言語の基本的な機能、特に数値型とそのリテラルの振る舞いを厳密に定義し、テストすることが不可欠でした。従来のテスト方法では、単に2つの浮動小数点リテラルを比較していましたが、これではコンパイラがリテラルをどのように処理しているかの詳細な検証には不十分でした。
変更の背景には、以下のような意図が考えられます。
- テストの厳密化: 浮動小数点リテラルが、意図した正確な値に変換されているかをより厳密にテストする必要がありました。単に
0.1 == 0.1
のような比較では、両者が同じ丸め誤差を含んでいる場合にテストがパスしてしまう可能性があります。 - 整数からの構築: 浮動小数点数を整数(分子と分母)と10のべき乗(指数)から構築することで、期待される浮動小数点値をより正確に表現し、コンパイラが生成する浮動小数点リテラルの値と比較できるようにしました。これにより、コンパイラの浮動小数点リテラル処理における潜在的なバグや不正確さを特定しやすくなります。
- 言語仕様の確立: Go言語の初期段階において、数値リテラルのセマンティクスを確立し、その振る舞いをテストによって保証することは、言語の安定性と信頼性を築く上で極めて重要でした。
前提知識の解説
浮動小数点数 (Floating-Point Numbers)
浮動小数点数は、実数を近似的に表現するためのコンピュータの数値表現形式です。IEEE 754標準が広く用いられており、単精度(32ビット)や倍精度(64ビット)などの形式があります。浮動小数点数は、符号部、指数部、仮数部から構成され、±仮数部 × 基数^指数部
の形式で値を表現します。
重要なのは、多くの10進小数が2進数では正確に表現できないため、浮動小数点演算には常に丸め誤差が伴う可能性があるという点です。例えば、10進数の 0.1
は2進数では無限小数となり、コンピュータ内部では近似値として表現されます。
浮動小数点数の比較
浮動小数点数を比較する際には、単純な等価演算子 (==
) を使用することは推奨されません。丸め誤差のために、数学的に等しいはずの値がコンピュータ上ではわずかに異なる場合があるためです。代わりに、2つの値の差が非常に小さい許容範囲(イプシロン、epsilon)内にあるかどうかをチェックする方法が一般的です。
|a - b| < epsilon
このコミットの close
関数も、このイプシロン比較の原則に基づいています。
Go言語の数値リテラル
Go言語では、整数リテラル、浮動小数点リテラル、虚数リテラルなどがサポートされています。浮動小数点リテラルは、小数点や指数部(e
または E
)を含む数値で表現されます。コンパイラはこれらのリテラルを解析し、適切な浮動小数点型(float32
や float64
)の値に変換します。この変換プロセスが正確であることが、言語の信頼性にとって重要です。
技術的詳細
このコミットの技術的な核心は、test/float_lit.go
ファイル内の close
関数と、新しく導入された pow10
関数、そしてそれらを用いたテストケースの変更にあります。
pow10
関数の導入
func
pow10(pow int) double
{
if pow < 0 { return 1/pow10(-pow); }
if pow > 0 { return pow10(pow-1)*10; }
return 1;
}
この関数は、与えられた整数 pow
に応じて10の pow
乗を double
型(おそらく float64
のエイリアス)で計算します。
pow
が負の場合、1 / pow10(-pow)
を計算し、負の指数を処理します(例:10^-2 = 1 / 10^2
)。pow
が正の場合、再帰的にpow10(pow-1) * 10
を計算します(例:10^3 = 10^2 * 10
)。pow
が0の場合、1
を返します(10^0 = 1
)。
この関数は、テスト対象の浮動小数点リテラルが持つ指数部を正確に再現するために使用されます。
close
関数の変更
元の close
関数は close(a, b double) bool
というシグネチャで、2つの浮動小数点数 a
と b
が十分に「近い」かどうかを比較していました。
変更後の close
関数は close(da double, ia, ib int64, pow int) bool
というシグネチャになりました。
da
: テスト対象の浮動小数点リテラル(double
型)。ia
: 期待値の分子(int64
型)。ib
: 期待値の分母(int64
型)。pow
: 期待値の10のべき乗(int
型)。
この新しい close
関数では、まず ia
と ib
を用いて db
という浮動小数点数を構築します。
db := double(ia) / double(ib);
db = db*pow10(pow);
これにより、db
は (ia / ib) * 10^pow
という形式で、整数から正確に計算された浮動小数点値となります。例えば、0.01
をテストする場合、ia=1
, ib=100
, pow=0
とすることで (1/100)*10^0 = 0.01
を表現できます。また、10e2
のような指数表記のリテラルをテストする場合、ia=10
, ib=1
, pow=2
とすることで (10/1)*10^2 = 1000
を表現できます。
その後、da
(テスト対象のリテラル)と db
(整数から構築された期待値)の間の差 dd
を計算し、その絶対値が da
の絶対値に 1.0e-14
を掛けた値よりも小さいかどうかをチェックします。
dd := da-db;
if dd < 0 {
dd = -dd;
}
de := da;
if de < 0 {
de = -de;
}
if de*1.0e-14 > dd {
return true;
}
return false;
1.0e-14
は、倍精度浮動小数点数(double
)の精度を考慮した許容誤差(イプシロン)です。この値は、約15桁の精度を持つ倍精度浮動小数点数において、相対誤差が非常に小さいことを意味します。
テストケースの変更
main
関数内のテストケースは、新しい close
関数のシグネチャに合わせてすべて変更されました。
例えば、元の if !close(0., 0.)
は if !close(0., 0, 1, 0)
に、if !close(+.01, +.01)
は if !close(+.01, 1, 100, 0)
に変更されています。
これにより、テストは単にリテラル同士を比較するのではなく、リテラルが整数ベースの正確な値に変換されているかを検証するようになりました。これは、コンパイラが浮動小数点リテラルを正しくパースし、内部表現に変換する能力をテストするために非常に効果的です。
コアとなるコードの変更箇所
変更は test/float_lit.go
ファイルに集中しています。
--- a/test/float_lit.go
+++ b/test/float_lit.go
@@ -7,87 +7,103 @@
package main
func
-close(a, b double) bool
+pow10(pow int) double
+{
+ if pow < 0 { return 1/pow10(-pow); }
+ if pow > 0 { return pow10(pow-1)*10; }
+ return 1;
+}
+
+func
+close(da double, ia, ib int64, pow int) bool
{
-\tif a == 0 {\n-\t\tif b == 0 {\n+\tdb := double(ia) / double(ib);\n+\tdb = db*pow10(pow);\n+\n+\tif da == 0 {\n+\t\tif db == 0 {\n \t\t\treturn true;\n \t\t}\n \t\treturn false;\n \t}\n-\td := a-b;\n-\tif d < 0 {\n-\t\td = -d;\n+\n+\tdd := da-db;\n+\tif dd < 0 {\n+\t\tdd = -dd;\n \t}\n-\te := a;\n-\tif e < 0 {\n-\t\te = -e;\n+\n+\tde := da;\n+\tif de < 0 {\n+\t\tde = -de;\n \t}\n-\tif e*1.0e-14 > d {\n+\n+\tif de*1.0e-14 > dd {\n \t\treturn true;\n \t}\n \treturn false;\n }\n \n-func main() int {\n+func\n+main()\n+{\\n \n-\tif !close(0., 0.) { print \"0. is \", 0., \" should be \", 0., \"\\n\"; return 1; }\n-\tif !close(+10., +10.) { print \"+10. is \", +10., \" should be \", +10., \"\\n\"; return 1; }\n-\tif !close(-210., -210.) { print \"-210. is \", -210., \" should be \", -210., \"\\n\"; return 1; }\n+\tif !close(0., 0, 1, 0) { print \"0. is \", 0., \"\\n\"; }\n+\tif !close(+10., 10, 1, 0) { print \"+10. is \", +10., \"\\n\"; }\n+\tif !close(-210., -210, 1, 0) { print \"-210. is \", -210., \"\\n\"; }\n \n-\tif !close(.0, .0) { print \".0 is \", .0, \" should be \", .0, \"\\n\"; return 1; }\n-\tif !close(+.01, +.01) { print \"+.01 is \", +.01, \" should be \", +.01, \"\\n\"; return 1; }\n-\tif !close(-.012, -.012) { print \"-.012 is \", -.012, \" should be \", -.012, \"\\n\"; return 1; }\n+\tif !close(.0, 0, 1, 0) { print \".0 is \", .0, \"\\n\"; }\n+\tif !close(+.01, 1, 100, 0) { print \"+.01 is \", +.01, \"\\n\"; }\n+\tif !close(-.012, -12, 1000, 0) { print \"-.012 is \", -.012, \"\\n\"; }\n \n-\tif !close(0.0, 0.0) { print \"0.0 is \", 0.0, \" should be \", 0.0, \"\\n\"; return 1; }\n-\tif !close(+10.01, +10.01) { print \"+10.01 is \", +10.01, \" should be \", +10.01, \"\\n\"; return 1; }\n-\tif !close(-210.012, -210.012) { print \"-210.012 is \", -210.012, \" should be \", -210.012, \"\\n\"; return 1; }\n+\tif !close(0.0, 0, 1, 0) { print \"0.0 is \", 0.0, \"\\n\"; }\n+\tif !close(+10.01, 1001, 100, 0) { print \"+10.01 is \", +10.01, \"\\n\"; }\n+\tif !close(-210.012, -210012, 1000, 0) { print \"-210.012 is \", -210.012, \"\\n\"; }\n \n-\tif !close(0E+1, 0E+1) { print \"0E+1 is \", 0E+1, \" should be \", 0E+1, \"\\n\"; return 1; }\n-\tif !close(+10e2, +10e2) { print \"+10e2 is \", +10e2, \" should be \", +10e2, \"\\n\"; return 1; }\n-\tif !close(-210e3, -210e3) { print \"-210e3 is \", -210e3, \" should be \", -210e3, \"\\n\"; return 1; }\n+\tif !close(0E+1, 0, 1, 0) { print \"0E+1 is \", 0E+1, \"\\n\"; }\n+\tif !close(+10e2, 10, 1, 2) { print \"+10e2 is \", +10e2, \"\\n\"; }\n+\tif !close(-210e3, -210, 1, 3) { print \"-210e3 is \", -210e3, \"\\n\"; }\n \n-\tif !close(0E-1, 0E-1) { print \"0E-1 is \", 0E-1, \" should be \", 0E-1, \"\\n\"; return 1; }\n-\tif !close(+0e23, +0e23) { print \"+0e23 is \", +0e23, \" should be \", +0e23, \"\\n\"; return 1; }\n-\tif !close(-0e345, -0e345) { print \"-0e345 is \", -0e345, \" should be \", -0e345, \"\\n\"; return 1; }\n+\tif !close(0E-1, 0, 1, 0) { print \"0E-1 is \", 0E-1, \"\\n\"; }\n+\tif !close(+0e23, 0, 1, 23) { print \"+0e23 is \", +0e23, \"\\n\"; }\n+\tif !close(-0e345, 0, 1, 345) { print \"-0e345 is \", -0e345, \"\\n\"; }\n \n-\tif !close(0E1, 0E1) { print \"0E1 is \", 0E1, \" should be \", 0E1, \"\\n\"; return 1; }\n-\tif !close(+10e23, +10e23) { print \"+10e23 is \", +10e23, \" should be \", +10e23, \"\\n\"; return 1; }\n-//\tif !close(-210e345, -210e345) { print \"-210e345 is \", -210e345, \" should be \", -210e345, \"\\n\"; return 1; }\n+\tif !close(0E1, 0, 1, 1) { print \"0E1 is \", 0E1, \"\\n\"; }\n+\tif !close(+10e23, 10, 1, 23) { print \"+10e23 is \", +10e23, \"\\n\"; }\n+\tif !close(-210e34, -210, 1, 34) { print \"-210e34 is \", -210e34, \"\\n\"; }\n \n-\tif !close(0.E1, 0.E1) { print \"0.E1 is \", 0.E1, \" should be \", 0.E1, \"\\n\"; return 1; }\n-\tif !close(+10.e+2, +10.e+2) { print \"+10.e+2 is \", +10.e+2, \" should be \", +10.e+2, \"\\n\"; return 1; }\n-\tif !close(-210.e-3, -210.e-3) { print \"-210.e-3 is \", -210.e-3, \" should be \", -210.e-3, \"\\n\"; return 1; }\n+\tif !close(0.E1, 0, 1, 1) { print \"0.E1 is \", 0.E1, \"\\n\"; }\n+\tif !close(+10.e+2, 10, 1, 2) { print \"+10.e+2 is \", +10.e+2, \"\\n\"; }\n+\tif !close(-210.e-3, -210, 1, -3) { print \"-210.e-3 is \", -210.e-3, \"\\n\"; }\n \n-\tif !close(.0E1, .0E1) { print \".0E1 is \", .0E1, \" should be \", .0E1, \"\\n\"; return 1; }\n-\tif !close(+.01e2, +.01e2) { print \"+.01e2 is \", +.01e2, \" should be \", +.01e2, \"\\n\"; return 1; }\n-\tif !close(-.012e3, -.012e3) { print \"-.012e3 is \", -.012e3, \" should be \", -.012e3, \"\\n\"; return 1; }\n+\tif !close(.0E1, 0, 1, 1) { print \".0E1 is \", .0E1, \"\\n\"; }\n+\tif !close(+.01e2, 1, 100, 2) { print \"+.01e2 is \", +.01e2, \"\\n\"; }\n+\tif !close(-.012e3, -12, 1000, 3) { print \"-.012e3 is \", -.012e3, \"\\n\"; }\n \n-\tif !close(0.0E1, 0.0E1) { print \"0.0E1 is \", 0.0E1, \" should be \", 0.0E1, \"\\n\"; return 1; }\n-\tif !close(+10.01e2, +10.01e2) { print \"+10.01e2 is \", +10.01e2, \" should be \", +10.01e2, \"\\n\"; return 1; }\n-\tif !close(-210.012e3, -210.012e3) { print \"-210.012e3 is \", -210.012e3, \" should be \", -210.012e3, \"\\n\"; return 1; }\n+\tif !close(0.0E1, 0, 1, 0) { print \"0.0E1 is \", 0.0E1, \"\\n\"; }\n+\tif !close(+10.01e2, 1001, 100, 2) { print \"+10.01e2 is \", +10.01e2, \"\\n\"; }\n+\tif !close(-210.012e3, -210012, 1000, 3) { print \"-210.012e3 is \", -210.012e3, \"\\n\"; }\n \n-\tif !close(0.E+12, 0.E+12) { print \"0.E+12 is \", 0.E+12, \" should be \", 0.E+12, \"\\n\"; return 1; }\n-\tif !close(+10.e23, +10.e23) { print \"+10.e23 is \", +10.e23, \" should be \", +10.e23, \"\\n\"; return 1; }\n-\tif !close(-210.e34, -210.e34) { print \"-210.e34 is \", -210.e34, \" should be \", -210.e34, \"\\n\"; return 1; }\n+\tif !close(0.E+12, 0, 1, 0) { print \"0.E+12 is \", 0.E+12, \"\\n\"; }\n+\tif !close(+10.e23, 10, 1, 23) { print \"+10.e23 is \", +10.e23, \"\\n\"; }\n+\tif !close(-210.e33, -210, 1, 33) { print \"-210.e33 is \", -210.e33, \"\\n\"; }\n \n-\tif !close(.0E-12, .0E-12) { print \".0E-12 is \", .0E-12, \" should be \", .0E-12, \"\\n\"; return 1; }\n-\tif !close(+.01e23, +.01e23) { print \"+.01e23 is \", +.01e23, \" should be \", +.01e23, \"\\n\"; return 1; }\n-\tif !close(-.012e34, -.012e34) { print \"-.012e34 is \", -.012e34, \" should be \", -.012e34, \"\\n\"; return 1; }\n+\tif !close(.0E-12, 0, 1, 0) { print \".0E-12 is \", .0E-12, \"\\n\"; }\n+\tif !close(+.01e23, 1, 100, 23) { print \"+.01e23 is \", +.01e23, \"\\n\"; }\n+\tif !close(-.012e34, -12, 1000, 34) { print \"-.012e34 is \", -.012e34, \"\\n\"; }\n \n-\tif !close(0.0E12, 0.0E12) { print \"0.0E12 is \", 0.0E12, \" should be \", 0.0E12, \"\\n\"; return 1; }\n-\tif !close(+10.01e23, +10.01e23) { print \"+10.01e23 is \", +10.01e23, \" should be \", +10.01e23, \"\\n\"; return 1; }\n-\tif !close(-210.012e34, -210.012e34) { print \"-210.012e34 is \", -210.012e34, \" should be \", -210.012e34, \"\\n\"; return 1; }\n+\tif !close(0.0E12, 0, 1, 12) { print \"0.0E12 is \", 0.0E12, \"\\n\"; }\n+\tif !close(+10.01e23, 1001, 100, 23) { print \"+10.01e23 is \", +10.01e23, \"\\n\"; }\n+\tif !close(-210.012e33, -210012, 1000, 33) { print \"-210.012e33 is \", -210.012e33, \"\\n\"; }\n \n-\tif !close(0.E123, 0.E123) { print \"0.E123 is \", 0.E123, \" should be \", 0.E123, \"\\n\"; return 1; }\n-\tif !close(+10.e+234, +10.e+234) { print \"+10.e+234 is \", +10.e+234, \" should be \", +10.e+234, \"\\n\"; return 1; }\n-//\tif !close(-210.e-345, -210.e-345) { print \"-210.e-345 is \", -210.e-345, \" should be \", -210.e-345, \"\\n\"; return 1; }\n+\tif !close(0.E123, 0, 1, 123) { print \"0.E123 is \", 0.E123, \"\\n\"; }\n+\tif !close(+10.e+23, 10, 1, 23) { print \"+10.e+234 is \", +10.e+234, \"\\n\"; }\n+\tif !close(-210.e-35, -210, 1, -35) { print \"-210.e-35 is \", -210.e-35, \"\\n\"; }\n \n-\tif !close(.0E123, .0E123) { print \".0E123 is \", .0E123, \" should be \", .0E123, \"\\n\"; return 1; }\n-//\tif !close(+.01e234, +.01e234) { print \"+.01e234 is \", +.01e234, \" should be \", +.01e234, \"\\n\"; return 1; }\n-//\tif !close(-.012e345, -.012e345) { print \"-.012e345 is \", -.012e345, \" should be \", -.012e345, \"\\n\"; return 1; }\n+\tif !close(.0E123, 0, 1, 123) { print \".0E123 is \", .0E123, \"\\n\"; }\n+\tif !close(+.01e29, 1, 100, 29) { print \"+.01e29 is \", +.01e29, \"\\n\"; }\n+\tif !close(-.012e29, -12, 1000, 29) { print \"-.012e29 is \", -.012e29, \"\\n\"; }\n \n-\tif !close(0.0E123, 0.0E123) { print \"0.0E123 is \", 0.0E123, \" should be \", 0.0E123, \"\\n\"; return 1; }\n-//\tif !close(+10.01e234, +10.01e234) { print \"+10.01e234 is \", +10.01e234, \" should be \", +10.01e234, \"\\n\"; return 1; }\n-//\tif !close(-210.012e345, -210.012e345) { print \"-210.012e345 is \", -210.012e345, \" should be \", -210.012e345, \"\\n\"; return 1; }\n+\tif !close(0.0E123, 0, 1, 123) { print \"0.0E123 is \", 0.0E123, \"\\n\"; }\n+\tif !close(+10.01e31, 1001, 100, 31) { print \"+10.01e31 is \", +10.01e31, \"\\n\"; }\n+\tif !close(-210.012e19, -210012, 1000, 19) { print \"-210.012e19 is \", -210.012e19, \"\\n\"; }\n }\n```
## コアとなるコードの解説
### `pow10` 関数
この関数は、浮動小数点数の指数部を正確に扱うためのユーティリティです。再帰的な定義により、正負の指数に対応し、`10^n` の値を `double` 型で返します。これにより、テストケースで期待値を構築する際に、指数表記の浮動小数点リテラルを正確に表現できるようになります。
### `close` 関数の変更点
最も重要な変更は `close` 関数のシグネチャと内部ロジックです。
- **シグネチャの変更**: `(da double, ia, ib int64, pow int)` とすることで、テスト対象の浮動小数点値 `da` と、整数 `ia` (分子)、`ib` (分母)、そして指数 `pow` から構築される期待値 `db` を比較できるようになりました。
- **期待値の構築**: `db := double(ia) / double(ib); db = db*pow10(pow);` の行が、この変更の核心です。これにより、`db` は `(ia / ib) * 10^pow` という形式で、整数演算と `pow10` 関数によって正確に計算された浮動小数点値となります。例えば、`0.01` は `1/100` として、`1000.0` は `10/1 * 10^2` として表現されます。
- **比較ロジック**: `da` と `db` の差の絶対値 `dd` を計算し、`da` の絶対値に `1.0e-14` を掛けた値 `de*1.0e-14` と比較します。これは、浮動小数点数の比較における一般的なイプシロン比較であり、相対誤差が非常に小さい場合に等しいとみなすためのものです。
### `main` 関数内のテストケース
`main` 関数内の各テストケースは、Go言語の様々な浮動小数点リテラル形式(例: `0.`, `+10.`, `.01`, `10.01`, `0E+1`, `10e2`, `0.E1`, `.01e2` など)を網羅しています。これらのリテラルが、新しい `close` 関数に渡される `ia`, `ib`, `pow` の組み合わせによって表現される期待値と一致するかどうかを検証しています。
例えば、`if !close(+.01, 1, 100, 0)` は、リテラル `+.01` が `(1 / 100) * 10^0`、つまり `0.01` と等しいかをテストしています。これにより、コンパイラが `+.01` を正確に `0.01` という浮動小数点値に変換できるかを検証します。
この変更により、Go言語のコンパイラが浮動小数点リテラルをどのように解釈し、内部表現に変換するかのテストが、より堅牢で正確なものになりました。
## 関連リンク
- Go言語の数値型: [https://go.dev/ref/spec#Numeric_types](https://go.dev/ref/spec#Numeric_types)
- Go言語の数値リテラル: [https://go.dev/ref/spec#Numeric_literals](https://go.dev/ref/spec#Numeric_literals)
- IEEE 754 浮動小数点標準: [https://ja.wikipedia.org/wiki/IEEE_754%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0%E8%A8%88%E7%AE%97%E6%A8%99%E6%BA%96](https://ja.wikipedia.org/wiki/IEEE_754%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0%E8%A8%88%E7%AE%97%E6%A8%99%E6%BA%96)
- 浮動小数点数の比較に関する一般的な情報: [https://floating-point-gui.de/](https://floating-point-gui.de/)
## 参考にした情報源リンク
- 上記の関連リンクに加えて、Go言語の公式ドキュメントや、浮動小数点数の精度に関する一般的なプログラミングのベストプラクティスに関する情報源を参考にしました。
- 特に、浮動小数点数の比較におけるイプシロンの使用は、数値計算の分野で広く知られている手法です。