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

[インデックス 19432] ファイルの概要

本コミットは、Goコンパイラ(cmd/gc)における浮動小数点数の丸め処理に関するバグ修正を目的としています。具体的には、src/cmd/gc/mparith3.cファイル内のmpgetfltN関数における浮動小数点数の変換ロジックが修正され、より正確な丸めが行われるようになりました。これにより、以前のバージョンで発生していた、下位ビットの考慮不足による不正確な丸めが解消されています。

コミット

commit 1be479df926fae2291e78f59be7474e6edb1cf97
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 21 17:11:52 2014 -0400

    cmd/gc: fix floating point rounding again
    
    Passes the expanded test in CL 100660044,
    which gives me some confidence that it
    might be right.
    
    (The old code failed by not considering all the
    low bits.)
    
    LGTM=r
    R=golang-codereviews, r, bradfitz
    CC=golang-codereviews, iant, khr
    https://golang.org/cl/99410051

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1be479df926fae2291e78f59be7474e6edb1cf97

元コミット内容

このコミットの元の内容は、Goコンパイラにおける浮動小数点数の丸め処理の再修正です。以前のコードでは、浮動小数点数の変換時に下位ビットが適切に考慮されていなかったため、不正確な丸めが発生していました。この修正は、拡張されたテストケース(CL 100660044)をパスすることで、その正確性が検証されています。

変更の背景

浮動小数点数の計算は、コンピュータサイエンスにおいて非常に重要な要素であり、その正確性は多くのアプリケーションで求められます。特に、コンパイラが定数計算やリテラル値を浮動小数点数に変換する際には、IEEE 754標準に準拠した正確な丸め処理が不可欠です。

Goコンパイラ(cmd/gc)は、Go言語のソースコードを機械語に変換する役割を担っています。この過程で、ソースコード中の浮動小数点数リテラルや、コンパイル時に評価される浮動小数点数演算の結果を、ターゲットアーキテクチャの浮動小数点数形式(通常はIEEE 754倍精度浮動小数点数)に変換する必要があります。

本コミット以前のcmd/gcの浮動小数点数丸め処理には、以下のような問題がありました。

  • 下位ビットの考慮不足: 浮動小数点数の丸めを行う際、単に最も近い表現可能な値に丸めるだけでなく、丸め対象の数値がちょうど中間点にある場合に、偶数に丸める(round half to even)というIEEE 754の規定があります。この処理を正確に行うためには、丸め対象の数値の最下位ビットだけでなく、それよりもさらに下位のビット(ガードビット、スティッキービットなどと呼ばれる)を考慮する必要があります。以前のコードでは、これらの下位ビットが十分に考慮されていなかったため、特定のケースで不正確な丸めが発生していました。
  • テストカバレッジの不足: 不正確な丸め処理が発覚した背景には、その問題を顕在化させるようなテストケースが不足していた可能性があります。コミットメッセージにある「expanded test in CL 100660044」は、この問題に対処するために、より厳密なテストケースが追加されたことを示唆しています。

これらの問題は、Goプログラムが浮動小数点数を含む計算を行う際に、予期せぬ結果や誤差を生じさせる可能性がありました。特に、金融計算、科学技術計算、グラフィックスなど、浮動小数点数の精度が厳密に求められる分野では、このようなコンパイラの丸め誤差は致命的な問題となり得ます。

このコミットは、これらの問題を解決し、Goコンパイラが生成する浮動小数点数の精度とIEEE 754標準への準拠を向上させることを目的としています。

前提知識の解説

本コミットの理解を深めるために、以下の前提知識を解説します。

1. 浮動小数点数とIEEE 754標準

現代のコンピュータにおける浮動小数点数の表現は、ほとんどの場合、IEEE 754標準に準拠しています。この標準は、浮動小数点数を符号部、指数部、仮数部(または有効数字部、mantissa/significand)の3つの要素で表現することを定めています。

  • 符号部 (Sign Bit): 数値が正か負かを示す1ビット。
  • 指数部 (Exponent): 数値の大きさを表す部分。バイアス形式で表現され、実際の指数に一定の値を加算した形で格納されます。
  • 仮数部 (Mantissa/Significand): 数値の精度を表す部分。正規化された形式では、常に1.xxxx...という形になり、先頭の1は暗黙的に表現されるため、小数点以下の部分のみが格納されます。

IEEE 754は、単精度(32ビット)と倍精度(64ビット)の2つの主要な形式を定義しており、Go言語のfloat32float64はそれぞれこれらに対応します。

2. 浮動小数点数の丸めモード

IEEE 754標準では、浮動小数点数の演算結果が正確に表現できない場合に、どのように丸めるかを規定する「丸めモード」が定義されています。最も一般的に使用されるデフォルトの丸めモードは「最近接偶数への丸め (Round to Nearest, Ties to Even)」です。

この丸めモードのルールは以下の通りです。

  • 表現可能な2つの数値の中間にない場合は、最も近い表現可能な数値に丸めます。
  • 表現可能な2つの数値のちょうど中間にある場合は、仮数部の最下位ビットが偶数になる方(つまり、末尾が0になる方)に丸めます。

この「最近接偶数への丸め」は、統計的なバイアスを最小限に抑える効果があります。例えば、常に切り上げたり切り捨てたりすると、誤差が一方向に偏って蓄積される可能性がありますが、偶数への丸めはこれを防ぎます。

この丸め処理を正確に行うためには、丸め対象の数値が表現可能な2つの数値のちょうど中間にあるかどうかを判断するために、仮数部の最下位ビットだけでなく、そのさらに下位のビット(ガードビット、スティッキービットなど)を考慮する必要があります。

  • ガードビット (Guard Bit): 丸め対象の数値の最下位ビットのすぐ下のビット。
  • スティッキービット (Sticky Bit): ガードビットよりさらに下位のビットのいずれかが1である場合に1となるビット。

これらのビットを適切に利用することで、正確な「最近接偶数への丸め」を実現します。

3. Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する主要なツールです。コンパイルの過程で、cmd/gcは以下のような処理を行います。

  • 字句解析と構文解析: ソースコードをトークンに分解し、抽象構文木(AST)を構築します。
  • 型チェックと意味解析: 変数の型が正しいか、関数呼び出しが適切かなどを検証します。
  • 最適化: コードの実行効率を向上させるための変換を行います。これには、定数畳み込み(constant folding)も含まれます。定数畳み込みとは、コンパイル時に計算可能な定数式(例: 3.14 * 2.0)を、その計算結果(例: 6.28)に置き換える最適化です。この際、浮動小数点数の計算と丸め処理がコンパイラ内部で行われます。
  • コード生成: ASTを基に、ターゲットアーキテクチャの機械語コードを生成します。

src/cmd/gc/mparith3.cは、cmd/gcの一部であり、多倍長浮動小数点数(multiple-precision floating-point numbers)の算術演算を扱うためのC言語ソースファイルです。Goコンパイラは、高い精度で浮動小数点数を扱うために、内部的に多倍長浮動小数点数ライブラリを使用しています。これにより、ソースコード中の浮動小数点数リテラルや定数式の評価を、ターゲットアーキテクチャの浮動小数点数形式に変換する前に、より高い精度で行うことができます。mpgetfltN関数は、この多倍長浮動小数点数から、指定された精度(prec)とバイアス(bias)を持つ標準的な浮動小数点数(double)に変換する役割を担っています。

4. uvlongldexp関数

  • uvlong: unsigned long longのGoコンパイラ内部での型エイリアスと考えられます。C言語における64ビット符号なし整数型であり、浮動小数点数の仮数部や指数部をビット操作で扱う際に使用されます。
  • ldexp関数: C言語の標準ライブラリ関数で、ldexp(x, exp)x * 2^exp を計算します。浮動小数点数の仮数部と指数部から、実際の浮動小数点数を構築する際に使用されます。

技術的詳細

本コミットの技術的詳細は、src/cmd/gc/mparith3.cファイル内のmpgetfltN関数の変更に集約されます。この関数は、多倍長浮動小数点数表現(Mpflt構造体)から、指定された精度(prec)を持つ標準的なdouble型浮動小数点数への変換を行います。

変更の核心は、浮動小数点数の丸め処理において、より多くの下位ビットを考慮し、IEEE 754標準の「最近接偶数への丸め」を正確に実装することにあります。

変更点の分析

  1. 丸めビットの考慮数の増加:

    • 変更前: s = prec+1;
    • 変更後: s = prec+2; この変更は、仮数部(precビット)に加えて、丸め処理に必要なビット数を1ビット増やしたことを意味します。新しいコメント「// pick up the mantissa, a rounding bit, and a tie-breaking bit in a uvlong」が示すように、これは「丸めビット(rounding bit)」と「タイブレーキングビット(tie-breaking bit)」の2つの追加ビットを考慮するためです。タイブレーキングビットは、ちょうど中間点にある場合に偶数に丸めるための判断に不可欠です。
  2. 下位ビットの取り込みロジックの改善: 変更前はvmという変数を使って下位ビットを保持していましたが、変更後はvに直接、より正確な下位ビットの情報を統合しています。

    特に重要なのは、以下の新しいコードブロックです。

    if(s > 0) {
    	v = (v<<s) | (a->val.a[i]>>(Mpscale-s));
    	if((a->val.a[i]&((1<<(Mpscale-s))-1)) != 0)
    		v |= 1;
    	i--;
    }
    for(; i >= 0; i--) {
    	if(a->val.a[i] != 0)
    		v |= 1;
    }
    
    • v = (v<<s) | (a->val.a[i]>>(Mpscale-s));: これは、多倍長浮動小数点数の配列a->val.aから、必要な仮数部と丸めビット、タイブレーキングビットをvに取り込む処理です。
    • if((a->val.a[i]&((1<<(Mpscale-s))-1)) != 0) v |= 1;: ここが重要な変更点です。a->val.a[i]の残りのビット(Mpscale-sビットより下位のビット)のいずれかが1である場合、vの最下位ビットを1に設定しています。これは、IEEE 754の丸め処理でいう「スティッキービット」の概念を実装しており、丸め対象の数値がちょうど中間点にあるかどうかを判断するために、より下位のビットの情報を集約しています。これにより、以前のコードで「考慮されていなかった下位ビット」が適切に扱われるようになります。
    • for(; i >= 0; i--) { if(a->val.a[i] != 0) v |= 1; }: さらに下位のワードに1が存在する場合も、vの最下位ビットを1に設定し、スティッキービットの情報を確実に伝播させています。
  3. 漸進的アンダーフロー(Gradual Underflow)の処理: アンダーフローが発生した場合(数値が非常に小さく、正規化された形式で表現できない場合)、非正規化数(denormalized numbers)として表現されます。この処理は変更前後で大きな違いはありませんが、丸め処理のロジックが変更されたことに伴い、vのシフト処理がよりシンプルになっています。

    • 変更前: v |= vm & ((1ULL<<s) - 1); vm >>= s;
    • 変更後: if((v & ((1<<s)-1)) != 0) v |= 1<<s; v >>= s; この変更は、vm変数の廃止と、vに直接スティッキービットの情報を統合した結果です。
  4. 「最近接偶数への丸め」ロジックの再実装: 変更前は複雑なif(v != 0 || (vm&2ULL) != 0)条件とvmを使った丸め処理でしたが、変更後はより簡潔で標準的な「最近接偶数への丸め」のロジックが導入されています。

    // round to even
    v |= (v&4)>>2;
    v += v&1;
    v >>= 2;
    

    この3行のコードは、IEEE 754の「最近接偶数への丸め」をビット操作で実現しています。

    • v |= (v&4)>>2;: これは、vのビット列において、丸め対象のビット(最下位から3番目のビット、つまりv...X00または...X10X)が1である場合に、最下位ビットを1に設定する操作です。これは、丸め対象の数値がちょうど中間点にあるかどうかを判断し、偶数に丸めるための準備です。
    • v += v&1;: vの最下位ビットが1(奇数)の場合に1を加算し、偶数に丸めます。
    • v >>= 2;: 最後に、丸め処理のために追加した2ビット(丸めビットとタイブレーキングビット)を右シフトで取り除き、最終的な仮数部を得ます。
  5. vm変数の廃止: 変更前はvmというuvlong型の変数が使用されていましたが、変更後はこれが完全に廃止され、すべての丸め関連のビット情報がvに集約されるようになりました。これにより、コードが簡潔になり、理解しやすくなっています。

これらの変更により、mpgetfltN関数は、多倍長浮動小数点数からdouble型への変換において、IEEE 754標準に厳密に準拠した「最近接偶数への丸め」を正確に実行できるようになりました。特に、以前のコードで考慮されていなかった「下位ビット」の情報が、スティッキービットの概念を通じて適切に丸め判断に利用されるようになった点が、この修正の最も重要な側面です。

コアとなるコードの変更箇所

変更はsrc/cmd/gc/mparith3.cファイル内のmpgetfltN関数に集中しています。

--- a/src/cmd/gc/mparith3.c
+++ b/src/cmd/gc/mparith3.c
@@ -203,7 +203,7 @@ static double
 mpgetfltN(Mpflt *a, int prec, int bias)
 {
  	int s, i, e, minexp;
- 	uvlong v, vm;
+ 	uvlong v;
  	double f;
  
  	if(a->val.ovf && nsavederrors+nerrors == 0)
@@ -226,25 +226,23 @@ mpgetfltN(Mpflt *a, int prec, int bias)
  			return 0;
  	}
  
- 	// pick up the mantissa and a rounding bit in a uvlong
- 	s = prec+1;
+ 	// pick up the mantissa, a rounding bit, and a tie-breaking bit in a uvlong
+ 	s = prec+2;
  	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)
+ 	if(s > 0) {
  		v = (v<<s) | (a->val.a[i]>>(Mpscale-s));
+ 		if((a->val.a[i]&((1<<(Mpscale-s))-1)) != 0)
+ 			v |= 1;
+ 		i--;
+ 	}
+ 	for(; i >= 0; i--) {
+ 		if(a->val.a[i] != 0)
+ 			v |= 1;
+ 	}
  
  	// gradual underflow
  	e = Mpnorm*Mpscale + a->exp - prec;
@@ -253,23 +251,23 @@ mpgetfltN(Mpflt *a, int prec, int bias)
  	s = minexp - e;
  	if(s > prec+1)
  		s = prec+1;
- 	v |= vm & ((1ULL<<s) - 1);
- 	vm >>= s;
+ 	if((v & ((1<<s)-1)) != 0)
+ 		v |= 1<<s;
+ 	v >>= s;
  	e = minexp;
  	}
- 	
- //print("vm=%.16llux v=%.16llux\n", vm, v);
- 	// round toward even
- 	if(v != 0 || (vm&2ULL) != 0)
- 		vm = (vm>>1) + (vm&1ULL);
- 	else
- 		vm >>= 1;
-
- 	f = (double)(vm);
+ 	
+ 	// round to even
+ 	v |= (v&4)>>2;
+ 	v += v&1;
+ 	v >>= 2;
+ 
+ 	f = (double)(v);
  	f = ldexp(f, e);
  
  	if(a->val.neg)
  		f = -f;
+
  	return f;
  }
  

コアとなるコードの解説

mpgetfltN関数は、多倍長浮動小数点数Mpflt *aを、指定された精度precと指数バイアスbiasを持つdouble型浮動小数点数に変換します。

主要な変更点は以下の通りです。

  1. uvlong vm; の削除: 以前のコードでは、vmという変数が一時的に仮数部と丸めビットを保持するために使われていましたが、このコミットで削除されました。これにより、コードが簡素化され、すべての関連ビット情報が単一のv変数で管理されるようになりました。

  2. s = prec+2;: sは、仮数部と丸め処理に必要な追加ビットの合計数を表します。precは仮数部のビット数です。以前はprec+1でしたが、prec+2に変更されたことで、仮数部に加えて「丸めビット」と「タイブレーキングビット」の2ビットを考慮するようになりました。これにより、IEEE 754の「最近接偶数への丸め」を正確に実行するための十分な情報が確保されます。

  3. 下位ビットの取り込みロジックの改善: forループ内で多倍長浮動小数点数の各ワード(a->val.a[i])からビットを取り込む際、以下の新しいロジックが追加されました。

    if(s > 0) {
    	v = (v<<s) | (a->val.a[i]>>(Mpscale-s));
    	if((a->val.a[i]&((1<<(Mpscale-s))-1)) != 0)
    		v |= 1;
    	i--;
    }
    for(; i >= 0; i--) {
    	if(a->val.a[i] != 0)
    		v |= 1;
    }
    

    この部分は、多倍長浮動小数点数の残りのワード(a->val.a[i])に1ビットでも値が存在する場合、vの最下位ビットを1に設定します。これは、丸め処理における「スティッキービット」の役割を果たします。スティッキービットは、丸め対象の数値がちょうど中間点にあるかどうかを判断する際に、より下位のビットに非ゼロの値が存在するかどうかを示す重要な情報です。この変更により、以前のコードで「考慮されていなかった下位ビット」が適切に丸め判断に反映されるようになりました。

  4. 漸進的アンダーフロー処理の簡素化: アンダーフローが発生した場合の処理も、vm変数の廃止に伴い簡素化されました。

    if((v & ((1<<s)-1)) != 0)
    	v |= 1<<s;
    v >>= s;
    

    これは、vの最下位sビットの中に非ゼロの値がある場合、vs番目のビットをセットし、その後sビット右シフトすることで、非正規化数の丸め処理を適切に行います。

  5. 「最近接偶数への丸め」ロジックの再実装: 最も重要な変更は、丸め処理のコアロジックです。

    // round to even
    v |= (v&4)>>2;
    v += v&1;
    v >>= 2;
    
    • v |= (v&4)>>2;: vの最下位から3番目のビット(v...X00または...X10X)が1である場合、vの最下位ビットを1に設定します。これは、丸め対象の数値がちょうど中間点にある場合に、偶数に丸めるための準備です。
    • v += v&1;: vの最下位ビットが1(奇数)の場合、1を加算して偶数に丸めます。例えば、...01...10に、...11...00(繰り上がり)になります。
    • v >>= 2;: 丸め処理のために追加した2ビット(丸めビットとタイブレーキングビット)を右シフトで取り除き、最終的な仮数部を得ます。

これらの変更により、mpgetfltN関数は、多倍長浮動小数点数からdouble型への変換において、IEEE 754標準の「最近接偶数への丸め」をより正確かつ効率的に実行できるようになりました。

関連リンク

参考にした情報源リンク