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

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

このコミットは、Goコンパイラ(cmd/gc)における多倍長浮動小数点数演算ライブラリ(mparith)の改善に関するものです。具体的には、浮動小数点定数の指数部がオーバーフローまたはアンダーフローするケースを適切に処理するための変更が導入されました。これにより、過度に大きな浮動小数点定数はコンパイルエラーとなり、過度に小さな浮動小数点定数はゼロに丸められるようになります。これは、Go言語の定数評価における数値の正確性と堅牢性を向上させるための重要な修正です。

コミット

commit cb502775107ce5f6f22e9b47c9c77300859864b4
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Wed Mar 19 05:48:00 2014 +0100

    cmd/gc: check exponent overflow and underflow in mparith
    
    A too large float constant is an error.
    A too small float constant is rounded to zero.
    
    Fixes #7419
    Update #6902
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/76730046

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

https://github.com/golang/go/commit/cb502775107ce5f6f22e9b47c9c77300859864b4

元コミット内容

Goコンパイラ(cmd/gc)において、多倍長浮動小数点数演算ライブラリ(mparith)で指数部のオーバーフローとアンダーフローをチェックするようにしました。

  • 過度に大きな浮動小数点定数はエラーとなります。
  • 過度に小さな浮動小数点定数はゼロに丸められます。

これはIssue #7419を修正し、Issue #6902を更新します。

変更の背景

Go言語のコンパイラは、ソースコード中の定数をコンパイル時に評価します。この際、浮動小数点定数、特に非常に大きな値や非常に小さな値を扱う場合、その数値がターゲットとなる浮動小数点形式(通常はIEEE 754倍精度浮動小数点数)で表現できる範囲を超えることがあります。

このコミットが修正する主な問題は、以下の2つのシナリオです。

  1. 指数部のオーバーフロー: 非常に大きな浮動小数点定数(例: 1e+1000000)が指定された場合、その指数部が表現可能な最大値を超えてしまう問題です。これまでのコンパイラでは、このようなケースが適切にエラーとして検出されず、予期せぬ動作や誤った数値として扱われる可能性がありました。
  2. 指数部のアンダーフロー: 非常に小さな浮動小数点定数(例: 1e-1000000)が指定された場合、その指数部が表現可能な最小値よりも小さくなってしまう問題です。IEEE 754標準では、このような「非正規化数」や「ゼロへのフラッシュ」といった概念がありますが、コンパイラがこれを適切に処理しないと、誤った値として扱われたり、実行時の挙動が予測不能になったりする可能性がありました。特に、Issue #7419では、Goコンパイラが非常に小さな浮動小数点定数をゼロに丸めるべきところで、誤った値として扱ってしまう挙動が報告されていました。

これらの問題を修正することで、Goコンパイラは浮動小数点定数の扱いにおいて、より堅牢で予測可能な挙動を提供するようになります。これにより、開発者は数値の精度に関する予期せぬバグに遭遇するリスクが低減されます。

前提知識の解説

浮動小数点数表現 (IEEE 754)

現代のコンピュータにおける浮動小数点数は、ほとんどの場合、IEEE 754標準に従って表現されます。この標準では、数値を以下の3つの要素で表現します。

  • 符号 (Sign): 数値が正か負かを示す1ビット。
  • 指数部 (Exponent): 数値のスケール(桁の大きさ)を示す部分。基数(通常は2)の何乗かを表現します。
  • 仮数部 (Mantissa/Significand): 数値の有効数字を示す部分。

例えば、倍精度浮動小数点数(float64)では、64ビットのうち1ビットが符号、11ビットが指数部、52ビットが仮数部に割り当てられます。

  • 正規化数 (Normalized Numbers): 仮数部が1.xxxx...の形式で表現される通常の浮動小数点数。
  • 非正規化数 (Denormalized Numbers): 非常にゼロに近い数値を表現するために、仮数部の先頭の暗黙の1を0として扱う数値。これにより、表現可能な最小の正規化数よりもさらに小さな値を表現できますが、精度は低下します。
  • 無限大 (Infinity): オーバーフローの結果など、表現可能な最大値を超える場合に用いられます。
  • NaN (Not a Number): 不定形な演算結果(例: 0/0, 無限大-無限大)を表します。

オーバーフローとアンダーフロー

  • オーバーフロー (Overflow): 計算結果が、そのデータ型で表現できる最大値を超えてしまう状態です。浮動小数点数の場合、指数部が表現可能な最大値を超えると発生し、通常は無限大として扱われるか、エラーとなります。
  • アンダーフロー (Underflow): 計算結果が、そのデータ型で表現できる最小の非ゼロ値よりも小さくなってしまう状態です。浮動小数点数の場合、指数部が表現可能な最小値よりも小さくなると発生します。これは、非正規化数として表現されるか、あるいは「ゼロへのフラッシュ (flush to zero)」としてゼロに丸められることがあります。

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルの過程で、ソースコード中の定数(数値リテラルなど)は、実行時ではなくコンパイル時に評価されます。この評価には、高い精度を必要とする多倍長演算ライブラリが使用されることがあります。

多倍長浮動小数点演算 (mparith)

mparithは、Goコンパイラ内部で使用される多倍長(multiple-precision)算術ライブラリの一部です。通常のfloat64型よりも高い精度で浮動小数点数を扱うことができます。これは、コンパイル時に定数を評価する際に、丸め誤差を最小限に抑え、正確な結果を得るために重要です。mparithは、Mpfltという構造体で浮動小数点数を表現し、その中に指数部(exp)や仮数部(val)などの情報を保持しています。

技術的詳細

このコミットの核心は、mparithライブラリにおける浮動小数点数の指数部(exp)の管理を強化することです。特に、新しい関数mpsetexpが導入され、既存の浮動小数点演算関数がこのmpsetexpを介して指数部を設定するように変更されました。

mpsetexp関数の役割

mpsetexp関数は、Mpflt型の浮動小数点数と、設定しようとする新しい指数値expを受け取ります。この関数は以下のロジックで指数部を処理します。

  1. 指数部の範囲チェック: (short)exp != expという条件で、expshort型で表現できる範囲を超えているかをチェックします。mparith内部では、指数部がshort型で管理されているため、このチェックはオーバーフローを検出するために重要です。0x7fffshort型の最大値(32767)に相当します。
    • オーバーフローの場合: exp > 0であれば、設定しようとしている指数が正の方向に大きすぎることを意味します。この場合、yyerror("float constant is too large")を呼び出してコンパイルエラーを報告し、a->exp0x7fff(最大値)に設定します。
    • アンダーフローの場合: exp <= 0であれば、設定しようとしている指数が負の方向に小さすぎることを意味します。この場合、mpmovecflt(a, 0)を呼び出して、浮動小数点数をゼロに丸めます。
  2. 正常な場合: expshort型で表現可能な範囲内であれば、a->exp = expとして指数部を直接設定します。

mpsetexpの統合

mpsetexp関数は、mparithライブラリ内の複数の浮動小数点演算関数に統合されました。これにより、指数部が変更される可能性のあるすべての操作で、自動的にオーバーフローとアンダーフローのチェックが行われるようになります。

  • mpatoflt (文字列から浮動小数点数への変換): 文字列リテラルから浮動小数点数を解析する際に、指数部が計算されます。この計算結果をmpsetexpに渡すことで、初期値の段階でオーバーフロー/アンダーフローがチェックされます。特に、dp-exshortの範囲を超える場合にゼロに丸める処理が追加されました。
  • mpnorm (正規化): 浮動小数点数を正規化する際に指数部が調整されます。この調整後の指数部がmpsetexpに渡されます。
  • mpaddfltflt (加算), mpmulfltflt (乗算), mpdivfltflt (除算): これらの演算では、結果の指数部が計算されます。計算された指数部がmpsetexpに渡され、結果の数値が表現可能かどうかが検証されます。
  • mpgetflt (浮動小数点数値の取得): 浮動小数点数を取得する際に、仮数部のシフトに伴って指数部が調整されます。この調整もmpsetexpを介して行われます。

Issue #7419の修正

Issue #7419は、1e-779137のような非常に小さな浮動小数点定数が、Goコンパイラによって正しくゼロに丸められないという問題でした。このコミットによって導入されたmpsetexp関数は、指数部が小さすぎる場合に数値をゼロに丸めるロジックを含んでいます。test/fixedbugs/issue7419.goで追加されたテストは、1e-7791371e-779138という2つの非常に小さな定数が、コンパイル時に正しくゼロとして評価されることを検証しています。

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

このコミットでは、主に以下の4つのファイルが変更されました。

  1. src/cmd/gc/go.h:

    • mpsetexp関数のプロトタイプ宣言が追加されました。
    --- a/src/cmd/gc/go.h
    +++ b/src/cmd/gc/go.h
    @@ -1255,6 +1255,7 @@ void	tmpmovecflt(Mpflt *a, double c);
     void	tmpmulfltflt(Mpflt *a, Mpflt *b);
     void	tmpnegflt(Mpflt *a);
     void	tmpnorm(Mpflt *a);
    +void	mpsetexp(Mpflt *a, int exp);
     int	mptestflt(Mpflt *a);
     int	sigfig(Mpflt *a);
    
  2. src/cmd/gc/mparith1.c:

    • mpatoflt関数内で、指数部を設定する箇所がmpsetexpの呼び出しに置き換えられました。
    • 特に、dp-exshortの範囲を超える場合に、aをゼロに丸めるための条件分岐が追加されました。
    --- a/src/cmd/gc/mparith1.c
    +++ b/src/cmd/gc/mparith1.c
    @@ -416,7 +416,7 @@ mpatoflt(Mpflt *a, char *as)
     	if(eb) {
     		if(dp)
     			goto bad;
    -		a->exp += ex;
    +		mpsetexp(a, a->exp+ex);
     		goto out;
     	}
     
    @@ -427,8 +427,13 @@ mpatoflt(Mpflt *a, char *as)
     		mppow10flt(&b, ex-dp);
     		mpmulfltflt(a, &b);
     	} else {
    -		mppow10flt(&b, dp-ex);
    -		mpdivfltflt(a, &b);
    +		if((short)(dp-ex) != dp-ex) {
    +			tmpmovecflt(a, 0.0);
    +		}
    +		else {
    +			mppow10flt(&b, dp-ex);
    +			mpdivfltflt(a, &b);
    +		}
     	}
     }
    
  3. src/cmd/gc/mparith3.c:

    • mpsetexp関数の実装が追加されました。
    • mpnorm, mpaddfltflt, mpmulfltflt, mpdivfltflt, mpgetfltといった複数の関数内で、指数部を直接設定していた箇所がmpsetexpの呼び出しに置き換えられました。
    --- a/src/cmd/gc/mparith3.c
    +++ b/src/cmd/gc/mparith3.c
    @@ -22,6 +22,27 @@ sigfig(Mpflt *a)
     	return i+1;
     }
     
    +/*
    + * sets the exponent.
    + * a too large exponent is an error.
    + * a too small exponent rounds the number to zero.
    + */
    +void
    +mpsetexp(Mpflt *a, int exp) {
    +	if((short)exp != exp) {
    +		if(exp > 0) {
    +			yyerror("float constant is too large");
    +			a->exp = 0x7fff;
    +		}
    +		else {
    +			tmpmovecflt(a, 0);
    +		}
    +	}
    +	else {
    +		a->exp = exp;
    +	}
    +}
    +
     /*
      * shifts the leading non-zero
      * word of the number to Mpnorm
    @@ -60,7 +81,7 @@ mpnorm(Mpflt *a)
     	}
     
     	mpshiftfix(&a->val, s);
    -	a->exp -= s;
    +	mpsetexp(a, a->exp-s);
     }
     
     /// implements float arihmetic
    @@ -95,7 +116,7 @@ mpaddfltflt(Mpflt *a, Mpflt *b)
     	if(s < 0) {
     		// b is larger, shift a right
     		mpshiftfix(&a->val, s);
    -		a->exp -= s;
    +		mpsetexp(a, a->exp-s);
     		mpaddfixfix(&a->val, &b->val, 0);
     		goto out;
     	}
    @@ -131,7 +152,7 @@ mpmulfltflt(Mpflt *a, Mpflt *b)
     	}
     
     	mpmulfract(&a->val, &b->val);
    -	a->exp = (a->exp + b->exp) + Mpscale*Mpprec - Mpscale - 1;
    +	mpsetexp(a, (a->exp + b->exp) + Mpscale*Mpprec - Mpscale - 1);
     
     	tmpnorm(a);
     	if(Mpdebug)
    @@ -171,7 +192,7 @@ mpdivfltflt(Mpflt *a, Mpflt *b)
     
     	// divide
     	mpdivfract(&a->val, &c.val);
    -	a->exp = (a->exp-c.exp) - Mpscale*(Mpprec-1) + 1;
    +	mpsetexp(a, (a->exp-c.exp) - Mpscale*(Mpprec-1) + 1);
     
     	tmpnorm(a);
     	if(Mpdebug)
    @@ -199,7 +220,10 @@ mpgetflt(Mpflt *a)
     
     	while((a->val.a[Mpnorm-1] & Mpsign) == 0) {
     		mpshiftfix(&a->val, 1);
    -		a->exp -= 1;
    +		mpsetexp(a, a->exp-1);	// can set 'a' to zero
    +		s = sigfig(a);
    +		if(s == 0)
    +			return 0;
     	}
     
     	// the magic numbers (64, 63, 53, 10, -1074) are
    
  4. test/fixedbugs/issue7419.go:

    • Issue #7419の修正を検証するための新しいテストファイルが追加されました。
    • 非常に小さな浮動小数点定数1e-7791371e-779138が、コンパイル時にゼロとして評価されることを確認します。
    // run
    
    // Copyright 2014 The Go Authors. All rights reserved.
    // Use of this source code is governed by a BSD-style
    // license that can be found in the LICENSE file.
    
    // Issue 7419: odd behavior for float constants underflowing to 0
    
    package main
    
    import (
    	"os"
    )
    
    var x = 1e-779137
    var y = 1e-779138
    
    func main() {
    	if x != 0 {
    		os.Exit(1)
    	}
    	if y != 0 {
    		os.Exit(2)
    	}
    }
    

コアとなるコードの解説

mpsetexp関数の詳細

mpsetexp関数は、mparithライブラリにおける浮動小数点数の指数部を安全に設定するためのゲートウェイとして機能します。

void
mpsetexp(Mpflt *a, int exp) {
	if((short)exp != exp) { // expがshort型で表現できる範囲を超えているかチェック
		if(exp > 0) { // 正の方向に大きすぎる場合(オーバーフロー)
			yyerror("float constant is too large"); // コンパイルエラーを報告
			a->exp = 0x7fff; // 指数部を最大値に設定
		}
		else { // 負の方向に小さすぎる場合(アンダーフロー)
			tmpmovecflt(a, 0); // 数値をゼロに丸める
		}
	}
	else { // 正常な範囲内であれば、そのまま設定
		a->exp = exp;
	}
}
  • if((short)exp != exp): この条件は、int型のexpshort型にキャストされた後、元のint型の値と異なる場合に真となります。これは、expshort型の表現範囲(通常-32768から32767)を超えていることを意味します。
  • yyerror("float constant is too large"): Goコンパイラのエラー報告関数です。これにより、コンパイル時にユーザーにエラーメッセージが表示されます。
  • a->exp = 0x7fff;: 0x7fffは16進数で32767であり、符号付き16ビット整数(short)の最大値です。オーバーフローの場合、指数部をこの最大値に設定することで、内部的な整合性を保ちつつ、エラー状態を示します。
  • tmpmovecflt(a, 0);: Mpflt型のaをゼロに設定する関数です。アンダーフローの場合、数値をゼロに丸めるというポリシーに従います。

各関数でのmpsetexpの適用

  • mpatoflt: 文字列から浮動小数点数を解析する際、指数部exが計算されます。以前はa->exp += ex;と直接加算していましたが、mpsetexp(a, a->exp+ex);とすることで、加算結果がオーバーフロー/アンダーフローしないかチェックされるようになりました。また、dp-ex(小数点以下の桁数と指数部の差)がshortの範囲を超えるような極端なアンダーフローの場合も、明示的にゼロに丸める処理が追加されました。
  • mpnorm: 浮動小数点数を正規化する際、仮数部をシフトした分だけ指数部を調整します。この調整もmpsetexpを介して行われることで、正規化の過程で指数部が範囲外になることを防ぎます。
  • mpmulfltflt / mpdivfltflt: 乗算や除算では、結果の指数部はオペランドの指数部の合計または差に基づいて計算されます。これらの計算結果もmpsetexpに渡されることで、演算結果が表現可能な範囲に収まるように保証されます。
  • mpgetflt: 浮動小数点数を取得する際、内部表現から最終的な値に変換する過程で指数部が調整されることがあります。この調整もmpsetexpを通じて行われ、特にゼロへの丸め処理が適切に適用されるようになりました。sigfig(a)が0になる(有効数字がなくなる)場合にreturn 0;とするロジックも追加され、より堅牢になっています。

test/fixedbugs/issue7419.goのテスト内容

このテストは、Go言語のソースコードに記述された非常に小さな浮動小数点定数が、コンパイル時に正しくゼロとして扱われることを検証します。

var x = 1e-779137
var y = 1e-779138

func main() {
	if x != 0 {
		os.Exit(1) // xが0でなければエラー終了
	}
	if y != 0 {
		os.Exit(2) // yが0でなければエラー終了
	}
}

1e-7791371e-779138といった数値は、標準的なfloat64型で表現できる最小の非ゼロ値(約4.9e-324)よりもはるかに小さいため、アンダーフローが発生し、ゼロに丸められるべきです。このテストは、コンパイラがこの丸め処理を正しく行っていることを確認することで、Issue #7419で報告されたバグが修正されたことを保証します。

関連リンク

参考にした情報源リンク