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

[インデックス 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

変更の背景

このコミットには、主に二つの重要な変更の背景があります。

  1. NaN (Not a Number) の扱いの一貫性向上: 浮動小数点数のNaNは、不定な演算結果(例: 0/0、無限大-無限大)を表すためにIEEE 754標準で定義されています。NaNには様々なビットパターンが存在し、特定のビットが「ペイロード」として利用されることがあります。以前のGoランタイムの実装では、sys.NaN() 関数が返す特定のNaN値のみを isNaN 関数が正しく認識していました。しかし、外部からの入力や他の演算結果として生成される可能性のある、IEEE 754標準に準拠した他の有効なNaN値も isNaN が正しく識別できるようにする必要がありました。これは、浮動小数点演算の堅牢性と標準への準拠を強化するためです。

  2. float64uint64 間の型変換における潜在的な問題の回避: 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) の利用です。共用体は、異なる型のメンバーが同じメモリ領域を共有するデータ構造です。共用体を使って float64uint64 を定義し、float64 メンバーに値を代入した後、uint64 メンバーを読み出すことで、厳密なエイリアシング規則に違反することなく、浮動小数点数のビットパターンを整数として安全に操作できます。

union {
    float64 f;
    uint64 i;
} u;

u.f = some_float_value; // float64として値を設定
uint64 bits = u.i;      // uint64としてビットパターンを読み出す

技術的詳細

このコミットは、Goランタイムにおける浮動小数点数(特にfloat64)の内部表現操作に関する二つの主要な改善を行っています。

  1. 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を区別しています。
  2. float64uint64 間の安全な変換:

    • これまでのコードでは、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): float32uint32 ビットパターンに安全に変換。
    • float64tobits(float64 f): float64uint64 ビットパターンに安全に変換。
    • float64frombits(uint64 i): uint64 ビットパターンを float64 に安全に変換。
  • 既存関数の変更:

    • isInf(float64 f, int32 sign): *(uint64*)&dfloat64tobits(f) に変更。
    • NaN(void): *(float64*)&uvnanfloat64frombits(uvnan) に変更。
    • isNaN(float64 f):
      • *(uint64*)&dfloat64tobits(f) に変更。
      • NaN判定ロジックを (uint32)(x>>32)==0x7FF00000 && !isInf(d, 0) から ((uint32)(x>>52) & 0x7FF) == 0x7FF && !isInf(f, 0) に変更。
    • Inf(int32 sign): *(float64*)&uvinf / *(float64*)&uvneginffloat64frombits(uvinf) / float64frombits(uvneginf) に変更。
    • frexp(float64 d, int32 *ep): *(uint64*)&dfloat64tobits(d) に、*(float64*)&xfloat64frombits(x) に変更。
    • ldexp(float64 d, int32 e): *(uint64*)&dfloat64tobits(d) に、*(float64*)&xfloat64frombits(x) に変更。
    • modf(float64 d, float64 *ip): *(uint64*)&dfloat64tobits(d) に、*(float64*)&xfloat64frombits(x) に変更。
    • sys·float32bits(float32 din, uint32 iou): *(uint32*)&diniou = float32tobits(din) に変更。
    • sys·float64bits(float64 din, uint64 iou): *(uint64*)&diniou = 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;
}

これらの関数は、float32float64 の値を共用体の浮動小数点メンバーに代入し、その後、共用体の整数メンバーからそのビットパターンを読み出すことで、厳密なエイリアシング規則に違反することなく、浮動小数点数の内部表現にアクセスすることを可能にします。これにより、コンパイラの最適化による潜在的なバグを回避し、コードの移植性と堅牢性を向上させています。

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 のような直接的なポインタキャストが、新しく導入された float64tobitsfloat64frombits 関数への呼び出しに置き換えられました。これにより、Goランタイム全体で浮動小数点数と整数ビットパターン間の変換が安全かつ標準に準拠した方法で行われるようになりました。

関連リンク

参考にした情報源リンク