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

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

このコミットは、Go言語の標準ライブラリであるtimeパッケージ内のテストコードにおける浮動小数点数表現の修正に関するものです。具体的には、Date関数の引数として渡されるナノ秒の値を計算する際に、gccgoコンパイラでの浮動小数点数の精度向上によって発生した問題に対処しています。

コミット

commit 426bf361316ab495a29c9e0a05da6236894dd392
Author: Ian Lance Taylor <iant@golang.org>
Date:   Mon Apr 23 15:46:54 2012 -0700

    time: change float expression to ensure it is an integer
    
    When I increased the number of bits that gccgo uses for
    untyped floats, the expression 0.52*1e9 was no longer
    integral.  This patch fixes that.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6113043

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

https://github.com/golang/go/commit/426bf361316ab495a29c9e0a05da6236894dd392

元コミット内容

time: change float expression to ensure it is an integer

When I increased the number of bits that gccgo uses for untyped floats, the expression 0.52*1e9 was no longer integral. This patch fixes that.

変更の背景

この変更の背景には、Go言語のコンパイラの一つであるgccgoにおける浮動小数点数の内部表現の変更があります。コミットメッセージによると、gccgoが「型なし浮動小数点数(untyped floats)」に使用するビット数を増やした際に、0.52 * 1e9という式の結果がもはや整数ではなくなってしまった、という問題が発生しました。

timeパッケージのDate関数は、秒の小数点以下の部分をナノ秒単位で受け取ります。例えば、0.52秒は0.52 * 1e9ナノ秒、つまり520,000,000ナノ秒として表現されるべきです。しかし、gccgoの浮動小数点数表現の変更により、この計算結果が厳密な整数値(例: 520000000.0000000000000001のような値)ではなくなったため、テストが失敗する可能性が出てきました。

この問題は、浮動小数点数の計算が常に厳密な結果を保証しないという性質に起因します。特に、0.52のような有限の小数であっても、二進数表現では無限小数となり、特定の精度で丸められる際に誤差が生じることがあります。gccgoが内部的に使用するビット数を増やしたことで、この誤差が以前よりも顕著になり、整数であるべき値がわずかにずれてしまう状況が発生したと考えられます。

このコミットは、このようなコンパイラの内部的な浮動小数点数処理の変更に起因する問題を回避し、Date関数に渡されるナノ秒の値が常に正確な整数であることを保証するために行われました。

前提知識の解説

1. Go言語のtimeパッケージ

Go言語の標準ライブラリであるtimeパッケージは、時刻の表現、操作、フォーマット、および時間間隔の測定を提供します。

  • time.Time型: 特定の時点を表します。
  • time.Date関数: 指定された年、月、日、時、分、秒、ナノ秒、およびロケーション情報からtime.Timeオブジェクトを構築します。引数nsecはナノ秒単位で、int型を受け取ります。

2. 浮動小数点数と精度

コンピュータにおける浮動小数点数(Floating-point number)は、実数を近似的に表現するための形式です。IEEE 754などの標準に基づいて表現されますが、多くの実数は二進数で正確に表現できないため、計算には常に丸め誤差が伴う可能性があります。 例えば、0.1という十進数は二進数では無限小数になるため、コンピュータ内部では近似値として表現されます。この近似値が連鎖的な計算によって誤差を拡大させることがあります。

3. 1e9とは

1e9は科学的記数法(exponential notation)で1 × 10^9、つまり1,000,000,000を意味します。これはGo言語を含む多くのプログラミング言語で浮動小数点数のリテラルとして使用できます。timeパッケージでは、秒をナノ秒に変換するためにこの値がよく使われます(1秒 = 10^9ナノ秒)。

4. 型なし浮動小数点数(Untyped Floats)

Go言語には「型なし定数(untyped constants)」という概念があります。これは、リテラル値(例: 0.52, 1e9, 520)が、特定の型に束縛される前に、より高い精度で表現されることを意味します。 例えば、0.52は最初は型なし浮動小数点数として扱われ、その後に変数に代入されたり、関数に渡されたりする際に、その変数の型や関数の引数の型(例: float64)に変換されます。この変換の際に、精度が失われる可能性があります。 gccgoが型なし浮動小数点数に使用するビット数を増やしたということは、内部的な計算精度が向上したことを意味しますが、それがかえって特定の計算で「厳密な整数」であるべき値がわずかにずれるという副作用を生んだと考えられます。これは、より高い精度で計算された結果が、最終的に整数型に変換される際に、わずかな誤差が原因で丸めが意図しない方向に働く可能性があるためです。

5. gccgoコンパイラ

Go言語には主に2つの公式コンパイラがあります。

  • gc (Go compiler): Goチームが開発している主要なコンパイラで、Go言語で書かれています。
  • gccgo: GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。C/C++など他の言語と同様にGCCの最適化やバックエンドを利用できます。

このコミットはgccgo特有の問題に対処しており、gcコンパイラでは同様の問題が発生しなかった可能性があります。これは、両コンパイラが浮動小数点数の内部表現や最適化戦略において異なる実装を持っているためです。

技術的詳細

問題の根源は、0.52 * 1e9という浮動小数点数演算の結果が、gccgoの特定のバージョン(型なし浮動小数点数のビット数を増やした後)で、厳密な整数値である520,000,000ではなく、例えば519,999,999.9999999999999999520,000,000.0000000000000001のような、ごくわずかな誤差を含む値になったことです。

time.Date関数のnsec引数はint型を期待します。Go言語では、浮動小数点数を整数型に変換する際、小数部分は切り捨てられます。

  • もし0.52 * 1e9519,999,999.999...となった場合、intに変換されると519,999,999となり、期待される520,000,000とは異なる値になります。
  • もし0.52 * 1e9520,000,000.000...1となった場合、intに変換されると520,000,000となり、結果的には正しい値になりますが、これは偶然に依存します。

この問題を解決するために、コミットでは0.52 * 1e9という浮動小数点数演算を、520 * 1e6という整数演算に近い形に変更しています。

  • 0.52 * 1e9

    • 0.52は浮動小数点数リテラル。
    • 1e9も浮動小数点数リテラル。
    • これらの乗算は浮動小数点数演算として実行され、前述の丸め誤差が生じる可能性があります。
  • 520 * 1e6

    • 520は整数リテラル。
    • 1e61,000,000を意味する浮動小数点数リテラルですが、この文脈では520と乗算されることで、結果的に520,000,000という厳密な整数値が期待されます。
    • 1e61,000,000であり、0.52 * 1e9と同じ520,000,000という結果を得るために、0.52520に、1e91e6に調整しています。
    • この変更により、520という整数と1e6という1,000,000の乗算は、浮動小数点数演算ではあるものの、より「正確な」整数結果を生成しやすくなります。特に、520は正確に二進数で表現できる整数であり、1,000,000も同様です。これらの乗算は、0.52のような無限小数になりうる値を含む計算よりも、丸め誤差の影響を受けにくい傾向があります。

この修正は、浮動小数点数の計算における誤差の性質を理解し、それを回避するための実用的なアプローチを示しています。特に、整数であるべき値は、可能な限り整数演算に近い形で表現することが重要です。

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

変更はsrc/pkg/time/time_test.goファイル内のjsonTestsというテストデータ構造体配列の一箇所です。

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -805,7 +805,7 @@ var jsonTests = []struct {
 	time Time
 	json string
 }{
-	{Date(9999, 4, 12, 23, 20, 50, .52*1e9, UTC), `"9999-04-12T23:20:50.52Z"`},
+	{Date(9999, 4, 12, 23, 20, 50, 520*1e6, UTC), `"9999-04-12T23:20:50.52Z"`},
 	{Date(1996, 12, 19, 16, 39, 57, 0, Local), `"1996-12-19T16:39:57-08:00"`},
 	{Date(0, 1, 1, 0, 0, 0, 1, FixedZone("", 1*60)), `"0000-01-01T00:00:00.000000001+00:01"`},
 }

コアとなるコードの解説

変更された行は、jsonTests配列の最初の要素です。このテストケースは、特定のtime.TimeオブジェクトがJSON形式で正しくシリアライズされることを検証しています。

元のコード:

{Date(9999, 4, 12, 23, 20, 50, .52*1e9, UTC), `"9999-04-12T23:20:50.52Z"`},

ここで、Date関数の第7引数(ナノ秒)に.52*1e9が渡されています。これは0.52 * 1,000,000,000を意味し、期待される値は520,000,000ナノ秒です。

修正後のコード:

{Date(9999, 4, 12, 23, 20, 50, 520*1e6, UTC), `"9999-04-12T23:20:50.52Z"`},

修正では、ナノ秒の計算式が520*1e6に変更されました。これは520 * 1,000,000を意味し、結果はやはり520,000,000ナノ秒です。

この変更の意図は、前述の通り、0.52という浮動小数点数を含む計算がgccgoで丸め誤差を生じる可能性があったため、より整数に近い形での計算に置き換えることで、ナノ秒の値が常に正確な整数としてDate関数に渡されるようにすることです。5201e61,000,000)はどちらも正確に二進数で表現できるため、これらの乗算は0.52を含む乗算よりも浮動小数点数誤差の影響を受けにくいと考えられます。

この修正はテストコード内で行われていますが、これはtime.Date関数自体の動作を保証するためのものであり、間接的にtimeパッケージの堅牢性を高めることに貢献しています。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分情報: /home/orange/Project/comemo/commit_data/12938.txt
  • GitHub上のコミットページ: https://github.com/golang/go/commit/426bf361316ab495a29c9e0a05da6236894dd392
  • Go言語の浮動小数点数に関する一般的な知識
  • gccgoコンパイラに関する一般的な知識
  • 浮動小数点数演算の精度に関する一般的な知識
  • time.Date関数のシグネチャと引数の意味