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

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

このコミットは、Go言語のテストスイートにおける浮動小数点リテラルのテスト (test/float_lit2.go および test/float_lit3.go) を大幅に書き換えるものです。特に、float32 および float64 の表現可能な値の境界付近での丸め動作をより厳密にテストするように変更されています。以前のテストが厳密な値のチェックに焦点を当てていたのに対し、この変更では算術的に導出されたテストケースを用いて、境界値での丸め処理の正確性を検証することを目指しています。

コミット

commit 2de449e7a04a571a4aefefc83601802af214ea0a
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 21 17:12:06 2014 -0400

    test/float_lit2.go: rewrite to test values near boundaries
    
    Add larger comment explaining testing methodology,
    and derive tests arithmetically.
    
    (These tests are checking rounding again; the derived
    tests they replace were checking exact values.)
    
    LGTM=r, gri
    R=gri, r
    CC=golang-codereviews
    https://golang.org/cl/100660044

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

https://github.com/golang/go/commit/2de449e7a04a571a4aefefc83601802af214ea0a

元コミット内容

test/float_lit2.go: rewrite to test values near boundaries Add larger comment explaining testing methodology, and derive tests arithmetically. (These tests are checking rounding again; the derived tests they replace were checking exact values.)

変更の背景

Go言語のコンパイラは、浮動小数点リテラルを処理する際に、IEEE 754標準に厳密に従って丸め処理を行う必要があります。特に、float32float64といった型で表現できる最大値や最小値の境界付近では、わずかな数値の違いが丸め結果に大きな影響を与える可能性があります。

このコミット以前のテストは、特定の浮動小数点リテラルが正確に表現されるかどうかを検証するものでしたが、境界付近での丸め動作、特に「最近接偶数への丸め(round half to even)」といったIEEE 754の重要な側面を十分にカバーしていませんでした。

この変更の背景には、以下の目的があります。

  1. 丸め処理の厳密な検証: 浮動小数点数の境界付近での丸め処理が、IEEE 754標準に則って正確に行われていることを保証するため。
  2. テストの堅牢性向上: 特定のハードコードされた値に依存するのではなく、算術的に導出された値を使用することで、テストケースの網羅性と堅牢性を高める。これにより、将来的な浮動小数点表現の変更や最適化に対しても、テストが適切に機能し続けることを期待できる。
  3. Goコンパイラの精度保証: Go言語の仕様では、コンパイル時の浮動小数点定数式は、少なくとも256ビットの精度を持つ内部表現で評価されることが求められています。この高精度な内部表現が、最終的なfloat32float64への変換時に正しく丸められることを確認する。

前提知識の解説

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

コンピュータにおける浮動小数点数は、IEEE 754標準によって定義されています。これは、数値を符号部、指数部、仮数部(または有効数字部)に分けて表現するものです。

  • float32 (単精度浮動小数点数): 32ビットで表現され、1ビットの符号、8ビットの指数、23ビットの仮数で構成されます。これにより、約7桁の10進精度と、約10^-38から10^38までの範囲を表現できます。
  • float64 (倍精度浮動小数点数): 64ビットで表現され、1ビットの符号、11ビットの指数、52ビットの仮数で構成されます。これにより、約15-17桁の10進精度と、約10^-308から10^308までの範囲を表現できます。

浮動小数点数は、その性質上、すべての実数を正確に表現できるわけではありません。特に、2進数で正確に表現できない10進数(例: 0.1)や、表現可能な範囲を超える/下回る数値は、丸められたり、オーバーフロー/アンダーフローとして扱われたりします。

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

IEEE 754標準では、数値が正確に表現できない場合にどのように丸めるかを定義しています。最も一般的な丸めモードは「最近接偶数への丸め(Round half to even)」です。

  • 最近接偶数への丸め:
    • もし丸め対象の数値が2つの表現可能な数値の中間にある場合、仮数部の最下位ビットが偶数になる方へ丸めます。
    • 例: 2.5 (2進数で正確に表現できない) を整数に丸める場合、2と3の中間ですが、2が偶数なので2に丸めます。3.5は3と4の中間ですが、4が偶数なので4に丸めます。
    • この丸めモードは、統計的なバイアスを最小限に抑えるために採用されています。

ULP (Unit in the Last Place)

ULPは「Unit in the Last Place」の略で、浮動小数点数の精度を測るための重要な概念です。ある浮動小数点数Xに対して、ULP(X)は、Xの次に大きい(または小さい)表現可能な浮動小数点数との差を表します。

  • ULPは、数値の絶対値が大きくなるにつれて大きくなります。これは、浮動小数点数の表現が、指数部によってスケールされるためです。
  • 浮動小数点演算の精度は、通常、ULPの数で測定されます。例えば、「1 ULP以内の誤差」は、計算結果が正確な値から1 ULP分しか離れていないことを意味します。

Go言語における定数式の精度

Go言語の仕様では、コンパイル時に評価される浮動小数点定数式は、IEEE 754のfloat64よりも高い精度で計算されることが保証されています。具体的には、少なくとも256ビットの仮数部を持つ内部表現が使用されます。これにより、ソースコードに記述された浮動小数点リテラルが、最終的なfloat32float64型に変換される前に、非常に高い精度で評価され、その後の丸め処理が正確に行われることが期待されます。

技術的詳細

このコミットは、test/float_lit2.gotest/float_lit3.goの2つのテストファイルを変更し、浮動小数点数の境界値テストを強化しています。

test/float_lit2.go の変更点

このファイルは、Goコンパイラが浮動小数点リテラルをfloat32float64に変換する際の丸め動作を検証するためのものです。

  1. 定数の再定義と算術的導出:

    • 以前はm32bits, e32maxなどのビットレベルの定数と、それらから直接計算されたmaxFloat32_0などの最大値が使われていました。
    • 新しいコードでは、two24, two53, two64, two128, two256, two512, two768, two1024といった2のべき乗の定数を導入し、これらを用いてulp32, max32, ulp64, max64を算術的に導出しています。
      • ulp32 = two128 / two24: float32の最大値付近でのULPを計算。float32の仮数部は24ビット(暗黙の先頭ビットを含む)なので、2^128 / 2^24 = 2^104が最大値付近のULPに相当します。
      • max32 = two128 - ulp32: float32の最大表現可能値を計算。これは2^128 - 2^104であり、IEEE 754 float32の最大有限値 (1 + (1 - 2^-23)) * 2^127 に相当します。
      • ulp64 = two1024 / two53: float64の最大値付近でのULPを計算。float64の仮数部は53ビット(暗黙の先頭ビットを含む)なので、2^1024 / 2^53 = 2^971が最大値付近のULPに相当します。
      • max64 = two1024 - ulp64: float64の最大表現可能値を計算。これは2^1024 - 2^971であり、IEEE 754 float64の最大有限値 (1 + (1 - 2^-52)) * 2^1023 に相当します。
    • これらの定数を用いることで、テストケースが浮動小数点数の内部構造とより密接に連携し、境界付近の挙動を正確に捉えることができます。
  2. 詳細なコメントの追加:

    • float32の最大値付近での丸め動作に関する詳細な説明が追加されました。
    • f₁ = (1+(1-2⁻²³))×2¹²⁷ = (1-2⁻²⁴)×2¹²⁸ = 2¹²⁸ - 2¹⁰⁴ が最大の正確なfloat32値であること。
    • 次のfloat32f₂ = (1+1)×2¹²⁷ = 1×2¹²⁸は範囲外であること。
    • f₁f₂の中間点 (f₁+f₂)/2 = 2¹²⁸ - 2¹⁰⁵f₂に丸められ、範囲外として拒否されること。
    • f₁が奇数仮数であるため、中間点がf₂に丸められるという「最近接偶数への丸め」の具体的な適用例が示されています。
  3. cvt スライスの構造変更とテストロジックの刷新:

    • cvtスライスは、テスト対象の浮動小数点リテラルとその期待される結果を格納します。
    • 新しいcvtエントリは、bits (期待されるビット表現), exact (期待される正確な浮動小数点値), approx (テスト対象の浮動小数点リテラル式), text (リテラル式の文字列表現) を持ちます。
    • approxフィールドには、max32 - ulp32/2 + ulp32/two64 のような、max32ulp32といった算術的に導出された定数を用いた複雑な式が記述されています。これらの式は、float32float64の表現可能な境界のすぐ内側や外側、あるいは中間点付近の値を生成するように設計されています。
    • main関数では、ulp64ulp32の計算が正しいかを確認した後、cvtスライスをループし、各エントリについて以下の検証を行います。
      • c.exactのビット表現がc.bitsと一致するか。
      • c.approxc.exactと一致し、かつc.approxのビット表現がc.bitsと一致するか。
    • これにより、リテラル式が期待される浮動小数点値に正確に丸められ、そのビット表現も正しいことを検証します。
  4. ヘルパー関数の追加:

    • bits(x interface{}) interface{}: float32またはfloat64のビット表現(uint32またはuint64)を返します。
    • fromBits(b uint64, x interface{}) interface{}: ビット表現からfloat32またはfloat64を再構築します。
    • これらの関数は、浮動小数点値とビット表現の間で変換を行い、テストの検証を容易にします。

test/float_lit3.go の変更点

このファイルは、浮動小数点リテラルがオーバーフローするかどうかを検証するためのものです。

  1. 定数の共有: float_lit2.goで定義されたtwoXX, ulpXX, maxXXといった算術的導出定数が、このファイルでも共有されるようになりました。
  2. オーバーフローテストの更新:
    • xスライス内のテストケースが、新しいmax32, ulp32, max64, ulp64定数を用いて書き換えられました。
    • 例えば、float32(max32 + ulp32/2)float32の最大値を超過するため、オーバーフローエラーとなるべきです。
    • float32(max32 + ulp32/2 - 1) のように、Goコンパイラの内部256ビット精度では表現可能だが、最終的なfloat32への丸め時にオーバーフローしない(またはオーバーフローする)境界値をテストしています。
    • 特にfloat64のテストケースでは、GC_ERRORというコメントが追加されています。これは、Goコンパイラ(gc)の内部浮動小数点表現が1024ビットよりも短い場合(実際には256ビット)、max64 + ulp64/2 - 1のような値とmax64 + ulp64/2を区別できない可能性があることを示唆しています。そのため、max64 + ulp64/2 - 1であってもオーバーフローエラーを出すことを許容しています。これは、Goコンパイラの内部精度が仕様で定められた256ビット以上であれば十分であり、それ以上の精度を仮定しないという現実的なアプローチを示しています。

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

test/float_lit2.go

--- a/test/float_lit2.go
+++ b/test/float_lit2.go
@@ -10,89 +10,155 @@ package main
 
 import (
 	"fmt"
+	"math"
 )
 
+// The largest exact float32 is f₁ = (1+(1-2²³))×2¹²⁷ = (1-2²⁴)×2¹²⁸ = 2¹²⁸ - 2¹⁰⁴.
+// The next float32 would be f₂ = (1+1)×2¹²⁷ = 1×2¹²⁸, except that exponent is out of range.
+// Float32 conversion rounds to the nearest float32, rounding to even mantissa:
+// between f₁ and f₂, values closer to f₁ round to f₁and values closer to f₂ are rejected as out of range.
+// f₁ is an odd mantissa, so the halfway point (f₁+f₂)/2 rounds to f₂ and is rejected.
+// The halfway point (f₁+f₂)/2 = 2¹²⁸ - 2¹⁰⁵.
+//
+// The same is true of float64, with different constants: s/24/53/ and s/128/1024/.
+
 const (
 -	m32bits   = 23  // number of float32 mantissa bits
 -	e32max    = 127 // max. float32 exponent
 -	maxExp32  = e32max - m32bits
 -	maxMant32 = 1<<(m32bits+1) - 1
 -
 -	maxFloat32_0 = (maxMant32 - 0) << maxExp32
 -	maxFloat32_1 = (maxMant32 - 1) << maxExp32
 -	maxFloat32_2 = (maxMant32 - 2) << maxExp32
++	two24   = 1.0 * (1 << 24)
++	two53   = 1.0 * (1 << 53)
++	two64   = 1.0 * (1 << 64)
++	two128  = two64 * two64
++	two256  = two128 * two128
++	two512  = two256 * two256
++	two768  = two512 * two256
++	two1024 = two512 * two512
++
++	ulp32 = two128 / two24
++	max32 = two128 - ulp32
++
++	ulp64 = two1024 / two53
++	max64 = two1024 - ulp64
 )
 
-func init() {
-	if maxExp32 != 104 {
-		panic("incorrect maxExp32")
-	}
-	if maxMant32 != 16777215 {
-		panic("incorrect maxMant32")
-	}
-	if maxFloat32_0 != 340282346638528859811704183484516925440 {
-		panic("incorrect maxFloat32_0")
-	}
-+var cvt = []struct {
-+	bits   uint64 // keep us honest
-+	exact  interface{}
-+	approx interface{}
-+	text   string
-+}{
-+	// 0
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32 - ulp32/2), "max32 - ulp32 - ulp32/2"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32), "max32 - ulp32"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32/2), "max32 - ulp32/2"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32 + ulp32/2), "max32 - ulp32 + ulp32/2"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + ulp32/two64), "max32 - ulp32 + ulp32/2 + ulp32/two64"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + ulp32/two64), "max32 - ulp32/2 + ulp32/two64"},
-+	{0x7f7fffff, float32(max32), float32(max32), "max32"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - ulp32/two64), "max32 + ulp32/2 - ulp32/two64"},
-+
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32 - ulp32/2)), "-(max32 - ulp32 - ulp32/2)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32)), "-(max32 - ulp32)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32/2)), "-(max32 - ulp32/2)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32 + ulp32/2)), "-(max32 - ulp32 + ulp32/2)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + ulp32/two64)), "-(max32 - ulp32 + ulp32/2 + ulp32/two64)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + ulp32/two64)), "-(max32 - ulp32/2 + ulp32/two64)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32)), "-(max32)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - ulp32/two64)), "-(max32 + ulp32/2 - ulp32/two64)"},
-+
-+	// These are required to work: according to the Go spec, the internal float mantissa must be at least 256 bits,
-+	// and these expressions can be represented exactly with a 256-bit mantissa.
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + 1), "max32 - ulp32 + ulp32/2 + 1"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + 1), "max32 - ulp32/2 + 1"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - 1), "max32 + ulp32/2 - 1"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + 1)), "-(max32 - ulp32 + ulp32/2 + 1)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + 1)), "-(max32 - ulp32/2 + 1)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - 1)), "-(max32 + ulp32/2 - 1)"},
-+
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + 1/two128), "max32 - ulp32 + ulp32/2 + 1/two128"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + 1/two128), "max32 - ulp32/2 + 1/two128"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - 1/two128), "max32 + ulp32/2 - 1/two128"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + 1/two128)), "-(max32 - ulp32 + ulp32/2 + 1/two128)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + 1/two128)), "-(max32 - ulp32/2 + 1/two128)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - 1/two128)), "-(max32 + ulp32/2 - 1/two128)"},
-+
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64 - ulp64/2), "max64 - ulp64 - ulp64/2"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64), "max64 - ulp64"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64/2), "max64 - ulp64/2"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64 + ulp64/2), "max64 - ulp64 + ulp64/2"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64 + ulp64/2 + ulp64/two64), "max64 - ulp64 + ulp64/2 + ulp64/two64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64/2 + ulp64/two64), "max64 - ulp64/2 + ulp64/two64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64), "max64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 + ulp64/2 - ulp64/two64), "max64 + ulp64/2 - ulp64/two64"},
-+
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64 - ulp64/2)), "-(max64 - ulp64 - ulp64/2)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64)), "-(max64 - ulp64)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64/2)), "-(max64 - ulp64/2)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64 + ulp64/2)), "-(max64 - ulp64 + ulp64/2)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64 + ulp64/2 + ulp64/two64)), "-(max64 - ulp64 + ulp64/2 + ulp64/two64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64/2 + ulp64/two64)), "-(max64 - ulp64/2 + ulp64/two64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64)), "-(max64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 + ulp64/2 - ulp64/two64)), "-(max64 + ulp64/2 - ulp64/two64)"},
-+
-+	// These are required to work.
-+	// The mantissas are exactly 256 bits.
-+	// max64 is just below 2¹⁰²⁴ so the bottom bit we can use is 2⁷⁶⁸.
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64 + ulp64/2 + two768), "max64 - ulp64 + ulp64/2 + two768"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64/2 + two768), "max64 - ulp64/2 + two768"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 + ulp64/2 - two768), "max64 + ulp64/2 - two768"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64 + ulp64/2 + two768)), "-(max64 - ulp64 + ulp64/2 + two768)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64/2 + two768)), "-(max64 - ulp64/2 + two768)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 + ulp64/2 - two768)), "-(max64 + ulp64/2 - two768)"},
+var bugged = false
  
-const (
-	m64bits   = 52   // number of float64 mantissa bits
-	e64max    = 1023 // max. float64 exponent
-	maxExp64  = e64max - m64bits
-	maxMant64 = 1<<(m64bits+1) - 1
-
-	// These expressions are not permitted due to implementation restrictions.
-	// maxFloat64_0 = (maxMant64-0) << maxExp64
-	// maxFloat64_1 = (maxMant64-1) << maxExp64
-	// maxFloat64_2 = (maxMant64-2) << maxExp64
-
-	// These equivalent values were computed using math/big.
-	maxFloat64_0 = 1.7976931348623157e308
-	maxFloat64_1 = 1.7976931348623155e308
-	maxFloat64_2 = 1.7976931348623153e308
-)
+func bug() {
+	if !bugged {
+		bugged = true
+		fmt.Println("BUG")
+	}
+}
  
-func init() {
-	if maxExp64 != 971 {
-		panic("incorrect maxExp64")
-	}
-	if maxMant64 != 9007199254740991 {
-		panic("incorrect maxMant64")
-	}
-}
- 
-var cvt = []struct {
-	val    interface{}
-	binary string
-}{
-
-	{float32(maxFloat32_0), fmt.Sprintf("%dp+%d", int32(maxMant32-0), maxExp32)},
-	{float32(maxFloat32_1), fmt.Sprintf("%dp+%d", int32(maxMant32-1), maxExp32)},
-	{float32(maxFloat32_2), fmt.Sprintf("%dp+%d", int32(maxMant32-2), maxExp32)},
-+func main() {
-+	u64 := math.Float64frombits(0x7fefffffffffffff) - math.Float64frombits(0x7feffffffffffffe)
-+	if ulp64 != u64 {
-+		bug()
-+		fmt.Printf("ulp64=%g, want %g", ulp64, u64)
-+	}
  
-	{float64(maxFloat64_0), fmt.Sprintf("%dp+%d", int64(maxMant64-0), maxExp64)},
-	{float64(maxFloat64_1), fmt.Sprintf("%dp+%d", int64(maxMant64-1), maxExp64)},
-	{float64(maxFloat64_2), fmt.Sprintf("%dp+%d", int64(maxMant64-2), maxExp64)},
-+	u32 := math.Float32frombits(0x7f7fffff) - math.Float32frombits(0x7f7ffffe)
-+	if ulp32 != u32 {
-+		bug()
-+		fmt.Printf("ulp32=%g, want %g", ulp32, u32)
-+	}
  
-	{float32(-maxFloat32_0), fmt.Sprintf("-%dp+%d", int32(maxMant32-0), maxExp32)},
-	{float32(-maxFloat32_1), fmt.Sprintf("-%dp+%d", int32(maxMant32-1), maxExp32)},
-	{float32(-maxFloat32_2), fmt.Sprintf("-%dp+%d", int32(maxMant32-2), maxExp32)},
-+	for _, c := range cvt {
-+		if bits(c.exact) != c.bits {
-+			bug()
-+			fmt.Printf("%s: inconsistent table: bits=%#x (%g) but exact=%g (%#x)\\n", c.text, c.bits, fromBits(c.bits, c.exact), c.exact, bits(c.exact))
-+		}
-+		if c.approx != c.exact || bits(c.approx) != c.bits {
-+			bug()
-+			fmt.Printf("%s: have %g (%#x) want %g (%#x)\\n", c.text, c.approx, bits(c.approx), c.exact, c.bits)
-+		}
-+	}
-+}\n 
-	{float64(-maxFloat64_0), fmt.Sprintf("-%dp+%d", int64(maxMant64-0), maxExp64)},
-	{float64(-maxFloat64_1), fmt.Sprintf("-%dp+%d", int64(maxMant64-1), maxExp64)},
-	{float64(-maxFloat64_2), fmt.Sprintf("-%dp+%d", int64(maxMant64-2), maxExp64)},
-+func bits(x interface{}) interface{} {
-+	switch x := x.(type) {
-+	case float32:
-+		return uint64(math.Float32bits(x))
-+	case float64:
-+		return math.Float64bits(x)
-+	}
-+	return 0
  }
  
-func main() {
-	bug := false
-	for i, c := range cvt {
-		s := fmt.Sprintf("%b", c.val)
-		if s != c.binary {
-			if !bug {
-				bug = true
-				fmt.Println("BUG")
-			}
-			fmt.Printf("#%d: have %s, want %s\\n", i, s, c.binary)
-		}
-	}
-+func fromBits(b uint64, x interface{}) interface{} {
-+	switch x.(type) {
-+	case float32:
-+		return math.Float32frombits(uint32(b))
-+	case float64:
-+		return math.Float64frombits(b)
  	}
-+	return "?"
  }

test/float_lit3.go

--- a/test/float_lit3.go
+++ b/test/float_lit3.go
@@ -8,21 +8,41 @@
  
  package main
  
+// See float_lit2.go for motivation for these values.
+const (
+	two24   = 1.0 * (1 << 24)
+	two53   = 1.0 * (1 << 53)
+	two64   = 1.0 * (1 << 64)
+	two128  = two64 * two64
+	two256  = two128 * two128
+	two512  = two256 * two256
+	two768  = two512 * two256
+	two1024 = two512 * two512
+
+	ulp32 = two128 / two24
+	max32 = two128 - ulp32
+
+	ulp64 = two1024 / two53
+	max64 = two1024 - ulp64
+)
+
  var x = []interface{}{
 -	float32(-340282356779733661637539395458142568448), // ERROR "constant -3\.40282e\+38 overflows float32"
 -	float32(-340282356779733661637539395458142568447),
 -	float32(-340282326356119256160033759537265639424),
 -	float32(340282326356119256160033759537265639424),
 -	float32(340282356779733661637539395458142568447),
 -	float32(340282356779733661637539395458142568448), // ERROR "constant 3\.40282e\+38 overflows float32"
 -	-1e1000, // ERROR "constant -1\.00000e\+1000 overflows float64"
 -	float64(-1.797693134862315907937289714053e+308), // ERROR "constant -1\.79769e\+308 overflows float64"
 -	float64(-1.797693134862315807937289714053e+308),
 -	float64(-1.797693134862315708145274237317e+308),
 -	float64(-1.797693134862315608353258760581e+308),
 -	float64(1.797693134862315608353258760581e+308),
 -	float64(1.797693134862315708145274237317e+308),
 -	float64(1.797693134862315807937289714053e+308),
 -	float64(1.797693134862315907937289714053e+308), // ERROR "constant 1\.79769e\+308 overflows float64"
 -	1e1000, // ERROR "constant 1\.00000e\+1000 overflows float64"
++	float32(max32 + ulp32/2 - 1),             // ok
++	float32(max32 + ulp32/2 - two128/two256), // ok
++	float32(max32 + ulp32/2),                 // ERROR "constant 3\.40282e\+38 overflows float32"
++
++	float32(-max32 - ulp32/2 + 1),             // ok
++	float32(-max32 - ulp32/2 + two128/two256), // ok
++	float32(-max32 - ulp32/2),                 // ERROR "constant -3\.40282e\+38 overflows float32"
++
++	// If the compiler's internal floating point representation
++	// is shorter than 1024 bits, it cannot distinguish max64+ulp64/2-1 and max64+ulp64/2.
++	// gc uses fewer than 1024 bits, so allow it to print the overflow error for the -1 case.
++	float64(max64 + ulp64/2 - two1024/two256), // ok
++	float64(max64 + ulp64/2 - 1),              // GC_ERROR "constant 1\.79769e\+308 overflows float64"
++	float64(max64 + ulp64/2),                  // ERROR "constant 1\.79769e\+308 overflows float64"
++
++	float64(-max64 - ulp64/2 + two1024/two256), // ok
++	float64(-max64 - ulp64/2 + 1),              // GC_ERROR "constant -1\.79769e\+308 overflows float64"
++	float64(-max64 - ulp64/2),                  // ERROR "constant -1\.79769e\+308 overflows float64"
  }

コアとなるコードの解説

test/float_lit2.go の解説

  • 定数定義の変更: 以前のビット操作に基づく定数定義は、浮動小数点数の内部表現を直接操作するものでしたが、新しいtwoXX系の定数とulpXX, maxXXの算術的導出は、より抽象的で数学的なアプローチを取っています。これにより、テストの意図が明確になり、浮動小数点数の性質に基づいたテストケースの生成が容易になります。
  • cvt スライスの強化: cvtスライス内の各エントリは、approxフィールドに複雑な算術式を持つことで、float32float64の表現可能な境界の非常に近い位置にある値を生成します。例えば、max32 - ulp32/2は、max32から半ULPだけ小さい値を意味し、丸め処理がどのように行われるかを厳密にテストします。ulp32/two64のような非常に小さな値を加減することで、丸め方向を決定する境界(中間点)のすぐ隣の値をテストし、Goコンパイラの丸めロジックがIEEE 754標準に厳密に従っていることを確認します。
  • テストロジックの改善: main関数内のループは、approx式がexact値に正しく丸められ、その結果のビット表現が期待されるbitsと一致するかを検証します。これは、Goコンパイラが浮動小数点リテラルを解析し、内部の高精度表現で計算し、最終的にターゲット型に丸めるという一連のプロセス全体をテストするものです。特に、bits(c.exact) != c.bitsのチェックは、テストテーブル自体が矛盾していないかを確認する自己検証の役割も果たします。
  • 詳細なコメント: float32の境界に関するコメントは、IEEE 754の「最近接偶数への丸め」がどのように機能するかを具体的に説明しており、テストケースの背後にある数学的・技術的根拠を明確にしています。

test/float_lit3.go の解説

  • オーバーフローテストの精度向上: float_lit3.goでは、float_lit2.goで定義されたmaxXXulpXXといった定数を利用することで、オーバーフローテストの精度が大幅に向上しました。以前はハードコードされた大きな数値が使われていましたが、新しいテストケースは、表現可能な最大値のすぐ外側や、Goコンパイラの内部精度と最終的な型変換の境界を突くような値を生成します。
  • GC_ERROR の導入: GC_ERRORコメントは、Goコンパイラ(gc)が内部で256ビットの精度を使用しているため、float64の最大値付近で非常に近い2つの値(例: max64 + ulp64/2 - 1max64 + ulp64/2)を区別できない可能性があることを示しています。Goの仕様は少なくとも256ビットの精度を要求していますが、それ以上の精度を保証しているわけではありません。したがって、このコメントは、特定のコンパイラ実装の挙動を許容しつつ、それでもなおオーバーフローが正しく検出されることを確認するためのものです。これは、Goコンパイラの浮動小数点処理が、仕様の範囲内で堅牢であることを示す重要な側面です。

これらの変更により、Goコンパイラが浮動小数点リテラルを処理する際の丸め動作とオーバーフロー検出が、IEEE 754標準およびGo言語の仕様に厳密に準拠していることを、より包括的かつ堅牢に検証できるようになりました。

関連リンク

参考にした情報源リンク

  • IEEE 754浮動小数点数標準に関する情報 (例: Wikipedia, 各種技術ドキュメント)
  • Go言語の仕様 (特に定数と浮動小数点数に関するセクション)
  • ULP (Unit in the Last Place) に関する情報```markdown

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

このコミットは、Go言語のテストスイートにおける浮動小数点リテラルのテスト (test/float_lit2.go および test/float_lit3.go) を大幅に書き換えるものです。特に、float32 および float64 の表現可能な値の境界付近での丸め動作をより厳密にテストするように変更されています。以前のテストが厳密な値のチェックに焦点を当てていたのに対し、この変更では算術的に導出されたテストケースを用いて、境界値での丸め処理の正確性を検証することを目指しています。

コミット

commit 2de449e7a04a571a4aefefc83601802af214ea0a
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 21 17:12:06 2014 -0400

    test/float_lit2.go: rewrite to test values near boundaries
    
    Add larger comment explaining testing methodology,
    and derive tests arithmetically.
    
    (These tests are checking rounding again; the derived
    tests they replace were checking exact values.)
    
    LGTM=r, gri
    R=gri, r
    CC=golang-codereviews
    https://golang.org/cl/100660044

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

https://github.com/golang/go/commit/2de449e7a04a571a4aefefc83601802af214ea0a

元コミット内容

test/float_lit2.go: rewrite to test values near boundaries Add larger comment explaining testing methodology, and derive tests arithmetically. (These tests are checking rounding again; the derived tests they replace were checking exact values.)

変更の背景

Go言語のコンパイラは、浮動小数点リテラルを処理する際に、IEEE 754標準に厳密に従って丸め処理を行う必要があります。特に、float32float64といった型で表現できる最大値や最小値の境界付近では、わずかな数値の違いが丸め結果に大きな影響を与える可能性があります。

このコミット以前のテストは、特定の浮動小数点リテラルが正確に表現されるかどうかを検証するものでしたが、境界付近での丸め動作、特に「最近接偶数への丸め(round half to even)」といったIEEE 754の重要な側面を十分にカバーしていませんでした。

この変更の背景には、以下の目的があります。

  1. 丸め処理の厳密な検証: 浮動小数点数の境界付近での丸め処理が、IEEE 754標準に則って正確に行われていることを保証するため。
  2. テストの堅牢性向上: 特定のハードコードされた値に依存するのではなく、算術的に導出された値を使用することで、テストケースの網羅性と堅牢性を高める。これにより、将来的な浮動小数点表現の変更や最適化に対しても、テストが適切に機能し続けることを期待できる。
  3. Goコンパイラの精度保証: Go言語の仕様では、コンパイル時の浮動小数点定数式は、少なくとも256ビットの精度を持つ内部表現で評価されることが求められています。この高精度な内部表現が、最終的なfloat32float64への変換時に正しく丸められることを確認する。

前提知識の解説

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

コンピュータにおける浮動小数点数は、IEEE 754標準によって定義されています。これは、数値を符号部、指数部、仮数部(または有効数字部)に分けて表現するものです。

  • float32 (単精度浮動小数点数): 32ビットで表現され、1ビットの符号、8ビットの指数、23ビットの仮数で構成されます。これにより、約7桁の10進精度と、約10^-38から10^38までの範囲を表現できます。
  • float64 (倍精度浮動小数点数): 64ビットで表現され、1ビットの符号、11ビットの指数、52ビットの仮数で構成されます。これにより、約15-17桁の10進精度と、約10^-308から10^308までの範囲を表現できます。

浮動小数点数は、その性質上、すべての実数を正確に表現できるわけではありません。特に、2進数で正確に表現できない10進数(例: 0.1)や、表現可能な範囲を超える/下回る数値は、丸められたり、オーバーフロー/アンダーフローとして扱われたりします。

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

IEEE 754標準では、数値が正確に表現できない場合にどのように丸めるかを定義しています。最も一般的な丸めモードは「最近接偶数への丸め(Round half to even)」です。

  • 最近接偶数への丸め:
    • もし丸め対象の数値が2つの表現可能な数値の中間にある場合、仮数部の最下位ビットが偶数になる方へ丸めます。
    • 例: 2.5 (2進数で正確に表現できない) を整数に丸める場合、2と3の中間ですが、2が偶数なので2に丸めます。3.5は3と4の中間ですが、4が偶数なので4に丸めます。
    • この丸めモードは、統計的なバイアスを最小限に抑えるために採用されています。

ULP (Unit in the Last Place)

ULPは「Unit in the Last Place」の略で、浮動小数点数の精度を測るための重要な概念です。ある浮動小数点数Xに対して、ULP(X)は、Xの次に大きい(または小さい)表現可能な浮動小数点数との差を表します。

  • ULPは、数値の絶対値が大きくなるにつれて大きくなります。これは、浮動小数点数の表現が、指数部によってスケールされるためです。
  • 浮動小数点演算の精度は、通常、ULPの数で測定されます。例えば、「1 ULP以内の誤差」は、計算結果が正確な値から1 ULP分しか離れていないことを意味します。

Go言語における定数式の精度

Go言語の仕様では、コンパイル時に評価される浮動小数点定数式は、IEEE 754のfloat64よりも高い精度で計算されることが保証されています。具体的には、少なくとも256ビットの仮数部を持つ内部表現が使用されます。これにより、ソースコードに記述された浮動小数点リテラルが、最終的なfloat32float64型に変換される前に、非常に高い精度で評価され、その後の丸め処理が正確に行われることが期待されます。

技術的詳細

このコミットは、test/float_lit2.gotest/float_lit3.goの2つのテストファイルを変更し、浮動小数点数の境界値テストを強化しています。

test/float_lit2.go の変更点

このファイルは、Goコンパイラが浮動小数点リテラルをfloat32float64に変換する際の丸め動作を検証するためのものです。

  1. 定数の再定義と算術的導出:

    • 以前はm32bits, e32maxなどのビットレベルの定数と、それらから直接計算されたmaxFloat32_0などの最大値が使われていました。
    • 新しいコードでは、two24, two53, two64, two128, two256, two512, two768, two1024といった2のべき乗の定数を導入し、これらを用いてulp32, max32, ulp64, max64を算術的に導出しています。
      • ulp32 = two128 / two24: float32の最大値付近でのULPを計算。float32の仮数部は24ビット(暗黙の先頭ビットを含む)なので、2^128 / 2^24 = 2^104が最大値付近のULPに相当します。
      • max32 = two128 - ulp32: float32の最大表現可能値を計算。これは2^128 - 2^104であり、IEEE 754 float32の最大有限値 (1 + (1 - 2^-23)) * 2^127 に相当します。
      • ulp64 = two1024 / two53: float64の最大値付近でのULPを計算。float64の仮数部は53ビット(暗黙の先頭ビットを含む)なので、2^1024 / 2^53 = 2^971が最大値付近のULPに相当します。
      • max64 = two1024 - ulp64: float64の最大表現可能値を計算。これは2^1024 - 2^971であり、IEEE 754 float64の最大有限値 (1 + (1 - 2^-52)) * 2^1023 に相当します。
    • これらの定数を用いることで、テストケースが浮動小数点数の内部構造とより密接に連携し、境界付近の挙動を正確に捉えることができます。
  2. 詳細なコメントの追加:

    • float32の最大値付近での丸め動作に関する詳細な説明が追加されました。
    • f₁ = (1+(1-2⁻²³))×2¹²⁷ = (1-2⁻²⁴)×2¹²⁸ = 2¹²⁸ - 2¹⁰⁴ が最大の正確なfloat32値であること。
    • 次のfloat32f₂ = (1+1)×2¹²⁷ = 1×2¹²⁸は範囲外であること。
    • f₁f₂の中間点 (f₁+f₂)/2 = 2¹²⁸ - 2¹⁰⁵f₂に丸められ、範囲外として拒否されること。
    • f₁が奇数仮数であるため、中間点がf₂に丸められるという「最近接偶数への丸め」の具体的な適用例が示されています。
  3. cvt スライスの構造変更とテストロジックの刷新:

    • cvtスライスは、テスト対象の浮動小数点リテラルとその期待される結果を格納します。
    • 新しいcvtエントリは、bits (期待されるビット表現), exact (期待される正確な浮動小数点値), approx (テスト対象の浮動小数点リテラル式), text (リテラル式の文字列表現) を持ちます。
    • approxフィールドには、max32 - ulp32/2 + ulp32/two64 のような、max32ulp32といった算術的に導出された定数を用いた複雑な式が記述されています。これらの式は、float32float64の表現可能な境界のすぐ内側や外側、あるいは中間点付近の値を生成するように設計されています。
    • main関数では、ulp64ulp32の計算が正しいかを確認した後、cvtスライスをループし、各エントリについて以下の検証を行います。
      • c.exactのビット表現がc.bitsと一致するか。
      • c.approxc.exactと一致し、かつc.approxのビット表現がc.bitsと一致するか。
    • これにより、リテラル式が期待される浮動小数点値に正確に丸められ、そのビット表現も正しいことを検証します。
  4. ヘルパー関数の追加:

    • bits(x interface{}) interface{}: float32またはfloat64のビット表現(uint32またはuint64)を返します。
    • fromBits(b uint64, x interface{}) interface{}: ビット表現からfloat32またはfloat64を再構築します。
    • これらの関数は、浮動小数点値とビット表現の間で変換を行い、テストの検証を容易にします。

test/float_lit3.go の変更点

このファイルは、浮動小数点リテラルがオーバーフローするかどうかを検証するためのものです。

  1. 定数の共有: float_lit2.goで定義されたtwoXX, ulpXX, maxXXといった算術的導出定数が、このファイルでも共有されるようになりました。
  2. オーバーフローテストの更新:
    • xスライス内のテストケースが、新しいmax32, ulp32, max64, ulp64定数を用いて書き換えられました。
    • 例えば、float32(max32 + ulp32/2)float32の最大値を超過するため、オーバーフローエラーとなるべきです。
    • float32(max32 + ulp32/2 - 1) のように、Goコンパイラの内部256ビット精度では表現可能だが、最終的なfloat32への丸め時にオーバーフローしない(またはオーバーフローする)境界値をテストしています。
    • 特にfloat64のテストケースでは、GC_ERRORというコメントが追加されています。これは、Goコンパイラ(gc)の内部浮動小数点表現が1024ビットよりも短い場合(実際には256ビット)、max64 + ulp64/2 - 1のような値とmax64 + ulp64/2を区別できない可能性があることを示唆しています。そのため、max64 + ulp64/2 - 1であってもオーバーフローエラーを出すことを許容しています。これは、Goコンパイラの内部精度が仕様で定められた256ビット以上であれば十分であり、それ以上の精度を仮定しないという現実的なアプローチを示しています。

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

test/float_lit2.go

--- a/test/float_lit2.go
+++ b/test/float_lit2.go
@@ -10,89 +10,155 @@ package main
 
 import (
 	"fmt"
+	"math"
 )
 
+// The largest exact float32 is f₁ = (1+(1-2²³))×2¹²⁷ = (1-2²⁴)×2¹²⁸ = 2¹²⁸ - 2¹⁰⁴.
+// The next float32 would be f₂ = (1+1)×2¹²⁷ = 1×2¹²⁸, except that exponent is out of range.
+// Float32 conversion rounds to the nearest float32, rounding to even mantissa:
+// between f₁ and f₂, values closer to f₁ round to f₁and values closer to f₂ are rejected as out of range.
+// f₁ is an odd mantissa, so the halfway point (f₁+f₂)/2 rounds to f₂ and is rejected.
+// The halfway point (f₁+f₂)/2 = 2¹²⁸ - 2¹⁰⁵.
+//
+// The same is true of float64, with different constants: s/24/53/ and s/128/1024/.
+
 const (
 -	m32bits   = 23  // number of float32 mantissa bits
 -	e32max    = 127 // max. float32 exponent
 -	maxExp32  = e32max - m32bits
 -	maxMant32 = 1<<(m32bits+1) - 1
 -
 -	maxFloat32_0 = (maxMant32 - 0) << maxExp32
 -	maxFloat32_1 = (maxMant32 - 1) << maxExp32
 -	maxFloat32_2 = (maxMant32 - 2) << maxExp32
++	two24   = 1.0 * (1 << 24)
++	two53   = 1.0 * (1 << 53)
++	two64   = 1.0 * (1 << 64)
++	two128  = two64 * two64
++	two256  = two128 * two128
++	two512  = two256 * two256
++	two768  = two512 * two256
++	two1024 = two512 * two512
++
++	ulp32 = two128 / two24
++	max32 = two128 - ulp32
++
++	ulp64 = two1024 / two53
++	max64 = two1024 - ulp64
 )
 
-func init() {
-	if maxExp32 != 104 {
-		panic("incorrect maxExp32")
-	}
-	if maxMant32 != 16777215 {
-		panic("incorrect maxMant32")
-	}
-	if maxFloat32_0 != 340282346638528859811704183484516925440 {
-		panic("incorrect maxFloat32_0")
-	}
-+var cvt = []struct {
-+	bits   uint64 // keep us honest
-+	exact  interface{}
-+	approx interface{}
-+	text   string
-+}{
-+	// 0
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32 - ulp32/2), "max32 - ulp32 - ulp32/2"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32), "max32 - ulp32"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32/2), "max32 - ulp32/2"},
-+	{0x7f7ffffe, float32(max32 - ulp32), float32(max32 - ulp32 + ulp32/2), "max32 - ulp32 + ulp32/2"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + ulp32/two64), "max32 - ulp32 + ulp32/2 + ulp32/two64"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + ulp32/two64), "max32 - ulp32/2 + ulp32/two64"},
-+	{0x7f7fffff, float32(max32), float32(max32), "max32"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - ulp32/two64), "max32 + ulp32/2 - ulp32/two64"},
-+
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32 - ulp32/2)), "-(max32 - ulp32 - ulp32/2)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32)), "-(max32 - ulp32)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32/2)), "-(max32 - ulp32/2)"},
-+	{0xff7ffffe, float32(-(max32 - ulp32)), float32(-(max32 - ulp32 + ulp32/2)), "-(max32 - ulp32 + ulp32/2)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + ulp32/two64)), "-(max32 - ulp32 + ulp32/2 + ulp32/two64)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + ulp32/two64)), "-(max32 - ulp32/2 + ulp32/two64)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32)), "-(max32)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - ulp32/two64)), "-(max32 + ulp32/2 - ulp32/two64)"},
-+
-+	// These are required to work: according to the Go spec, the internal float mantissa must be at least 256 bits,
-+	// and these expressions can be represented exactly with a 256-bit mantissa.
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + 1), "max32 - ulp32 + ulp32/2 + 1"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + 1), "max32 - ulp32/2 + 1"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - 1), "max32 + ulp32/2 - 1"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + 1)), "-(max32 - ulp32 + ulp32/2 + 1)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + 1)), "-(max32 - ulp32/2 + 1)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - 1)), "-(max32 + ulp32/2 - 1)"},
-+
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32 + ulp32/2 + 1/two128), "max32 - ulp32 + ulp32/2 + 1/two128"},
-+	{0x7f7fffff, float32(max32), float32(max32 - ulp32/2 + 1/two128), "max32 - ulp32/2 + 1/two128"},
-+	{0x7f7fffff, float32(max32), float32(max32 + ulp32/2 - 1/two128), "max32 + ulp32/2 - 1/two128"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32 + ulp32/2 + 1/two128)), "-(max32 - ulp32 + ulp32/2 + 1/two128)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 - ulp32/2 + 1/two128)), "-(max32 - ulp32/2 + 1/two128)"},
-+	{0xff7fffff, float32(-(max32)), float32(-(max32 + ulp32/2 - 1/two128)), "-(max32 + ulp32/2 - 1/two128)"},
-+
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64 - ulp64/2), "max64 - ulp64 - ulp64/2"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64), "max64 - ulp64"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64/2), "max64 - ulp64/2"},
-+	{0x7feffffffffffffe, float64(max64 - ulp64), float64(max64 - ulp64 + ulp64/2), "max64 - ulp64 + ulp64/2"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64 + ulp64/2 + ulp64/two64), "max64 - ulp64 + ulp64/2 + ulp64/two64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64/2 + ulp64/two64), "max64 - ulp64/2 + ulp64/two64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64), "max64"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 + ulp64/2 - ulp64/two64), "max64 + ulp64/2 - ulp64/two64"},
-+
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64 - ulp64/2)), "-(max64 - ulp64 - ulp64/2)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64)), "-(max64 - ulp64)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64/2)), "-(max64 - ulp64/2)"},
-+	{0xffeffffffffffffe, float64(-(max64 - ulp64)), float64(-(max64 - ulp64 + ulp64/2)), "-(max64 - ulp64 + ulp64/2)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64 + ulp64/2 + ulp64/two64)), "-(max64 - ulp64 + ulp64/2 + ulp64/two64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64/2 + ulp64/two64)), "-(max64 - ulp64/2 + ulp64/two64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64)), "-(max64)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 + ulp64/2 - ulp64/two64)), "-(max64 + ulp64/2 - ulp64/two64)"},
-+
-+	// These are required to work.
-+	// The mantissas are exactly 256 bits.
-+	// max64 is just below 2¹⁰²⁴ so the bottom bit we can use is 2⁷⁶⁸.
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64 + ulp64/2 + two768), "max64 - ulp64 + ulp64/2 + two768"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 - ulp64/2 + two768), "max64 - ulp64/2 + two768"},
-+	{0x7fefffffffffffff, float64(max64), float64(max64 + ulp64/2 - two768), "max64 + ulp64/2 - two768"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64 + ulp64/2 + two768)), "-(max64 - ulp64 + ulp64/2 + two768)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 - ulp64/2 + two768)), "-(max64 - ulp64/2 + two768)"},
-+	{0xffefffffffffffff, float64(-(max64)), float64(-(max64 + ulp64/2 - two768)), "-(max64 + ulp64/2 - two768)"},
+var bugged = false
  
-const (
-	m64bits   = 52   // number of float64 mantissa bits
-	e64max    = 1023 // max. float64 exponent
-	maxExp64  = e64max - m64bits
-	maxMant64 = 1<<(m64bits+1) - 1
-
-	// These expressions are not permitted due to implementation restrictions.
-	// maxFloat64_0 = (maxMant64-0) << maxExp64
-	// maxFloat64_1 = (maxMant64-1) << maxExp64
-	// maxFloat64_2 = (maxMant64-2) << maxExp64
-
-	// These equivalent values were computed using math/big.
-	maxFloat64_0 = 1.7976931348623157e308
-	maxFloat64_1 = 1.7976931348623155e308
-	maxFloat64_2 = 1.7976931348623153e308
-)
+func bug() {
+	if !bugged {
+		bugged = true
+		fmt.Println("BUG")
+	}
+}
  
-func init() {
-	if maxExp64 != 971 {
-		panic("incorrect maxExp64")
-	}
-	if maxMant64 != 9007199254740991 {
-		panic("incorrect maxMant64")
-	}
-}
- 
-var cvt = []struct {
-	val    interface{}
-	binary string
-}{
-
-	{float32(maxFloat32_0), fmt.Sprintf("%dp+%d", int32(maxMant32-0), maxExp32)},
-	{float32(maxFloat32_1), fmt.Sprintf("%dp+%d", int32(maxMant32-1), maxExp32)},
-	{float32(maxFloat32_2), fmt.Sprintf("%dp+%d", int32(maxMant32-2), maxExp32)},
-+func main() {
-+	u64 := math.Float64frombits(0x7fefffffffffffff) - math.Float64frombits(0x7feffffffffffffe)
-+	if ulp64 != u64 {
-+		bug()
-+		fmt.Printf("ulp64=%g, want %g", ulp64, u64)
-+	}
  
-	{float64(maxFloat64_0), fmt.Sprintf("%dp+%d", int64(maxMant64-0), maxExp64)},
-	{float64(maxFloat64_1), fmt.Sprintf("%dp+%d", int64(maxMant64-1), maxExp64)},
-	{float64(maxFloat64_2), fmt.Sprintf("%dp+%d", int64(maxMant64-2), maxExp64)},
-+	u32 := math.Float32frombits(0x7f7fffff) - math.Float32frombits(0x7f7ffffe)
-+	if ulp32 != u32 {
-+		bug()
-+		fmt.Printf("ulp32=%g, want %g", ulp32, u32)
-+	}
  
-	{float32(-maxFloat32_0), fmt.Sprintf("-%dp+%d", int32(maxMant32-0), maxExp32)},
-	{float32(-maxFloat32_1), fmt.Sprintf("-%dp+%d", int32(maxMant32-1), maxExp32)},
-	{float32(-maxFloat32_2), fmt.Sprintf("-%dp+%d", int32(maxMant32-2), maxExp32)},
-+	for _, c := range cvt {
-+		if bits(c.exact) != c.bits {
-+			bug()
-+			fmt.Printf("%s: inconsistent table: bits=%#x (%g) but exact=%g (%#x)\\n", c.text, c.bits, fromBits(c.bits, c.exact), c.exact, bits(c.exact))
-+		}
-+		if c.approx != c.exact || bits(c.approx) != c.bits {
-+			bug()
-+			fmt.Printf("%s: have %g (%#x) want %g (%#x)\\n", c.text, c.approx, c.exact, c.bits)\n", c.text, c.approx, bits(c.approx), c.exact, c.bits)
-+		}
-+	}
-+}\n 
-	{float64(-maxFloat64_0), fmt.Sprintf("-%dp+%d", int64(maxMant64-0), maxExp64)},
-	{float64(-maxFloat64_1), fmt.Sprintf("-%dp+%d", int64(maxMant64-1), maxExp64)},
-	{float64(-maxFloat64_2), fmt.Sprintf("-%dp+%d", int64(maxMant64-2), maxExp64)},
-+func bits(x interface{}) interface{} {
-+	switch x := x.(type) {
-+	case float32:
-+		return uint64(math.Float32bits(x))
-+	case float64:
-+		return math.Float64bits(x)
-+	}
-+	return 0
  }
  
-func main() {
-	bug := false
-	for i, c := range cvt {
-		s := fmt.Sprintf("%b", c.val)
-		if s != c.binary {
-			if !bug {
-				bug = true
-				fmt.Println("BUG")
-			}
-			fmt.Printf("#%d: have %s, want %s\\n", i, s, c.binary)
-		}
-	}
-+func fromBits(b uint64, x interface{}) interface{} {
-+	switch x.(type) {
-+	case float32:
-+		return math.Float32frombits(uint32(b))
-+	case float64:
-+		return math.Float64frombits(b)
  	}
-+	return "?"
  }

test/float_lit3.go

--- a/test/float_lit3.go
+++ b/test/float_lit3.go
@@ -8,21 +8,41 @@
  
  package main
  
+// See float_lit2.go for motivation for these values.
+const (
+	two24   = 1.0 * (1 << 24)
+	two53   = 1.0 * (1 << 53)
+	two64   = 1.0 * (1 << 64)
+	two128  = two64 * two64
+	two256  = two128 * two128
+	two512  = two256 * two256
+	two768  = two512 * two256
+	two1024 = two512 * two512
+
+	ulp32 = two128 / two24
+	max32 = two128 - ulp32
+
+	ulp64 = two1024 / two53
+	max64 = two1024 - ulp64
+)
+
  var x = []interface{}{
 -	float32(-340282356779733661637539395458142568448), // ERROR "constant -3\.40282e\+38 overflows float32"
 -	float32(-340282356779733661637539395458142568447),
 -	float32(-340282326356119256160033759537265639424),
 -	float32(340282326356119256160033759537265639424),
 -	float32(340282356779733661637539395458142568447),
 -	float32(340282356779733661637539395458142568448), // ERROR "constant 3\.40282e\+38 overflows float32"
 -	-1e1000, // ERROR "constant -1\.00000e\+1000 overflows float64"
 -	float64(-1.797693134862315907937289714053e+308), // ERROR "constant -1\.79769e\+308 overflows float64"
 -	float64(-1.797693134862315807937289714053e+308),
 -	float64(-1.797693134862315708145274237317e+308),
 -	float64(-1.797693134862315608353258760581e+308),
 -	float64(1.797693134862315608353258760581e+308),
 -	float64(1.797693134862315708145274237317e+308),
 -	float64(1.797693134862315807937289714053e+308),
 -	float64(1.797693134862315907937289714053e+308), // ERROR "constant 1\.79769e\+308 overflows float64"
 -	1e1000, // ERROR "constant 1\.00000e\+1000 overflows float64"
++	float32(max32 + ulp32/2 - 1),             // ok
++	float32(max32 + ulp32/2 - two128/two256), // ok
++	float32(max32 + ulp32/2),                 // ERROR "constant 3\.40282e\+38 overflows float32"
++
++	float32(-max32 - ulp32/2 + 1),             // ok
++	float32(-max32 - ulp32/2 + two128/two256), // ok
++	float32(-max32 - ulp32/2),                 // ERROR "constant -3\.40282e\+38 overflows float32"
++
++	// If the compiler's internal floating point representation
++	// is shorter than 1024 bits, it cannot distinguish max64+ulp64/2-1 and max64+ulp64/2.
++	// gc uses fewer than 1024 bits, so allow it to print the overflow error for the -1 case.
++	float64(max64 + ulp64/2 - two1024/two256), // ok
++	float64(max64 + ulp64/2 - 1),              // GC_ERROR "constant 1\.79769e\+308 overflows float64"
++	float64(max64 + ulp64/2),                  // ERROR "constant 1\.79769e\+308 overflows float64"
++
++	float64(-max64 - ulp64/2 + two1024/two256), // ok
++	float64(-max64 - ulp64/2 + 1),              // GC_ERROR "constant -1\.79769e\+308 overflows float64"
++	float64(-max64 - ulp64/2),                  // ERROR "constant -1\.79769e\+308 overflows float64"
  }

コアとなるコードの解説

test/float_lit2.go の解説

  • 定数定義の変更: 以前のビット操作に基づく定数定義は、浮動小数点数の内部表現を直接操作するものでしたが、新しいtwoXX系の定数とulpXX, maxXXの算術的導出は、より抽象的で数学的なアプローチを取っています。これにより、テストの意図が明確になり、浮動小数点数の性質に基づいたテストケースの生成が容易になります。
  • cvt スライスの強化: cvtスライス内の各エントリは、approxフィールドに複雑な算術式を持つことで、float32float64の表現可能な境界の非常に近い位置にある値を生成します。例えば、max32 - ulp32/2は、max32から半ULPだけ小さい値を意味し、丸め処理がどのように行われるかを厳密にテストします。ulp32/two64のような非常に小さな値を加減することで、丸め方向を決定する境界(中間点)のすぐ隣の値をテストし、Goコンパイラの丸めロジックがIEEE 754標準に厳密に従っていることを確認します。
  • テストロジックの改善: main関数内のループは、approx式がexact値に正しく丸められ、その結果のビット表現が期待されるbitsと一致するかを検証します。これは、Goコンパイラが浮動小数点リテラルを解析し、内部の高精度表現で計算し、最終的にターゲット型に丸めるという一連のプロセス全体をテストするものです。特に、bits(c.exact) != c.bitsのチェックは、テストテーブル自体が矛盾していないかを確認する自己検証の役割も果たします。
  • 詳細なコメント: float32の境界に関するコメントは、IEEE 754の「最近接偶数への丸め」がどのように機能するかを具体的に説明しており、テストケースの背後にある数学的・技術的根拠を明確にしています。

test/float_lit3.go の解説

  • オーバーフローテストの精度向上: float_lit3.goでは、float_lit2.goで定義されたmaxXXulpXXといった定数を利用することで、オーバーフローテストの精度が大幅に向上しました。以前はハードコードされた大きな数値が使われていましたが、新しいテストケースは、表現可能な最大値のすぐ外側や、Goコンパイラの内部精度と最終的な型変換の境界を突くような値を生成します。
  • GC_ERROR の導入: GC_ERRORコメントは、Goコンパイラ(gc)が内部で256ビットの精度を使用しているため、float64の最大値付近で非常に近い2つの値(例: max64 + ulp64/2 - 1max64 + ulp64/2)を区別できない可能性があることを示しています。Goの仕様は少なくとも256ビットの精度を要求していますが、それ以上の精度を保証しているわけではありません。したがって、このコメントは、特定のコンパイラ実装の挙動を許容しつつ、それでもなおオーバーフローが正しく検出されることを確認するためのものです。これは、Goコンパイラの浮動小数点処理が、仕様の範囲内で堅牢であることを示す重要な側面です。

これらの変更により、Goコンパイラが浮動小数点リテラルを処理する際の丸め動作とオーバーフロー検出が、IEEE 754標準およびGo言語の仕様に厳密に準拠していることを、より包括的かつ堅牢に検証できるようになりました。

関連リンク

参考にした情報源リンク

  • IEEE 754浮動小数点数標準に関する情報 (例: Wikipedia, 各種技術ドキュメント)
  • Go言語の仕様 (特に定数と浮動小数点数に関するセクション)
  • ULP (Unit in the Last Place) に関する情報