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

[インデックス 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つの主要な問題に対処するために行われました。

  1. float32定数変換の不正確さ: 以前のGoコンパイラでは、ソースコード中の浮動小数点リテラルをfloat32型に変換する際、まずその値をfloat64(倍精度浮動小数点数)に丸め、その後、そのfloat64値をハードウェアの浮動小数点ユニット(FPU)を使ってfloat32(単精度浮動小数点数)に丸めるという二段階のプロセスを踏んでいました。この「二重丸め」は、特に境界値に近い場合や、float32の表現範囲の限界に近い値において、予期せぬ不正確さを引き起こす可能性がありました。

    • 問題点: float64への最初の丸めが、float32の正確な表現から値を遠ざけてしまうことがありました。その結果、float32の範囲内にあるべき値が、float64に丸められた後ではfloat32で表現できない値になってしまい、ハードウェアが0を返すなどの未定義の動作を引き起こす可能性がありました。これは、コンパイラが定数を扱う際の予測可能性と正確性を損なうものでした。また、特定の境界ケースでは、誤った値が使用される原因にもなっていました。
  2. 非常に大きな浮動小数点定数の表示問題: Goコンパイラは、内部的に任意精度浮動小数点数(Mpflt型)を使用して定数を扱います。しかし、これらの定数がfloat64の表現範囲をはるかに超える非常に大きな値である場合、コンパイラはそれらを人間が読みにくい二進浮動小数点表記(例: 1.234p+1000のような形式)で出力していました。これはデバッグやエラーメッセージの可読性を著しく低下させていました。

これらの問題は、Go言語の数値計算の信頼性と開発者の利便性に影響を与えるため、修正が必要とされました。特に、float32の二重丸め問題は、Go言語の厳密な型システムと数値の正確性への期待に反するものでした。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が役立ちます。

  1. 浮動小数点数 (Floating-Point Numbers):

    • コンピュータで実数を近似的に表現するための形式です。一般的に、符号部、仮数部(有効数字)、指数部で構成されます。
    • IEEE 754 標準: 浮動小数点数の表現と演算に関する国際標準です。Go言語を含む多くのプログラミング言語やハードウェアで採用されています。
      • float32 (単精度浮動小数点数): 32ビットで表現され、約7桁の十進精度を持ちます。指数部の範囲が狭いため、表現できる数値の範囲がfloat64よりもはるかに小さいです。
      • float64 (倍精度浮動小数点数): 64ビットで表現され、約15-17桁の十進精度を持ちます。float32よりも広い範囲と高い精度で数値を表現できます。
    • 丸め誤差 (Rounding Error): 浮動小数点数は有限のビット数で表現されるため、正確に表現できない実数(例: 1/3)や、演算結果が表現可能な範囲を超える場合に、最も近い表現可能な値に丸められます。この丸めによって生じる誤差を丸め誤差と呼びます。
    • 二重丸め (Double Rounding): ある精度(例: float64)に丸めた後、さらに低い精度(例: float32)に丸めるプロセスです。この二段階の丸めが、単一の丸めでは発生しない追加の誤差や不正確さを引き起こすことがあります。理想的には、最終的な精度に直接丸めるべきです。
  2. Goコンパイラ (cmd/gc):

    • Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。
    • 定数伝播 (Constant Propagation): コンパイル時に、既知の定数値を計算し、その結果をコードに直接埋め込む最適化手法です。これにより、実行時の計算を減らし、パフォーマンスを向上させます。浮動小数点定数もこのプロセスで処理されます。
    • 任意精度算術 (Arbitrary-Precision Arithmetic): コンパイラ内部で、ソースコード中の数値リテラルを扱う際に、標準のfloat32float64の精度を超えた任意精度で計算を行うことがあります。これにより、コンパイル時の計算精度を最大限に保ち、最終的な型への変換時にのみ丸めを行うことができます。Goコンパイラでは、Mpflt(Multi-precision float)のような型がこれに該当します。
  3. 二進浮動小数点表記 (Binary Floating-Point Notation):

    • %bフォーマット指定子などで表示される、二進数形式での浮動小数点数表記です。例えば、1.234p+1000は「1.234掛ける2の1000乗」を意味します。これはコンピュータ内部の表現に近いため正確ですが、人間にとっては直感的でなく、非常に大きな値や小さな値を理解するのが難しいです。

技術的詳細

このコミットの技術的な変更点は大きく分けて2つあります。

  1. float32定数変換の改善:

    • 問題の特定: 以前の実装では、src/cmd/gc/const.ctruncfltlit関数内で、TFLOAT32型への変換時に、まずmpgetflt(fv)で任意精度浮動小数点数fvdoublefloat64)に変換し、次にそのdouble値をfloatfloat32)にキャストし、さらにそのfloat値をdoubleに戻してからmpmovecfltMpfltに変換していました。このdouble -> float -> doubleという一連の操作が二重丸めを引き起こし、不正確さの原因となっていました。
    • 解決策: 新たにmpgetflt32関数が導入されました。この関数は、任意精度浮動小数点数Mpfltを直接float32の精度で丸めてdouble型で返すように設計されています。これにより、truncfltlit関数内でTFLOAT32への変換を行う際に、mpgetflt32(fv)を直接呼び出すことで、二重丸めを回避し、float32の精度に直接かつ正確に丸めることができるようになりました。
    • mpgetfltNの導入: src/cmd/gc/mparith3.cでは、mpgetflt関数がmpgetfltNという汎用的な関数にリファクタリングされました。mpgetfltNは、指定された精度(prec)とバイアス(bias)に基づいて任意精度浮動小数点数をdoubleに変換します。
      • mpgetfltmpgetfltN(a, 53, -1023)を呼び出し、これはIEEE 754 float64の仮数部53ビット、指数部バイアス-1023に対応します。
      • mpgetflt32mpgetfltN(a, 24, -127)を呼び出し、これはIEEE 754 float32の仮数部24ビット、指数部バイアス-127に対応します。
    • この変更により、コンパイラはfloat32定数を扱う際に、より正確で予測可能な丸め動作を実現できるようになりました。
  2. 非常に大きな浮動小数点定数の表示改善:

    • 問題の特定: src/cmd/gc/mparith1.cFconv関数(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を一度float64d)に変換し、それをさらにfloat32f)にキャストし、再びfloat64に戻すという冗長なステップを踏んでいました。このプロセスが、float32の表現範囲の限界で不正確さを引き起こす可能性がありました。新しいコードでは、mpgetflt32(fv)を直接呼び出すことで、fvfloat32の精度で直接丸め、その結果を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);
+}

このセクションは、浮動小数点数変換の汎用化と正確性の向上を示しています。

  • mpgetfltmpgetfltNという新しい静的関数にリファクタリングされました。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->expMpfltの二進指数)が-900から900の範囲外の場合、二進表記にフォールバックしていました。
  • 新しいコードでは、exp = fvp->exp + sigfig(fvp)*Mpscale;でより正確な指数を計算し、このexp-900から900の範囲外の場合に、手動で十進近似を計算するロジックが実行されます。
  • dexp = fvp->exp * 0.301029995663981195;は、二進指数を十進指数に変換するためのlog_10(2)(約0.30103)を使用しています。
  • その後、仮数部d1から10の範囲に調整し、最終的に"%.5fe+%d"形式で出力することで、人間が読みやすい科学的表記(例: 1.23456e+1000)を実現しています。これにより、コンパイラのエラーメッセージやデバッグ出力が格段に分かりやすくなりました。

関連リンク

参考にした情報源リンク