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

[インデックス 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つの部分に分けられます。

  1. 符号部 (Sign): 1ビット。数値が正か負かを示します。
  2. 指数部 (Exponent): 11ビット。数値の大きさを表します。
  3. 仮数部 (Mantissa/Fraction): 52ビット。数値の有効数字を表します。ただし、正規化された浮動小数点数では、仮数部の最上位ビットは常に1であるため、明示的に格納されるのは52ビットですが、実質的には53ビットの精度を持ちます。

多倍長浮動小数点数

多倍長浮動小数点数とは、標準のデータ型(floatdouble)では表現できない、より高い精度や広い範囲の数値を扱うための表現方法です。通常、複数のワード(例えば、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に変換しようとすると、不正確な丸めや情報損失が発生する可能性がありました。

修正後のコードでは、以下の点が変更されています。

  1. 初期シフト量の変更: s = 63s = 53に変更されました。これは、double型の仮数部の精度(53ビット)に直接対応させるための変更です。
  2. 仮数部の段階的な構築:
    • まず、vに上位のビットを集めます。このループはsMpscale(多倍長表現の1ワードあたりのビット数)より小さくなるまで続きます。
    • 次に、vmという新しい変数が導入され、vの現在の値がコピーされます。
    • s > 0の場合、残りのビットをvmにシフトして追加します。これにより、vmdoubleの仮数部に直接対応する53ビットの仮数部を保持するようになります。
    • その後、s64が加算され、さらに下位のビット(64ビット分)をvに集めます。このvは、丸め処理のために使用される追加の精度情報を提供します。
  3. 丸め処理の改善:
    • 元のコードでは、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が奇数であれば切り上げ、偶数であれば切り捨て(またはそのまま)という挙動を実現します。
  4. 最終的な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規格に準拠したより正確な丸め処理を実行できるようになりました。

関連リンク

参考にした情報源リンク