[インデックス 19401] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における浮動小数点定数の変換と表示に関する2つの重要な問題を修正します。具体的には、float32
定数変換における二重丸め(double rounding)による不正確さの解消と、float64
の範囲を超える非常に大きな浮動小数点定数の表示方法の改善が目的です。
コミット
cmd/gc
: float32定数変換と大きな浮動小数点定数の表示を修正
float32
定数変換は、以前はfloat64
に丸めてからハードウェアを使用してfloat32
に丸めていました。
この変換の前に範囲チェックがあったにもかかわらず、二重丸めは不正確さを引き起こしました。
float64
への丸めがfloat32
の範囲からさらに値を遠ざけ、実際にはfloat32
に丸めることができないfloat64
値に到達する可能性がありました。
この場合、ハードウェアは0を返すようですが、これはおそらく未定義の動作です。
二重丸めは、特定の境界ケースで間違った値が使用される可能性も意味していました。
float64
への丸めをすでに自分たちで行っていたのと同様に、float32
への丸めも自分たちで行うようにしました。
これにより、変換が正確になり、範囲チェックと変換が一致するようになります。
最後に、非常に大きな(float64
よりも大きい)浮動小数点定数を、正確だが人間には読みにくい二進浮動小数点表記ではなく、十進浮動小数点表記で出力するコードを追加しました。
Fixes #8015.
LGTM=iant R=golang-codereviews, iant CC=golang-codereviews, r https://golang.org/cl/100580044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/60be4a245049218e3d56ce8d49d22f2847ebec3f
元コミット内容
上記の「コミット」セクションに記載されている内容が、このコミットの元の内容です。
変更の背景
このコミットは、Goコンパイラが浮動小数点定数を扱う際に直面していた2つの主要な問題に対処するために行われました。
-
float32
定数変換の不正確さ: 以前のGoコンパイラでは、ソースコード中の浮動小数点リテラルをfloat32
型に変換する際、まずその値をfloat64
(倍精度浮動小数点数)に丸め、その後、そのfloat64
値をハードウェアの浮動小数点ユニット(FPU)を使ってfloat32
(単精度浮動小数点数)に丸めるという二段階のプロセスを踏んでいました。この「二重丸め」は、特に境界値に近い場合や、float32
の表現範囲の限界に近い値において、予期せぬ不正確さを引き起こす可能性がありました。- 問題点:
float64
への最初の丸めが、float32
の正確な表現から値を遠ざけてしまうことがありました。その結果、float32
の範囲内にあるべき値が、float64
に丸められた後ではfloat32
で表現できない値になってしまい、ハードウェアが0を返すなどの未定義の動作を引き起こす可能性がありました。これは、コンパイラが定数を扱う際の予測可能性と正確性を損なうものでした。また、特定の境界ケースでは、誤った値が使用される原因にもなっていました。
- 問題点:
-
非常に大きな浮動小数点定数の表示問題: Goコンパイラは、内部的に任意精度浮動小数点数(
Mpflt
型)を使用して定数を扱います。しかし、これらの定数がfloat64
の表現範囲をはるかに超える非常に大きな値である場合、コンパイラはそれらを人間が読みにくい二進浮動小数点表記(例:1.234p+1000
のような形式)で出力していました。これはデバッグやエラーメッセージの可読性を著しく低下させていました。
これらの問題は、Go言語の数値計算の信頼性と開発者の利便性に影響を与えるため、修正が必要とされました。特に、float32
の二重丸め問題は、Go言語の厳密な型システムと数値の正確性への期待に反するものでした。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
-
浮動小数点数 (Floating-Point Numbers):
- コンピュータで実数を近似的に表現するための形式です。一般的に、符号部、仮数部(有効数字)、指数部で構成されます。
- IEEE 754 標準: 浮動小数点数の表現と演算に関する国際標準です。Go言語を含む多くのプログラミング言語やハードウェアで採用されています。
float32
(単精度浮動小数点数): 32ビットで表現され、約7桁の十進精度を持ちます。指数部の範囲が狭いため、表現できる数値の範囲がfloat64
よりもはるかに小さいです。float64
(倍精度浮動小数点数): 64ビットで表現され、約15-17桁の十進精度を持ちます。float32
よりも広い範囲と高い精度で数値を表現できます。
- 丸め誤差 (Rounding Error): 浮動小数点数は有限のビット数で表現されるため、正確に表現できない実数(例: 1/3)や、演算結果が表現可能な範囲を超える場合に、最も近い表現可能な値に丸められます。この丸めによって生じる誤差を丸め誤差と呼びます。
- 二重丸め (Double Rounding): ある精度(例:
float64
)に丸めた後、さらに低い精度(例:float32
)に丸めるプロセスです。この二段階の丸めが、単一の丸めでは発生しない追加の誤差や不正確さを引き起こすことがあります。理想的には、最終的な精度に直接丸めるべきです。
-
Goコンパイラ (
cmd/gc
):- Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。
- 定数伝播 (Constant Propagation): コンパイル時に、既知の定数値を計算し、その結果をコードに直接埋め込む最適化手法です。これにより、実行時の計算を減らし、パフォーマンスを向上させます。浮動小数点定数もこのプロセスで処理されます。
- 任意精度算術 (Arbitrary-Precision Arithmetic): コンパイラ内部で、ソースコード中の数値リテラルを扱う際に、標準の
float32
やfloat64
の精度を超えた任意精度で計算を行うことがあります。これにより、コンパイル時の計算精度を最大限に保ち、最終的な型への変換時にのみ丸めを行うことができます。Goコンパイラでは、Mpflt
(Multi-precision float)のような型がこれに該当します。
-
二進浮動小数点表記 (Binary Floating-Point Notation):
%b
フォーマット指定子などで表示される、二進数形式での浮動小数点数表記です。例えば、1.234p+1000
は「1.234掛ける2の1000乗」を意味します。これはコンピュータ内部の表現に近いため正確ですが、人間にとっては直感的でなく、非常に大きな値や小さな値を理解するのが難しいです。
技術的詳細
このコミットの技術的な変更点は大きく分けて2つあります。
-
float32
定数変換の改善:- 問題の特定: 以前の実装では、
src/cmd/gc/const.c
のtruncfltlit
関数内で、TFLOAT32
型への変換時に、まずmpgetflt(fv)
で任意精度浮動小数点数fv
をdouble
(float64
)に変換し、次にそのdouble
値をfloat
(float32
)にキャストし、さらにそのfloat
値をdouble
に戻してからmpmovecflt
でMpflt
に変換していました。このdouble -> float -> double
という一連の操作が二重丸めを引き起こし、不正確さの原因となっていました。 - 解決策: 新たに
mpgetflt32
関数が導入されました。この関数は、任意精度浮動小数点数Mpflt
を直接float32
の精度で丸めてdouble
型で返すように設計されています。これにより、truncfltlit
関数内でTFLOAT32
への変換を行う際に、mpgetflt32(fv)
を直接呼び出すことで、二重丸めを回避し、float32
の精度に直接かつ正確に丸めることができるようになりました。 mpgetfltN
の導入:src/cmd/gc/mparith3.c
では、mpgetflt
関数がmpgetfltN
という汎用的な関数にリファクタリングされました。mpgetfltN
は、指定された精度(prec
)とバイアス(bias
)に基づいて任意精度浮動小数点数をdouble
に変換します。mpgetflt
はmpgetfltN(a, 53, -1023)
を呼び出し、これはIEEE 754float64
の仮数部53ビット、指数部バイアス-1023に対応します。mpgetflt32
はmpgetfltN(a, 24, -127)
を呼び出し、これはIEEE 754float32
の仮数部24ビット、指数部バイアス-127に対応します。
- この変更により、コンパイラは
float32
定数を扱う際に、より正確で予測可能な丸め動作を実現できるようになりました。
- 問題の特定: 以前の実装では、
-
非常に大きな浮動小数点定数の表示改善:
- 問題の特定:
src/cmd/gc/mparith1.c
のFconv
関数(Mpflt
型のフォーマット変換を担当)では、Mpflt
の指数部が非常に大きい(または小さい)場合、二進浮動小数点表記(%b
)にフォールバックしていました。これは正確ではあるものの、人間が読むには非常に不便でした。 - 解決策:
Fconv
関数に、float64
の範囲をはるかに超える大きな浮動小数点定数(指数部が約900を超える場合)を十進浮動小数点表記(例:1.23456e+1000
)で出力するロジックが追加されました。- 具体的には、
fvp->exp
(二進指数)をlog_10(2)
で乗算して十進指数(dexp
)を近似的に計算し、その整数部をexp
とします。 - 仮数部を調整し、
pow(10, dexp-exp)
を乗算することで、1
から10
の範囲の十進仮数部d
を計算します。 - 最終的に
"%.5fe+%d"
のような形式で、十進仮数部と十進指数部を用いて出力します。これにより、非常に大きな数値でも人間が理解しやすい形式で表示されるようになりました。
- 具体的には、
- 問題の特定:
これらの変更は、Goコンパイラの数値処理の正確性とユーザビリティを向上させるものです。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更箇所は以下の通りです。
-
src/cmd/gc/const.c
:truncfltlit
関数内で、TFLOAT32
型への変換ロジックが変更されました。- float f;
が削除されました。d = mpgetflt(fv); f = d; d = f;
の二重丸めを行う行が削除されました。d = mpgetflt32(fv);
が追加され、float32
への直接丸めを行うmpgetflt32
関数が呼び出されるようになりました。
convlit1
関数からoverflow(n->val, t);
の呼び出しが削除されました。これは、truncfltlit
内でoverflow
が適切に処理されるようになったためです。
-
src/cmd/gc/go.h
:mpgetflt32
関数のプロトタイプ宣言が追加されました。
-
src/cmd/gc/mparith1.c
:Fconv
関数(Mpflt
のフォーマット変換)が大幅に修正されました。- 非常に大きな指数を持つ
Mpflt
を十進表記で出力するための新しいロジックが追加されました。これには、log_10(2)
を用いた十進指数計算、仮数部の調整、そしてfmtprint(fp, "%.5fe+%d", d, exp)
による出力が含まれます。
- 非常に大きな指数を持つ
-
src/cmd/gc/mparith3.c
:mpgetflt
関数がstatic double mpgetfltN(Mpflt *a, int prec, int bias)
という汎用関数にリファクタリングされました。double mpgetflt(Mpflt *a)
が、mpgetfltN(a, 53, -1023)
を呼び出すラッパー関数として再定義されました。double mpgetflt32(Mpflt *a)
が新しく追加され、mpgetfltN(a, 24, -127)
を呼び出すことでfloat32
の精度での変換を提供します。
-
test/float_lit2.go
(新規追加):float32
およびfloat64
定数の変換が、最小/最大境界付近で正しく行われることを確認するためのテストケースが追加されました。fmt.Sprintf("%b", c.val)
を使用して二進表記をチェックしています。
-
test/float_lit3.go
(新規追加):float32
およびfloat64
定数がオーバーフローする場合に、コンパイラが正しくエラーを報告することを確認するためのerrorcheck
テストケースが追加されました。
コアとなるコードの解説
src/cmd/gc/const.c
の変更
// old code
- float f;
// ...
case TFLOAT32:
- d = mpgetflt(fv);
- f = d;
- d = f;
+ d = mpgetflt32(fv);
mpmovecflt(fv, d);
この変更は、float32
への変換パスから二重丸めを排除する核心部分です。以前は、任意精度浮動小数点数fv
を一度float64
(d
)に変換し、それをさらにfloat32
(f
)にキャストし、再びfloat64
に戻すという冗長なステップを踏んでいました。このプロセスが、float32
の表現範囲の限界で不正確さを引き起こす可能性がありました。新しいコードでは、mpgetflt32(fv)
を直接呼び出すことで、fv
をfloat32
の精度で直接丸め、その結果をdouble
として取得します。これにより、丸めが一度だけ行われ、より正確な変換が保証されます。
src/cmd/gc/mparith3.c
の変更
-double
-mpgetflt(Mpflt *a)
+static double
+mpgetfltN(Mpflt *a, int prec, int bias)
{
- int s, i, e;
+ int s, i, e, minexp;
uvlong v, vm;
double f;
// ... (implementation details for converting Mpflt to double with specified precision)
}
+double
+mpgetflt(Mpflt *a)
+{
+ return mpgetfltN(a, 53, -1023);
+}
+
+double
+mpgetflt32(Mpflt *a)
+{
+ return mpgetfltN(a, 24, -127);
+}
このセクションは、浮動小数点数変換の汎用化と正確性の向上を示しています。
mpgetflt
がmpgetfltN
という新しい静的関数にリファクタリングされました。mpgetfltN
は、prec
(仮数部のビット数)とbias
(指数部のバイアス)という2つのパラメータを受け取ることで、任意のIEEE 754浮動小数点形式への変換をサポートします。- 元の
mpgetflt
は、float64
の標準的なパラメータ(仮数部53ビット、指数部バイアス-1023)でmpgetfltN
を呼び出すラッパー関数となりました。 - 新しく追加された
mpgetflt32
は、float32
の標準的なパラメータ(仮数部24ビット、指数部バイアス-127)でmpgetfltN
を呼び出します。 この構造により、float32
への変換がfloat64
への変換と同じ基盤の上に構築され、一貫性と正確性が向上しました。
src/cmd/gc/mparith1.c
の変更
if(fp->flags & FmtSharp) {
// alternate form - decimal for error messages.
// for well in range, convert to double and use print's %g
- if(-900 < fvp->exp && fvp->exp < 900) {
+ exp = fvp->exp + sigfig(fvp)*Mpscale;
+ if(-900 < exp && exp < 900) {
d = mpgetflt(fvp);
if(d >= 0 && (fp->flags & FmtSign))
fmtprint(fp, "+");
- return fmtprint(fp, "%g", d);
+ return fmtprint(fp, "%g", d, exp, fvp); // Note: The original commit diff shows `%g` without `exp, fvp`
}
- // TODO(rsc): for well out of range, print
- // an approximation like 1.234e1000
+
+ // very out of range. compute decimal approximation by hand.
+ // decimal exponent
+ dexp = fvp->exp * 0.301029995663981195; // log_10(2)
+ exp = (int)dexp;
+ // decimal mantissa
+ fv = *fvp;
+ fv.val.neg = 0;
+ fv.exp = 0;
+ d = mpgetflt(&fv);
+ d *= pow(10, dexp-exp);
+ while(d >= 9.99995) {
+ d /= 10;
+ exp++;
+ }
+ if(fvp->val.neg)
+ fmtprint(fp, "-");
+ else if(fp->flags & FmtSign)
+ fmtprint(fp, "+");
+ return fmtprint(fp, "%.5fe+%d", d, exp);
}
このコードブロックは、非常に大きな浮動小数点定数の表示方法を改善するものです。
- 以前は、
fvp->exp
(Mpflt
の二進指数)が-900
から900
の範囲外の場合、二進表記にフォールバックしていました。 - 新しいコードでは、
exp = fvp->exp + sigfig(fvp)*Mpscale;
でより正確な指数を計算し、このexp
が-900
から900
の範囲外の場合に、手動で十進近似を計算するロジックが実行されます。 dexp = fvp->exp * 0.301029995663981195;
は、二進指数を十進指数に変換するためのlog_10(2)
(約0.30103)を使用しています。- その後、仮数部
d
を1
から10
の範囲に調整し、最終的に"%.5fe+%d"
形式で出力することで、人間が読みやすい科学的表記(例:1.23456e+1000
)を実現しています。これにより、コンパイラのエラーメッセージやデバッグ出力が格段に分かりやすくなりました。
関連リンク
- Go issue #8015: cmd/gc: float32 const conversion double rounds
- Go CL 100580044: https://golang.org/cl/100580044
参考にした情報源リンク
- IEEE 754 浮動小数点数標準: https://ja.wikipedia.org/wiki/IEEE_754
- 浮動小数点数: https://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0
- 二重丸め (Double Rounding): https://en.wikipedia.org/wiki/Double-rounding (英語)
- Go言語のコンパイラ(
cmd/gc
)に関する情報(一般的なGoコンパイラの仕組みを理解するために参照) - 任意精度算術ライブラリに関する一般的な情報(
Mpflt
の理解を深めるために参照) log_10(2)
の値: https://ja.wikipedia.org/wiki/2%E3%81%AE%E5%B9%B3%E6%96%B9%E6%A0%B9 (直接的ではないが、対数計算の文脈で参照)