[インデックス 13588] ファイルの概要
このコミットは、Go言語のmath
パッケージとruntime
パッケージにおけるNaN(Not a Number)の内部表現を更新し、特にARMアーキテクチャのVFPv2(Vector Floating Point Unit version 2)環境でのfloat64
からfloat32
への変換時のNaNの挙動の不整合を修正することを目的としています。具体的には、Goが内部的に使用するNaNのビットパターンを、GCCが使用するものと一致するように変更し、より堅牢な浮動小数点演算の互換性と正確性を確保しています。
コミット
commit 6e9506a7b45958665c3f48deecc8555f3ee2c42b
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Tue Aug 7 09:57:14 2012 +0800
math, runtime: use a NaN that matches gcc's
our old choice is not working properly at least on VFPv2 in
ARM1136JF-S (it's not preserved across float64->float32 conversions).
Fixes #3745.
R=dave, rsc
CC=golang-dev
https://golang.org/cl/6344078
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e9506a7b45958665c3f48deecc8555f3ee2c42b
元コミット内容
このコミットの元のメッセージは以下の通りです。
「math, runtime: use a NaN that matches gcc's 私たちの古い選択は、少なくともARM1136JF-SのVFPv2では正しく機能していません(float64からfloat32への変換で保持されません)。 Issue #3745を修正します。」
変更の背景
この変更の主な背景は、Go言語が内部的に定義していたNaN(Not a Number)のビットパターンが、特定のハードウェア環境、特にARMアーキテクチャのVFPv2(Vector Floating Point Unit version 2)を搭載したARM1136JF-Sプロセッサにおいて、float64
(倍精度浮動小数点数)からfloat32
(単精度浮動小数点数)への型変換時に正しく保持されないという問題が発生したためです。
IEEE 754浮動小数点標準では、NaNには「シグナリングNaN(sNaN)」と「クワイエットNaN(qNaN)」の2種類があります。sNaNは演算時に例外を発生させることを意図しており、qNaNは例外を発生させずに伝播することを意図しています。Goの以前のNaN表現は、おそらくsNaNに近いものであったか、あるいは特定のプラットフォームでsNaNのように振る舞い、型変換時にその「NaN性」が失われたり、予期せぬ値に変化したりする可能性がありました。
GCC(GNU Compiler Collection)のような広く使用されているコンパイラは、特定のNaNビットパターン、特にqNaNを生成・伝播させるための確立された慣行を持っています。GoがGCCと異なるNaN表現を使用していたため、Goで生成されたNaNがGCCでコンパイルされたコードや、GCCが期待するNaNの挙動に依存するハードウェア(この場合はARM VFPv2)と相互作用する際に問題が生じました。
この不整合は、Goプログラムが浮動小数点演算を行う際に、異なる精度間での型変換や、外部ライブラリとの連携において、予期せぬ結果やバグを引き起こす可能性がありました。このコミットは、GoのNaN表現をGCCの標準的なqNaN表現に合わせることで、これらの互換性問題を解決し、浮動小数点演算の信頼性を向上させることを目的としています。
前提知識の解説
浮動小数点数とIEEE 754標準
浮動小数点数は、非常に大きな数や非常に小さな数を表現するためのコンピュータの数値表現形式です。IEEE 754は、浮動小数点数の表現と演算に関する国際標準であり、ほとんどの現代のコンピュータシステムで採用されています。
IEEE 754では、浮動小数点数は通常、以下の3つの部分で構成されます。
- 符号部 (Sign): 数の正負を表します(0が正、1が負)。
- 指数部 (Exponent): 数の大きさを表します。
- 仮数部 (Fraction/Mantissa): 数の精度を表します。
NaN (Not a Number)
NaNは、IEEE 754標準で定義されている特殊な浮動小数点値の一つで、「非数」を意味します。これは、0/0、無限大/無限大、負の数の平方根など、数学的に未定義または表現不可能な演算の結果として生成されます。
NaNの重要な特性は以下の通りです。
- 比較演算: NaNは、自分自身を含め、いかなる値とも等しくありません(
NaN == NaN
は常にfalse
)。これは、NaNを検出するための一般的な方法です。 - 伝播: ほとんどの浮動小数点演算において、入力の一つがNaNであれば、結果もNaNになります。
- 種類:
- クワイエットNaN (qNaN): 演算時に例外を発生させずに伝播します。仮数部の最上位ビットが1に設定されます。
- シグナリングNaN (sNaN): 演算時に浮動小数点例外(無効演算例外など)を発生させることを意図しています。仮数部の最上位ビットが0に設定されます。
float64とfloat32
- float64 (倍精度浮動小数点数): IEEE 754 double-precision formatに対応し、通常64ビット(8バイト)で表現されます。符号部1ビット、指数部11ビット、仮数部52ビットで構成され、約15〜17桁の10進精度を持ちます。
- float32 (単精度浮動小数点数): IEEE 754 single-precision formatに対応し、通常32ビット(4バイト)で表現されます。符号部1ビット、指数部8ビット、仮数部23ビットで構成され、約6〜9桁の10進精度を持ちます。
float64
からfloat32
への変換は、精度が失われる可能性があるため、NaNのビットパターンが正しく変換されることが重要です。
ARM VFPv2 (Vector Floating Point Unit version 2)
VFPv2は、ARMアーキテクチャのプロセッサに搭載される浮動小数点演算ユニット(FPU)の一種です。これは、浮動小数点演算をハードウェアレベルで高速に実行するために設計されています。FPUは、IEEE 754標準に準拠した浮動小数点演算をサポートしますが、特定のFPUの実装やバージョンによっては、NaNの処理、特にsNaNの挙動や、異なる精度間での変換時のNaNの伝播に微妙な違いがあることがあります。ARM1136JF-Sは、VFPv2を搭載したARMプロセッサの一例です。
技術的詳細
このコミットの技術的な核心は、Go言語の内部でNaNを表現するために使用される64ビットの定数uvnan
の値を変更することにあります。
変更前: uvnan = 0x7FF0000000000001
変更後: uvnan = 0x7FF8000000000001
この変更は、src/pkg/math/bits.go
(Goのmath
パッケージのビット操作関連の定義)とsrc/pkg/runtime/float.c
(Goランタイムの浮動小数点関連のC言語コード)の両方で行われています。
これらの16進数値をIEEE 754倍精度浮動小数点数のビットパターンとして解析すると、以下のようになります。
-
0x7FF0000000000001
:- 符号ビット: 0 (正)
- 指数部:
7FF
(11ビット全てが1) - これはNaNまたは無限大を示します。 - 仮数部:
0000000000001
(52ビット) この仮数部の最上位ビットは0です。IEEE 754では、指数部が全て1で、仮数部が非ゼロの場合にNaNとなります。仮数部の最上位ビットが0の場合、これは**シグナリングNaN (sNaN)**として解釈されることが一般的です。sNaNは、演算時に例外を発生させることを意図しています。
-
0x7FF8000000000001
:- 符号ビット: 0 (正)
- 指数部:
7FF
(11ビット全てが1) - 仮数部:
8000000000001
(52ビット) この仮数部の最上位ビットは1です(8
の2進数は1000
)。IEEE 754では、指数部が全て1で、仮数部の最上位ビットが1の場合、これは**クワイエットNaN (qNaN)**として解釈されることが一般的です。qNaNは、例外を発生させずに伝播することを意図しています。
コミットメッセージにある「our old choice is not working properly at least on VFPv2 in ARM1136JF-S (it's not preserved across float64->float32 conversions)」という記述は、Goが以前使用していたsNaNのようなビットパターンが、ARM VFPv2環境でfloat64
からfloat32
への変換を行う際に、そのNaNとしての特性が失われたり、予期せぬ値に変化したりする問題があったことを示唆しています。これは、特定のハードウェアFPUがsNaNを異なる方法で処理したり、変換時にsNaNの「シグナリング」特性を維持できない場合に発生する可能性があります。
Goがuvnan
の値をqNaNのビットパターンに変更することで、GCCが生成するNaNの挙動と一致させ、異なる精度間での変換時や、特定のハードウェア環境でのNaNの伝播がより予測可能で標準に準拠するようになります。これにより、Goの浮動小数点演算の堅牢性と互換性が向上します。
また、src/pkg/math/all_test.go
にTestNaN
という新しいテスト関数が追加されています。このテストは、GoのNaN()
関数が返す値が本当にNaNであるか(f64 == f64
がfalse
であること)、そしてそのfloat64
のNaNをfloat32
に変換しても、結果がNaNとして正しく認識されるか(f32 == f32
がfalse
であること)を確認します。これは、まさにこのコミットが解決しようとしている問題、すなわちNaNの伝播と型変換時の保持を検証するためのものです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の3つのファイルにわたります。
-
src/pkg/math/all_test.go
: 新しいテスト関数TestNaN
が追加されました。--- a/src/pkg/math/all_test.go +++ b/src/pkg/math/all_test.go @@ -1693,6 +1693,17 @@ func alike(a, b float64) bool { return false } +func TestNaN(t *testing.T) { +\tf64 := NaN() +\tif f64 == f64 { +\t\tt.Fatalf("NaN() returns %g, expected NaN", f64) +\t} +\tf32 := float32(f64) +\tif f32 == f32 { +\t\tt.Fatalf("float32(NaN()) is %g, expected NaN", f32) +\t} +} + func TestAcos(t *testing.T) { for i := 0; i < len(vf); i++ { \ta := vf[i] / 10
-
src/pkg/math/bits.go
:uvnan
定数の値が変更されました。--- a/src/pkg/math/bits.go +++ b/src/pkg/math/bits.go @@ -5,7 +5,7 @@ package math const ( -\tuvnan = 0x7FF0000000000001 +\tuvnan = 0x7FF8000000000001 uvinf = 0x7FF0000000000000 uvneginf = 0xFFF0000000000000 mask = 0x7FF
-
src/pkg/runtime/float.c
: C言語で定義されているuvnan
変数の値が変更されました。--- a/src/pkg/runtime/float.c +++ b/src/pkg/runtime/float.c @@ -4,7 +4,7 @@ #include "runtime.h"\n \n-static\tuint64\tuvnan\t\t= 0x7FF0000000000001ULL;\n+static\tuint64\tuvnan\t\t= 0x7FF8000000000001ULL;\n static\tuint64\tuvinf\t\t= 0x7FF0000000000000ULL;\n static\tuint64\tuvneginf\t= 0xFFF0000000000000ULL;\n \n ```
コアとなるコードの解説
src/pkg/math/bits.go
と src/pkg/runtime/float.c
の変更
これらのファイルでは、Go言語が内部的にNaNを表現するために使用する64ビットの整数定数(または変数)uvnan
のビットパターンが変更されています。
-
変更前:
0x7FF0000000000001
このビットパターンは、IEEE 754倍精度浮動小数点数において、指数部が全て1(0x7FF
)であり、仮数部の最上位ビットが0であるNaNを表します。これは一般的に**シグナリングNaN (sNaN)**として解釈されます。sNaNは、浮動小数点演算でこの値が使用されると、通常、無効演算例外などの浮動小数点例外を発生させることを意図しています。 -
変更後:
0x7FF8000000000001
このビットパターンも、指数部が全て1(0x7FF
)であるNaNを表しますが、仮数部の最上位ビットが1(0x8
の2進数は1000
)である点が異なります。これは一般的に**クワイエットNaN (qNaN)**として解釈されます。qNaNは、例外を発生させずに演算結果として伝播することを意図しており、より「静かに」NaNを扱う場合に用いられます。
この変更の目的は、Goが生成するNaNの挙動を、GCCのような他の主要なコンパイラが生成する標準的なqNaNの挙動と一致させることです。これにより、特にARM VFPv2のような特定のハードウェア環境において、float64
からfloat32
への型変換時にNaNの特性が正しく保持されないという問題を解決します。sNaNは、その「シグナリング」特性のために、異なるFPU実装や型変換プロセスで予期せぬ挙動を示すことがありますが、qNaNはより一貫して伝播することが期待されます。
src/pkg/math/all_test.go
の変更
TestNaN
関数の追加は、このNaN表現の変更が意図した通りに機能するかを検証するための重要なテストです。
func TestNaN(t *testing.T) {
f64 := NaN() // GoのmathパッケージのNaN関数を呼び出し、float64のNaNを取得
if f64 == f64 { // NaNの基本的な特性(NaN == NaNがfalseであること)をチェック
t.Fatalf("NaN() returns %g, expected NaN", f64)
}
f32 := float32(f64) // float64のNaNをfloat32に変換
if f32 == f32 { // 変換後のfloat32のNaNも同様の特性を持つかチェック
t.Fatalf("float32(NaN()) is %g, expected NaN", f32)
}
}
このテストは以下の2つの重要な点を検証しています。
NaN()
関数が正しくNaNを生成しているか:NaN == NaN
がfalse
であるというNaNの定義的な特性を利用して、math.NaN()
が返す値が実際にNaNであることを確認します。もしf64 == f64
がtrue
であれば、それはNaNではないため、テストは失敗します。float64
からfloat32
への変換時にNaNが保持されるか: コミットの背景にある問題(「float64->float32変換で保持されない」)を直接検証します。float64
のNaNをfloat32
にキャストした後も、その値がNaNとしての特性(f32 == f32
がfalse
)を維持していることを確認します。これが成功することで、特定のハードウェア環境での互換性問題が解決されたことを示します。
このテストの追加により、NaNの内部表現の変更が、Goの浮動小数点演算の正確性と堅牢性に寄与していることが保証されます。
関連リンク
- Go Issue 3745 (元のGo issue trackerでの詳細情報があればここに記載されますが、現在の検索では直接的なリンクは見つかりませんでした。通常は
https://go.dev/issue/3745
のような形式になります。) - Go Change List 6344078: https://golang.org/cl/6344078 (GoのコードレビューシステムGerritのリンク)
参考にした情報源リンク
- IEEE 754標準に関する情報 (例: Wikipediaの「IEEE 754」エントリ)
- 浮動小数点数のNaNに関する情報 (例: 各プログラミング言語のドキュメントや浮動小数点演算に関する技術記事)
- ARM VFPv2に関する情報 (例: ARMの公式ドキュメントや関連する技術フォーラム)
- GCCの浮動小数点処理に関する情報 (例: GCCのドキュメントや関連する技術記事)
- Go言語の
math
パッケージのドキュメント: https://pkg.go.dev/math - Go言語の
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime