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

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

このコミットは、Go言語の標準ライブラリ time パッケージにおける ParseDuration 関数のバグ修正に関するものです。具体的には、32ビットアーキテクチャ上で、小数点以下9桁を超える精度を持つ時間文字列をパースする際に発生するオーバーフローの問題を解決しています。

コミット

commit d220c9957ccc4f2985849ee2e8c7e49fb20f04fe
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Oct 22 18:33:05 2013 -0400

    time: fix ParseDuration overflow when given more than 9 digits on 32-bit arch
    Fixes #6617.
    
    R=golang-dev, rsc, r
    CC=golang-dev
    https://golang.org/cl/15080043

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

https://github.com/golang/go/commit/d220c9957ccc4f2985849ee2e8c7e49fb20f04fe

元コミット内容

time: fix ParseDuration overflow when given more than 9 digits on 32-bit arch Fixes #6617.

このコミットは、time パッケージの ParseDuration 関数が、32ビットアーキテクチャ上で小数点以下9桁を超える時間文字列を解析する際に発生するオーバーフローを修正します。関連する問題として #6617 が挙げられています。

変更の背景

time.ParseDuration 関数は、"1h30m" や "1.5s" のような文字列形式の期間を time.Duration 型に変換するために使用されます。この関数は、秒以下の非常に細かい時間(ナノ秒など)も正確に解析できる必要があります。

問題は、32ビットシステムにおいて、小数点以下の桁数が非常に多い(9桁を超える)時間文字列を処理する際に発生していました。具体的には、小数点以下の部分を計算するために使用される scale 変数が int 型で宣言されており、これが非常に大きな値になるとオーバーフローを引き起こしていました。例えば、"0.3333333333333333333h" のような文字列を解析する際、scale10^19 のような値になる可能性があり、これは32ビット整数の最大値(約 2 * 10^9)をはるかに超えてしまいます。結果として、誤った期間が計算されたり、パースエラーが発生したりする可能性がありました。

このバグは、特に高精度な時間計測や、外部システムから受け取った時間文字列を正確に処理する必要があるアプリケーションにおいて、深刻な問題となり得ました。

前提知識の解説

  • time.Duration: Go言語の time パッケージで定義されている型で、ナノ秒単位で時間を表す整数型です。これにより、時間の加算や減算、比較などを安全かつ効率的に行えます。
  • time.ParseDuration 関数: 文字列形式の期間(例: "1h30m", "500ms", "1.5s")を time.Duration 型に変換する関数です。この関数は、数値部分と単位(ns, us, ms, s, m, h)を解析し、最終的な Duration 値を計算します。小数点以下の値もサポートしており、例えば "1.5s" は1秒500ミリ秒として解析されます。
  • 浮動小数点数 (float64): 倍精度浮動小数点数であり、非常に広い範囲の数値を表現でき、小数点以下の精度も高く保てます。Go言語では float64 が標準的な浮動小数点数型です。
  • 整数型 (int): 整数値を扱う型です。Go言語の int 型は、実行環境のアーキテクチャ(32ビットまたは64ビット)によってサイズが異なります。32ビットシステムでは32ビット整数、64ビットシステムでは64ビット整数として扱われます。32ビット整数の最大値は約20億(2^31 - 1)です。
  • オーバーフロー: 変数に格納できる最大値を超えた値を代入しようとしたときに発生する現象です。整数型の場合、オーバーフローが発生すると、値が予期せぬ小さな値になったり、負の値になったりすることがあります。
  • 32ビットアーキテクチャと64ビットアーキテクチャ: CPUのレジスタやメモリアドレスの幅を示すものです。32ビットシステムでは、一度に処理できるデータの量が32ビットに制限され、整数の最大値も32ビットで表現できる範囲に限られます。一方、64ビットシステムでは、より大きな整数値やメモリアドレスを直接扱うことができます。

技術的詳細

このコミットの核心は、ParseDuration 関数内で小数点以下の部分を計算する際に使用される scale 変数の型を int から float64 に変更した点にあります。

ParseDuration 関数は、時間文字列を解析する際に、数値部分と単位を分離し、それぞれの部分を Duration 型に変換して合計します。小数点以下の部分を処理する際には、例えば "0.5s" の "0.5" のように、小数点以下の桁数に応じて適切なスケールファクタを乗算または除算する必要があります。

元のコードでは、このスケールファクタを計算する scale 変数が int 型でした。小数点以下の桁数が増えるにつれて、scale の値は 10^1, 10^2, 10^3... と増加していきます。32ビットシステムでは、int 型が32ビット整数として扱われるため、scale10^10 (100億) を超えるような値になると、オーバーフローが発生してしまいます。これにより、正確なスケールファクタが計算できなくなり、結果として ParseDuration が誤った値を返したり、エラーになったりしていました。

この修正では、scale 変数を float64 型に変更することで、このオーバーフローの問題を解決しています。float64 は倍精度浮動小数点数であり、非常に大きな数値(約 1.8 * 10^308 まで)を表現できるため、小数点以下の桁数が非常に多くなっても scale がオーバーフローすることはありません。これにより、ParseDuration は32ビットアーキテクチャ上でも、小数点以下9桁を超える高精度な時間文字列を正確に解析できるようになりました。

また、float64(x) / float64(scale) の計算も float64(x) / scale に変更されていますが、これは scale が既に float64 であるため、冗長な型変換を削除したものです。

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

変更は src/pkg/time/format.gosrc/pkg/time/time_test.go の2つのファイルにわたります。

src/pkg/time/format.go

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -1204,11 +1204,11 @@ func ParseDuration(s string) (Duration, error) {
 		if err != nil {
 			return 0, errors.New("time: invalid duration " + orig)
 		}
-		scale := 1
+		scale := 1.0
 		for n := pl - len(s); n > 0; n-- {
 			scale *= 10
 		}
-		g += float64(x) / float64(scale)
+		g += float64(x) / scale
 		post = pl != len(s)
 	}
 	if !pre && !post {

src/pkg/time/time_test.go

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -1318,6 +1318,8 @@ var parseDurationTests = []struct {
 	{"39h9m14.425s", true, 39*Hour + 9*Minute + 14*Second + 425*Millisecond},
 	// large value
 	{"52763797000ns", true, 52763797000 * Nanosecond},
+	// more than 9 digits after decimal point, see http://golang.org/issue/6617
+	{"0.3333333333333333333h", true, 20 * Minute},
 
 	// errors
 	{"", false, 0},

コアとなるコードの解説

src/pkg/time/format.go の変更

  • scale := 1 から scale := 1.0 への変更:
    • 元のコードでは scaleint 型として初期化されていました。これにより、scale *= 10 のループで値が大きくなりすぎると、32ビットシステムでオーバーフローが発生していました。
    • scale := 1.0 とすることで、scalefloat64 型として初期化されます。これにより、scale が非常に大きな値になってもオーバーフローすることなく、正確な浮動小数点数として値を保持できるようになります。
  • g += float64(x) / float64(scale) から g += float64(x) / scale への変更:
    • scale が既に float64 型になったため、float64(scale) という冗長な型変換が不要になりました。これはコードの簡潔化と、意図がより明確になるための変更です。

これらの変更により、ParseDuration 関数は、小数点以下の桁数が多く、scale が大きな値になる場合でも、正確な浮動小数点演算を使用して期間を計算できるようになりました。

src/pkg/time/time_test.go の変更

  • 新しいテストケースの追加:
    • {"0.3333333333333333333h", true, 20 * Minute} という新しいテストケースが追加されました。
    • このテストケースは、小数点以下に非常に多くの桁数を持つ時間文字列(19桁)を ParseDuration が正しく解析できることを検証します。
    • 0.3333333333333333333h は、正確には 1/3 時間に相当し、これは 20分 です。このテストケースは、修正が正しく機能し、高精度な時間文字列が期待通りに 20 * Minute として解析されることを保証します。
    • コメント // more than 9 digits after decimal point, see http://golang.org/issue/6617 は、このテストケースが修正対象の問題(#6617)に直接関連していることを示しています。

このテストケースの追加は、バグが修正されたことを確認するだけでなく、将来の回帰を防ぐための重要なステップです。

関連リンク

参考にした情報源リンク

  • GitHub上のコミットページ: https://github.com/golang/go/commit/d220c9957ccc4f2985849ee2e8c7e49fb20f04fe
  • Go言語の公式Issueトラッカー (ただし、Issue #6617は公開リポジトリでは見つかりませんでした。これは内部的なトラッキング番号であるか、非常に古いIssueである可能性があります。)
  • Go言語のソースコード (timeパッケージ): https://github.com/golang/go/tree/master/src/time
  • Go言語の浮動小数点数に関するドキュメントや記事 (一般的な情報源)
  • 32ビット/64ビットアーキテクチャにおける数値表現に関する一般的な情報源