[インデックス 1316] ファイルの概要
このコミットは、Goコンパイラのガベージコレクタ(gc
)の一部であるsrc/cmd/gc/mparith3.c
ファイルに対する変更です。このファイルは、多倍長浮動小数点数(multi-precision floating-point numbers)の演算に関連する機能を提供していると考えられます。特に、mpgetflt
関数は、多倍長浮動小数点数表現から標準のdouble
型への変換を行う役割を担っています。この変換プロセスにおいて、数値の精度と丸め処理が正確に行われることが極めて重要です。
コミット
このコミット 5f1a3be9dd1f6c0bda0993351effffaec0ce49ad
は、src/cmd/gc/mparith3.c
内の mpgetflt
関数における浮動小数点数の丸め処理と精度に関するバグ(内部的なbug120
)を修正するものです。具体的には、多倍長浮動小数点数の仮数部をuvlong
(符号なし64ビット整数)に変換する際のビットシフトと丸めロジックが調整され、より正確なdouble
値への変換が保証されるようになりました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5f1a3be9dd1f6c0bda0993351effffaec0ce49ad
元コミット内容
bug120
R=r
OCL=20921
CL=20921
変更の背景
このコミットの背景には、多倍長浮動小数点数から標準のdouble
型への変換における精度問題、特に丸め処理の不正確さがあったと考えられます。コミットメッセージの「bug120」は、Go開発初期の内部的なバグトラッキングシステムにおける識別子である可能性が高いです。浮動小数点演算は、その性質上、非常に高い精度が求められるため、わずかな計算誤差も最終結果に大きな影響を与えることがあります。mpgetflt
関数は、コンパイラが定数計算などで多倍長精度を扱う際に、最終的にハードウェアがサポートするdouble
型に変換するために使用されます。この変換が不正確であれば、コンパイルされたプログラムの数値計算結果に誤りが生じる可能性がありました。したがって、この修正は、Goコンパイラが生成するバイナリの数値計算の正確性を保証するために不可欠なものでした。
前提知識の解説
浮動小数点数表現
コンピュータにおける浮動小数点数は、一般的にIEEE 754規格に基づいて表現されます。double
型は倍精度浮動小数点数であり、通常64ビットで表現されます。この64ビットは、以下の3つの部分に分けられます。
- 符号部 (Sign): 1ビット。数値が正か負かを示します。
- 指数部 (Exponent): 11ビット。数値の大きさを表します。
- 仮数部 (Mantissa/Fraction): 52ビット。数値の有効数字を表します。ただし、正規化された浮動小数点数では、仮数部の最上位ビットは常に1であるため、明示的に格納されるのは52ビットですが、実質的には53ビットの精度を持ちます。
多倍長浮動小数点数
多倍長浮動小数点数とは、標準のデータ型(float
やdouble
)では表現できない、より高い精度や広い範囲の数値を扱うための表現方法です。通常、複数のワード(例えば、uint64
の配列)を用いて仮数部を表現し、別途指数部と符号部を持ちます。これにより、計算の途中で発生する丸め誤差を最小限に抑え、最終的な結果の精度を向上させることができます。
丸め処理
浮動小数点演算では、計算結果が正確に表現できない場合、最も近い表現可能な値に「丸める」必要があります。IEEE 754規格では、いくつかの丸めモードが定義されていますが、最も一般的なのは「最近接偶数への丸め(Round half to even)」です。これは、ちょうど中間点にある値(例: 0.5)を丸める際に、結果が偶数になるように丸める方法です。これにより、多数の計算を行った際の累積誤差が偏るのを防ぐ効果があります。
ldexp
関数
ldexp(x, exp)
は、x * 2^exp
を計算する関数です。浮動小数点数の指数部を操作する際に使用されます。mpgetflt
関数では、多倍長表現から抽出した仮数部と指数部を組み合わせて最終的なdouble
値を構築するために利用されます。
技術的詳細
このコミットの主要な変更点は、mpgetflt
関数における多倍長浮動小数点数の仮数部をdouble
型(53ビットの仮数部)に変換する際のロジックの改善です。
元のコードでは、仮数部をuvlong
(符号なし64ビット整数)に集める際に、s = 63
から開始していました。これは、63ビットの仮数部を想定していたことを示唆しています。しかし、double
型の仮数部は実質53ビットであるため、この63ビットを直接double
に変換しようとすると、不正確な丸めや情報損失が発生する可能性がありました。
修正後のコードでは、以下の点が変更されています。
- 初期シフト量の変更:
s = 63
がs = 53
に変更されました。これは、double
型の仮数部の精度(53ビット)に直接対応させるための変更です。 - 仮数部の段階的な構築:
- まず、
v
に上位のビットを集めます。このループはs
がMpscale
(多倍長表現の1ワードあたりのビット数)より小さくなるまで続きます。 - 次に、
vm
という新しい変数が導入され、v
の現在の値がコピーされます。 s > 0
の場合、残りのビットをvm
にシフトして追加します。これにより、vm
はdouble
の仮数部に直接対応する53ビットの仮数部を保持するようになります。- その後、
s
に64
が加算され、さらに下位のビット(64ビット分)をv
に集めます。このv
は、丸め処理のために使用される追加の精度情報を提供します。
- まず、
- 丸め処理の改善:
- 元のコードでは、
v
の最下位10ビット(0x3ffULL
)と11ビット目(0x400
)をチェックし、0x200ULL
を加算して「偶数への丸め」を行っていました。これは、63ビットの仮数部を53ビットに丸めるためのロジックでしたが、実装が不正確であった可能性があります。 - 新しい丸めロジックでは、
v != (1ULL<<63) || (vm&1ULL) != 0
という条件が導入されました。v != (1ULL<<63)
: これは、v
がちょうど中間点(1ULL<<63
は64ビットの最上位ビットが1で他が0、つまり0x8000000000000000ULL
)であるかどうかをチェックしています。(vm&1ULL) != 0
:vm
の最下位ビットが1(奇数)であるかどうかをチェックしています。- この条件が真の場合、
vm += v>>63;
を実行します。v>>63
は、v
の最上位ビット(丸め対象の次のビット)が1であれば1、0であれば0になります。これにより、最近接偶数への丸めがより正確に実装されます。具体的には、丸め対象のビットが1で、かつvm
が奇数であれば切り上げ、偶数であれば切り捨て(またはそのまま)という挙動を実現します。
- 元のコードでは、
- 最終的な
double
値の構築:- 元のコードでは
v >>= 10;
としてからf = (double)(v);
としていましたが、これはv
が63ビットの仮数部を持っていたため、53ビットに調整するためのシフトでした。 - 新しいコードでは、
vm
がすでに53ビットの仮数部を保持しているため、f = (double)(vm);
と直接vm
を使用します。 f = ldexp(f, Mpnorm*Mpscale + a->exp - 53);
は、仮数部f
に適切な指数を適用して最終的なdouble
値を生成する部分であり、これは変更されていません。
- 元のコードでは
この修正により、多倍長浮動小数点数からdouble
への変換がIEEE 754規格に準拠した正確な丸め処理で行われるようになり、コンパイラが扱う数値の精度が向上しました。
コアとなるコードの変更箇所
diff --git a/src/cmd/gc/mparith3.c b/src/cmd/gc/mparith3.c
index 7098ba68b2..8e3da7a4b3 100644
--- a/src/cmd/gc/mparith3.c
+++ b/src/cmd/gc/mparith3.c
@@ -161,7 +161,7 @@ double
mpgetflt(Mpflt *a)
{
int s, i;
- uvlong v;
+ uvlong v, vm;
double f;
if(a->val.ovf)
@@ -186,22 +186,31 @@ mpgetflt(Mpflt *a)
// independently or in the 6g half of the compiler
// pick up the mantissa in a uvlong
- s = 63;
+ s = 53;
v = 0;
for(i=Mpnorm-1; s>=Mpscale; i--) {
v = (v<<Mpscale) | a->val.a[i];
s -= Mpscale;
}
+ vm = v;
+ if(s > 0)
+ vm = (vm<<s) | (a->val.a[i]>>(Mpscale-s));
+
+ // continue with 64 more bits
+ s += 64;
+ for(; s>=Mpscale; i--) {
+ v = (v<<Mpscale) | a->val.a[i];
+ s -= Mpscale;
+ }
if(s > 0)
v = (v<<s) | (a->val.a[i]>>(Mpscale-s));
- // 63 bits of mantissa being rounded to 53
- // should do this in multi precision
- if((v&0x3ffULL) != 0x200ULL || (v&0x400) != 0)
- v += 0x200ULL; // round toward even
+//print("vm=%.16llux v=%.16llux\n", vm, v);
+ // round toward even
+ if(v != (1ULL<<63) || (vm&1ULL) != 0)
+ vm += v>>63;
- v >>= 10;
- f = (double)(v);
+ f = (double)(vm);
f = ldexp(f, Mpnorm*Mpscale + a->exp - 53);
if(a->val.neg)
コアとなるコードの解説
変数 vm
の導入
- uvlong v;
+ uvlong v, vm;
vm
という新しいuvlong
型変数が導入されました。これは、最終的なdouble
の仮数部(53ビット)を保持するために使用されます。v
は引き続き、丸め処理のための追加の精度情報(ガードビットなど)を保持するために使用されます。
初期シフト量の変更
- s = 63;
+ s = 53;
s
は、多倍長仮数部からビットを抽出する際の残りビット数を示します。元のコードでは63ビットを想定していましたが、double
の仮数部が実質53ビットであるため、これを53に変更することで、vm
に直接53ビットの仮数部を構築する準備をします。
仮数部 vm
の構築
for(i=Mpnorm-1; s>=Mpscale; i--) {
v = (v<<Mpscale) | a->val.a[i];
s -= Mpscale;
}
+ vm = v;
+ if(s > 0)
+ vm = (vm<<s) | (a->val.a[i]>>(Mpscale-s));
最初のループで、多倍長仮数部の最上位ワードからv
にビットを集めます。このv
は、double
の仮数部を構成するのに十分なビット数(53ビット)をカバーする部分です。
ループ終了後、v
の現在の値がvm
にコピーされます。
if(s > 0)
の条件は、最後のワードからs
ビットだけを抽出する必要がある場合に対応します。これにより、vm
は正確に53ビットの仮数部を保持するようになります。
追加の64ビットの取得
+ // continue with 64 more bits
+ s += 64;
+ for(; s>=Mpscale; i--) {
+ v = (v<<Mpscale) | a->val.a[i];
+ s -= Mpscale;
+ }
if(s > 0)
v = (v<<s) | (a->val.a[i]>>(Mpscale-s));
s
に64を加算し、さらに64ビット分の下位のビットをv
に集めます。このv
は、丸め処理に必要な「ガードビット」や「スティッキービット」などの追加情報を提供します。double
の仮数部(53ビット)のさらに下位のビットを考慮することで、より正確な丸めが可能になります。
丸め処理の改善
- // 63 bits of mantissa being rounded to 53
- // should do this in multi precision
- if((v&0x3ffULL) != 0x200ULL || (v&0x400) != 0)
- v += 0x200ULL; // round toward even
+//print("vm=%.16llux v=%.16llux\n", vm, v);
+ // round toward even
+ if(v != (1ULL<<63) || (vm&1ULL) != 0)
+ vm += v>>63;
元のコードの丸めロジックは削除され、より洗練された「最近接偶数への丸め」が実装されました。
v != (1ULL<<63)
: これは、丸め対象のビット(v
の最上位ビット、つまりvm
の最下位ビットの次のビット)がちょうど中間点(0.5)であるかどうかをチェックします。1ULL<<63
は、64ビットのuvlong
で最上位ビットのみが1である値です。
(vm&1ULL) != 0
: これは、vm
の最下位ビットが1(奇数)であるかどうかをチェックします。
この2つの条件を組み合わせることで、以下の「最近接偶数への丸め」のルールが適用されます。
- 丸め対象のビットが1で、かつ
vm
が奇数であれば、vm
を切り上げます(vm += 1
)。 - それ以外の場合(丸め対象のビットが0、または丸め対象のビットが1で
vm
が偶数)、vm
はそのままか、切り捨てられます。vm += v>>63;
は、v
の最上位ビット(丸め対象のビット)が1であればvm
に1を加算し、0であれば何も加算しないという効果を持ちます。これにより、vm
が偶数になるように丸められます。
最終的なdouble
値の構築
- v >>= 10;
- f = (double)(v);
+ f = (double)(vm);
元のコードでは、63ビットのv
を53ビットに調整するために10ビット右シフトしていましたが、新しいコードではvm
がすでに53ビットの仮数部を保持しているため、vm
を直接double
にキャストします。これにより、double
型の仮数部に正確な値が設定されます。
これらの変更により、mpgetflt
関数は、多倍長浮動小数点数からdouble
への変換において、IEEE 754規格に準拠したより正確な丸め処理を実行できるようになりました。
関連リンク
- Go言語の公式ドキュメント
- IEEE 754 浮動小数点数規格 (Wikipedia)
- Goコンパイラのソースコード
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/1316.txt
- GitHubコミットページ: https://github.com/golang/go/commit/5f1a3be9dd1f6c0bda0993351effffaec0ce49ad
- Google Web Search (for "golang bug120" and general floating-point concepts)