[インデックス 1099] ファイルの概要
このコミットは、Go言語のランタイムの一部である src/runtime/runtime.c
ファイルに対する変更です。runtime.c
は、Goプログラムの実行を支える低レベルな機能、例えばメモリ管理、ゴルーチン(軽量スレッド)のスケジューリング、プリミティブな型操作などをC言語で実装しているファイルです。このファイルは、Go言語の標準ライブラリやユーザーコードが直接触れることはありませんが、Goプログラムが正しく動作するための基盤を提供しています。
コミット
- accept all NaNs, not just the one sys.NaN() returns.
- use union, not cast, to convert between uint64 and float64, to avoid possible problems with gcc in future.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/600ee088b6234cc5a7037c280e8ad89f230e4a6e
元コミット内容
commit 600ee088b6234cc5a7037c280e8ad89f230e4a6e
Author: Russ Cox <rsc@golang.org>
Date: Mon Nov 10 15:17:56 2008 -0800
* accept all NaNs, not just the one sys.NaN() returns.
* use union, not cast, to convert between uint64 and float64,
to avoid possible problems with gcc in future.
R=r
DELTA=75 (39 added, 15 deleted, 21 changed)
OCL=18926
CL=18926
変更の背景
このコミットには、主に二つの重要な変更の背景があります。
-
NaN (Not a Number) の扱いの一貫性向上: 浮動小数点数のNaNは、不定な演算結果(例: 0/0、無限大-無限大)を表すためにIEEE 754標準で定義されています。NaNには様々なビットパターンが存在し、特定のビットが「ペイロード」として利用されることがあります。以前のGoランタイムの実装では、
sys.NaN()
関数が返す特定のNaN値のみをisNaN
関数が正しく認識していました。しかし、外部からの入力や他の演算結果として生成される可能性のある、IEEE 754標準に準拠した他の有効なNaN値もisNaN
が正しく識別できるようにする必要がありました。これは、浮動小数点演算の堅牢性と標準への準拠を強化するためです。 -
float64
とuint64
間の型変換における潜在的な問題の回避: C言語では、異なる型のポインタ間でキャストを行い、そのポインタを介してデータにアクセスする「型パンニング (type punning)」と呼ばれる手法が使われることがあります(例:*(uint64*)&d
)。これは、浮動小数点数のビット表現を整数として直接操作する際によく用いられます。しかし、C標準の「厳密なエイリアシング規則 (Strict Aliasing Rule)」に違反する可能性があり、コンパイラ(特にGCC)が最適化を行う際に予期せぬ動作やバグを引き起こすことがあります。この規則は、異なる型のポインタが同じメモリ位置を指すことを禁止しており、コンパイラはこれを利用してコードを最適化します。この最適化が、意図しない結果をもたらすことを避けるため、より安全で標準に準拠した共用体(union)を用いた変換方法に切り替える必要がありました。
前提知識の解説
IEEE 754 浮動小数点標準
IEEE 754は、コンピュータにおける浮動小数点数の表現と演算に関する国際標準です。この標準は、単精度(32ビット)と倍精度(64ビット)の浮動小数点数について、符号、指数部、仮数部のビット割り当てを定めています。
- NaN (Not a Number): 不定な結果(例: 0/0, sqrt(-1))を表す特殊な値です。NaNのビットパターンは、指数部がすべて1で、仮数部が0以外であることで識別されます。仮数部の残りのビットは「ペイロード」として利用でき、様々な種類のNaNを区別するために使われることがあります。
- Infinity (無限大): オーバーフローなどによって発生する無限大を表す特殊な値です。指数部がすべて1で、仮数部がすべて0であることで識別されます。符号ビットによって正の無限大と負の無限大が区別されます。
- 正規化数 (Normalized Numbers): 一般的な有限の非ゼロ数を表します。
- 非正規化数 (Denormalized Numbers): ゼロに近い非常に小さな数を表します。
C言語における厳密なエイリアシング規則と共用体 (Union)
C言語の標準には「厳密なエイリアシング規則」というものがあります。これは、異なる型のポインタが同じメモリ領域を指す(エイリアシングする)場合、特定の例外を除いて、そのポインタを介したアクセスは未定義動作を引き起こす可能性があるというものです。例えば、float64
型の変数 d
のアドレスを uint64*
にキャストし、そのポインタを介して d
のビットパターンを uint64
として読み書きする *(uint64*)&d
のようなコードは、この規則に違反する可能性があります。
コンパイラは、この規則を利用してコードを最適化します。例えば、あるメモリ位置が float64
型としてアクセスされた後、別の uint64
型としてアクセスされた場合、コンパイラはそれらが異なるメモリ位置を指していると仮定し、最適化によって予期せぬ結果を生むことがあります。
この問題を回避するための安全な方法の一つが共用体 (Union) の利用です。共用体は、異なる型のメンバーが同じメモリ領域を共有するデータ構造です。共用体を使って float64
と uint64
を定義し、float64
メンバーに値を代入した後、uint64
メンバーを読み出すことで、厳密なエイリアシング規則に違反することなく、浮動小数点数のビットパターンを整数として安全に操作できます。
union {
float64 f;
uint64 i;
} u;
u.f = some_float_value; // float64として値を設定
uint64 bits = u.i; // uint64としてビットパターンを読み出す
技術的詳細
このコミットは、Goランタイムにおける浮動小数点数(特にfloat64
)の内部表現操作に関する二つの主要な改善を行っています。
-
NaNの認識ロジックの改善:
- 以前の
isNaN
関数は、特定のNaN値(sys.NaN()
が返すもの)にのみ対応していました。 - 変更後、
isNaN
関数はIEEE 754標準に準拠し、より広範なNaNビットパターンを認識するように修正されました。具体的には、float64
のビット表現において、指数部がすべて1 (0x7FF
) であり、かつ無限大ではない(仮数部が0ではない)場合にNaNと判断するロジックが導入されました。 ((uint32)(x>>52) & 0x7FF) == 0x7FF
は、float64
の64ビット表現のうち、上位11ビット(指数部)がすべて1であるかをチェックしています。これは、NaNと無限大の共通の特性です。!isInf(f, 0)
は、その値が無限大ではないことを確認しています。これにより、指数部がすべて1で仮数部が0である無限大と、指数部がすべて1で仮数部が0ではないNaNを区別しています。
- 以前の
-
float64
とuint64
間の安全な変換:- これまでのコードでは、
float64
の値をuint64
として、またはその逆として扱う際に、*(uint64*)&d
のようなポインタキャストが多用されていました。これは、前述の通りC言語の厳密なエイリアシング規則に違反し、コンパイラの最適化によって未定義動作を引き起こす可能性がありました。 - このコミットでは、この問題を解決するために、
float32tobits
,float64tobits
,float64frombits
というヘルパー関数が導入されました。これらの関数は、共用体 (union
) を内部的に使用することで、型安全なビットパターン変換を実現しています。 - 例えば、
float64tobits(float64 f)
関数は、float64
型の引数f
を受け取り、共用体を介してそのビットパターンをuint64
として返します。これにより、コンパイラがエイリアシング違反を検出して最適化を誤るリスクがなくなります。 - この変更は、
isInf
,isNaN
,NaN
,Inf
,frexp
,ldexp
,modf
,sys·float32bits
,sys·float64bits
など、浮動小数点数のビット表現を直接操作するすべての関数に適用されました。
- これまでのコードでは、
コアとなるコードの変更箇所
変更は src/runtime/runtime.c
ファイルに集中しています。
-
新規追加関数:
float32tobits(float32 f)
:float32
をuint32
ビットパターンに安全に変換。float64tobits(float64 f)
:float64
をuint64
ビットパターンに安全に変換。float64frombits(uint64 i)
:uint64
ビットパターンをfloat64
に安全に変換。
-
既存関数の変更:
isInf(float64 f, int32 sign)
:*(uint64*)&d
をfloat64tobits(f)
に変更。NaN(void)
:*(float64*)&uvnan
をfloat64frombits(uvnan)
に変更。isNaN(float64 f)
:*(uint64*)&d
をfloat64tobits(f)
に変更。- NaN判定ロジックを
(uint32)(x>>32)==0x7FF00000 && !isInf(d, 0)
から((uint32)(x>>52) & 0x7FF) == 0x7FF && !isInf(f, 0)
に変更。
Inf(int32 sign)
:*(float64*)&uvinf
/*(float64*)&uvneginf
をfloat64frombits(uvinf)
/float64frombits(uvneginf)
に変更。frexp(float64 d, int32 *ep)
:*(uint64*)&d
をfloat64tobits(d)
に、*(float64*)&x
をfloat64frombits(x)
に変更。ldexp(float64 d, int32 e)
:*(uint64*)&d
をfloat64tobits(d)
に、*(float64*)&x
をfloat64frombits(x)
に変更。modf(float64 d, float64 *ip)
:*(uint64*)&d
をfloat64tobits(d)
に、*(float64*)&x
をfloat64frombits(x)
に変更。sys·float32bits(float32 din, uint32 iou)
:*(uint32*)&din
をiou = float32tobits(din)
に変更。sys·float64bits(float64 din, uint64 iou)
:*(uint64*)&din
をiou = float64tobits(din)
に変更。
コアとなるコードの解説
float32tobits
, float64tobits
, float64frombits
関数
これらの関数は、C言語の共用体(union)を利用して、浮動小数点数と整数(ビットパターン)間の安全な変換を提供します。
static uint32
float32tobits(float32 f)
{
// The obvious cast-and-pointer code is technically
// not valid, and gcc miscompiles it. Use a union instead.
union {
float32 f;
uint32 i;
} u;
u.f = f;
return u.i;
}
static uint64
float64tobits(float64 f)
{
// The obvious cast-and-pointer code is technically
// not valid, and gcc miscompiles it. Use a union instead.
union {
float64 f;
uint64 i;
} u;
u.f = f;
return u.i;
}
static float64
float64frombits(uint64 i)
{
// The obvious cast-and-pointer code is technically
// not valid, and gcc miscompiles it. Use a union instead.
union {
float64 f;
uint64 i;
} u;
u.i = i;
return u.f;
}
これらの関数は、float32
や float64
の値を共用体の浮動小数点メンバーに代入し、その後、共用体の整数メンバーからそのビットパターンを読み出すことで、厳密なエイリアシング規則に違反することなく、浮動小数点数の内部表現にアクセスすることを可能にします。これにより、コンパイラの最適化による潜在的なバグを回避し、コードの移植性と堅牢性を向上させています。
isNaN
関数のロジック変更
isNaN
関数の変更は、IEEE 754標準への準拠を強化し、より広範なNaN値を正しく識別できるようにするためのものです。
bool
isNaN(float64 f)
{
uint64 x;
x = float64tobits(f); // 安全なビットパターン取得
// NaNの判定ロジックを更新
// 指数部がすべて1 (0x7FF) であり、かつ無限大ではないことを確認
return ((uint32)(x>>52) & 0x7FF) == 0x7FF && !isInf(f, 0);
}
以前の isNaN
は、特定のビットパターンに依存していましたが、新しいロジックはIEEE 754のNaNの定義(指数部がすべて1で、仮数部が0以外)に直接対応しています。x>>52
は、float64
の64ビット表現から指数部(上位11ビット)を抽出します。& 0x7FF
は、その11ビットがすべて1であるか(つまり 0x7FF
であるか)をチェックします。最後に !isInf(f, 0)
で、無限大ではないことを確認することで、NaNを正確に識別します。
その他の浮動小数点数操作関数の変更
isInf
, NaN
, Inf
, frexp
, ldexp
, modf
, sys·float32bits
, sys·float64bits
といった、浮動小数点数のビット表現を直接操作するすべての関数において、*(uint64*)&d
のような直接的なポインタキャストが、新しく導入された float64tobits
や float64frombits
関数への呼び出しに置き換えられました。これにより、Goランタイム全体で浮動小数点数と整数ビットパターン間の変換が安全かつ標準に準拠した方法で行われるようになりました。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- IEEE 754 浮動小数点標準 (Wikipedia): https://ja.wikipedia.org/wiki/IEEE_754
- C言語の厳密なエイリアシング規則 (Wikipedia): https://ja.wikipedia.org/wiki/%E5%8E%B3%E5%AF%86%E3%81%AA%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B7%E3%83%B3%E3%82%B0%E8%A6%8F%E5%89%87
参考にした情報源リンク
- IEEE 754 standard for floating-point arithmetic: https://en.wikipedia.org/wiki/IEEE_754
- Strict aliasing rule in C: https://en.wikipedia.org/wiki/Strict_aliasing
- Type punning with unions in C: https://stackoverflow.com/questions/1162609/type-punning-with-unions-in-c
- Go runtime source code (for general context): https://github.com/golang/go/tree/master/src/runtime