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

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

このコミットは、Go言語のstrconvパッケージにおける浮動小数点数から文字列への変換処理、特にFormatFloat(x, *, -1, 64)のパフォーマンスを大幅に向上させるものです。具体的には、Grisu3アルゴリズムを導入することで、最短かつ正確な10進数表現の生成を高速化しています。

コミット

commit 0575cd9de45215c069ffb15afe11599dcb409f62
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Jan 13 23:24:33 2012 +0100

    strconv: faster FormatFloat(x, *, -1, 64) using Grisu3 algorithm.
    
    The implementation is similar to the one from the double-conversion
    library used in the Chrome V8 engine.
    
                                old ns/op   new ns/op  speedup
    BenchmarkAppendFloatDecimal      591         480      1.2x
    BenchmarkAppendFloat            2956         486      6.1x
    BenchmarkAppendFloatExp        10622         503     21.1x
    BenchmarkAppendFloatNegExp     40343         483     83.5x
    BenchmarkAppendFloatBig         2798         664      4.2x
    
    See F. Loitsch, ``Printing Floating-Point Numbers Quickly and
    Accurately with Integers'', Proceedings of the ACM, 2010.
    
    R=rsc
    CC=golang-dev, remy
    https://golang.org/cl/5502079

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

https://github.com/golang/go/commit/0575cd9de45215c069ffb15afe11599dcb409f62

元コミット内容

このコミットは、Go言語の標準ライブラリであるstrconvパッケージにおいて、浮動小数点数(float64)を文字列に変換する関数FormatFloatの性能改善を目的としています。特に、最短かつ正確な10進数表現を生成するモード(prec < 0)において、Grisu3アルゴリズムを導入することで、既存の実装よりも大幅な高速化を実現しています。

コミットメッセージには、以下のベンチマーク結果が示されており、その性能向上が明確に示されています。

ベンチマーク名旧性能 (ns/op)新性能 (ns/op)改善率 (speedup)
BenchmarkAppendFloatDecimal5914801.2x
BenchmarkAppendFloat29564866.1x
BenchmarkAppendFloatExp1062250321.1x
BenchmarkAppendFloatNegExp4034348383.5x
BenchmarkAppendFloatBig27986644.2x

これらの結果は、特に指数部を持つ大きな数や小さな数、そして一般的な浮動小数点数の変換において、劇的な速度向上が達成されたことを示しています。

変更の背景

浮動小数点数を文字列に変換する処理は、多くのアプリケーションで頻繁に行われる操作であり、その性能はアプリケーション全体の応答性に大きな影響を与えます。特に、科学技術計算、データ処理、ログ出力など、浮動小数点数を扱う場面では、正確性と速度の両方が求められます。

従来の浮動小数点数から文字列への変換アルゴリズム(例えば、Dragon4など)は、正確性を保証するために任意精度演算(bignum arithmetic)を使用することが多く、これが性能上のボトルネックとなっていました。Go言語のstrconvパッケージも、この問題に直面していました。

このコミットの背景には、より高速な浮動小数点数変換アルゴリズムの必要性がありました。Grisu3アルゴリズムは、固定サイズの整数演算のみを使用することで、この問題を解決し、大幅な高速化を実現できることが知られていました。ChromeのV8 JavaScriptエンジンで採用されているdouble-conversionライブラリも同様のアプローチを取っており、その成功がGo言語への導入を後押ししたと考えられます。

前提知識の解説

浮動小数点数 (Floating-Point Numbers)

コンピュータにおける浮動小数点数は、実数を近似的に表現するための形式です。IEEE 754規格が広く用いられており、符号部、指数部、仮数部から構成されます。float64は倍精度浮動小数点数であり、64ビットで表現されます。

  • 符号部 (Sign Bit): 数の正負を表します。
  • 指数部 (Exponent): 数のスケール(桁の大きさ)を表します。
  • 仮数部 (Mantissa/Significand): 数の有効数字を表します。

浮動小数点数の表現には、精度と範囲のトレードオフがあります。

浮動小数点数から文字列への変換 (Floating-Point to String Conversion)

浮動小数点数を人間が読める10進数文字列に変換する際には、以下の課題があります。

  1. 正確性 (Accuracy): 変換された文字列を再度浮動小数点数に変換したときに、元の値と完全に一致すること(または最も近い値になること)が求められます。これは「正確なラウンドトリップ」と呼ばれます。
  2. 最短性 (Shortest Representation): 可能な限り少ない桁数で、元の浮動小数点数を一意に識別できる文字列を生成することが求められます。例えば、0.1はバイナリでは正確に表現できませんが、10進数では0.1が最短表現です。
  3. 効率性 (Efficiency): 変換処理が高速であること。

これらの要件を同時に満たすことは容易ではなく、特に最短性と正確性を両立させつつ高速化することは、長年の研究課題でした。

Grisu3 アルゴリズム

Grisu3は、Florian Loitschが2010年に発表した、浮動小数点数を最短かつ正確な10進数文字列に変換するためのアルゴリズムです。その最大の特徴は、任意精度演算をほとんど使用せず、固定サイズの整数演算(主に64ビット整数)のみで処理を行う点にあります。これにより、従来のアルゴリズム(例: Dragon4)と比較して大幅な高速化を実現しました。

Grisu3は、以下の原理に基づいています。

  • 区間ベースのアプローチ: 浮動小数点数xは、正確には[x - ulp/2, x + ulp/2]という区間内の任意の実数を表します(ulpはUnit in the Last Place)。Grisu3は、この区間内に含まれる最短の10進数表現を見つけようとします。
  • バイナリ浮動小数点数を10進数にスケーリング: 浮動小数点数を適切な10のべき乗でスケーリングし、整数部分と小数部分に分けます。このスケーリングにより、浮動小数点数の内部表現(バイナリ)を10進数の桁にマッピングしやすくなります。
  • 固定精度整数演算: スケーリングされた値を固定精度の整数で表現し、その整数に対して10進数の桁を抽出する操作を行います。これにより、任意精度演算のオーバーヘッドを回避します。
  • 「不完全性」とフォールバック: Grisu3は、ごく一部の浮動小数点数(約0.5%)に対しては、最短ではないが正確な結果を生成する可能性があります。このような「不完全」なケースを検出し、より遅いが確実に最短かつ正確な結果を生成できるフォールバックアルゴリズム(例えば、Dragon4のような任意精度演算ベースのアルゴリズム)に切り替えることで、全体としての性能と正確性を両立させます。

Grisu3は、その高速性から、Chrome V8、Firefox、WebKitなどの主要なJavaScriptエンジンや、Go、Julia、Rustといったプログラミング言語で採用されています。

技術的詳細

このコミットでは、Grisu3アルゴリズムをGo言語のstrconvパッケージに統合するために、いくつかの新しい関数とロジックが導入されています。

  1. extFloat構造体の拡張:
    • extFloatは、浮動小数点数を内部的に拡張精度で扱うための構造体です。
    • AssignComputeBounds(x float64) (lower, upper extFloat): この関数は、与えられたfloat64xextFloatに変換し、さらにxを正確に表現できる10進数区間の下限lowerと上限upperを計算して返します。これはGrisu3アルゴリズムの重要なステップです。
  2. 10のべき乗によるスケーリング:
    • frexp10(expMin, expMax int) (exp10, index int): math.Frexpの10進数版アナログです。extFloatの値を約10のべき乗でスケーリングし、その結果のバイナリ指数がexpMinexpMaxの範囲に収まるように調整します。これにより、10進数の桁を抽出しやすい形にデータを変換します。
    • frexp10Many(expMin, expMax int, a, b, c *extFloat) (exp10 int): 複数のextFloatlower, f, upper)に対して共通の10のべき乗スケーリングを適用します。
  3. Grisu3アルゴリズムの実装:
    • ShortestDecimal(d *decimal, lower, upper *extFloat) bool: この関数がGrisu3アルゴリズムの主要な実装です。fの最短10進数表現をdに格納しようとします。lowerupperは、fが属する開区間 (lower, upper)を定義します。
      • この関数は、f.mant == 0(ゼロの場合)の特殊処理から始まります。
      • minExpmaxExpという定数で指数範囲を制限します。
      • upper.Normalize()で正規化を行います。
      • frexp10Manyを使って、lower, f, upperの指数を統一し、10進数スケーリングを適用します。
      • upper.mant++lower.mant--で安全マージンを取ります。
      • 整数部分と小数部分の桁を順に計算し、allowance(許容誤差)に基づいてどこまで桁を生成するかを決定します。
      • adjustLastDigit関数を呼び出して、最後の桁を調整し、最短性を確保します。
      • 結果が不確実な場合はfalseを返します。
  4. 最後の桁の調整:
    • adjustLastDigit(d *decimal, currentDiff, targetDiff, maxDiff, ulpDecimal, ulpBinary uint64) bool: Grisu3の重要な部分で、生成された10進数表現の最後の桁を調整し、最短かつ正確な表現を保証します。これは、浮動小数点数の丸め誤差と10進数表現の間の関係を考慮して行われます。

ftoa.goの変更点

strconv/ftoa.gogenericFtoa関数が変更され、prec < 0(最短表現モード)かつbitSize == 64float64)の場合に、Grisu3アルゴリズムを試行するロジックが追加されました。

	shortest := prec < 0

	d := new(decimal)
	if shortest {
		ok := false
		if optimize && bitSize == 64 {
			// Try Grisu3 algorithm.
			f := new(extFloat)
			lower, upper := f.AssignComputeBounds(val)
			ok = f.ShortestDecimal(d, &lower, &upper)
		}
		if !ok {
			// Fallback to slower, but always correct method
			// Create exact decimal representation.
			// ... (existing slower logic)
			d.Assign(mant)
			d.Shift(exp - int(flt.mantbits))
			roundShortest(d, mant, exp, flt)
		}
		// ... (precision adjustment for shortest mode)
	} else {
		// ... (existing logic for fixed precision)
		d.Assign(mant)
		d.Shift(exp - int(flt.mantbits))
		// ... (rounding for fixed precision)
	}

このコードスニペットは、optimizeフラグが有効で、float64の最短表現が要求された場合に、まずGrisu3アルゴリズム(f.ShortestDecimal)を試みることを示しています。Grisu3が成功しなかった場合(okfalseの場合)、従来のより遅いが確実に正確な方法にフォールバックします。これにより、Grisu3の「不完全性」がカバーされ、常に正確な結果が保証されます。

ftoa_test.goの変更点

テストファイルには、Grisu3の導入による性能向上と正確性を検証するための新しいベンチマークとテストが追加されています。

  • TestFtoaRandom: ランダムなfloat64値に対して、高速なGrisu3パスと従来の遅いパスで生成される文字列が一致するかどうかを検証します。これにより、Grisu3が正確な結果を生成していることを確認します。
  • BenchmarkFormatFloatNegExpBenchmarkAppendFloatNegExp: 非常に小さな負の指数を持つ浮動小数点数の変換性能を測定するためのベンチマークが追加されました。これは、コミットメッセージに示された83.5xという劇的な改善を裏付けるものです。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/pkg/strconv/extfloat.go:
    • AssignComputeBounds関数の追加 (行191-221)
    • frexp10関数の追加 (行339-370)
    • frexp10Many関数の追加 (行372-378)
    • ShortestDecimal関数の追加 (行380-469) - Grisu3アルゴリズムの主要な実装
    • adjustLastDigit関数の追加 (行471-495) - Grisu3の最後の桁調整ロジック
  2. src/pkg/strconv/ftoa.go:
    • genericFtoa関数内で、prec < 0かつbitSize == 64の場合にGrisu3アルゴリズム(f.ShortestDecimal)を呼び出すロジックの追加と、フォールバック処理の導入 (行101-129)
  3. src/pkg/strconv/ftoa_test.go:
    • TestFtoaRandomテスト関数の追加 (行154-173)
    • BenchmarkFormatFloatNegExpベンチマーク関数の追加 (行188-192)
    • BenchmarkAppendFloatNegExpベンチマーク関数の追加 (行215-219)

コアとなるコードの解説

src/pkg/strconv/extfloat.go

このファイルは、浮動小数点数の内部表現を操作するためのユーティリティ関数を提供します。Grisu3アルゴリズムは、浮動小数点数をバイナリ表現から10進数表現に変換する際に、その内部構造を深く利用するため、このファイルに多くの新しい関数が追加されました。

  • AssignComputeBounds: この関数は、与えられたfloat64xに対して、そのextFloat表現を設定し、さらにxを正確に表現できる10進数区間の下限lowerと上限upperを計算します。浮動小数点数は、厳密にはある範囲の実数を表すため、この区間計算はGrisu3が最短表現を見つけるための基礎となります。特に、非正規化数(denormalized numbers)の処理や、仮数部と指数部のビット操作が含まれます。

  • frexp10 および frexp10Many: これらの関数は、浮動小数点数を10のべき乗でスケーリングする役割を担います。Grisu3は、浮動小数点数を10進数の桁に変換するために、内部的に値を適切な10のべき乗で乗算(または除算)して、そのバイナリ指数を特定の範囲に収める必要があります。frexp10は単一のextFloatに対して、frexp10Manyは複数のextFloatlower, f, upper)に対してこのスケーリングを適用し、共通の指数調整を行います。これにより、後の桁抽出処理が簡素化されます。

  • ShortestDecimal: これがGrisu3アルゴリズムの心臓部です。

    1. まず、ゼロ値の特殊ケースを処理します。
    2. lowerupperの正規化と指数部の統一を行います。
    3. frexp10Manyを呼び出して、lower, f, upperを適切な10進数スケールに調整します。
    4. upper.mant++lower.mant--によって、計算上の丸め誤差に対する安全マージンを設けます。
    5. 整数部分の桁を計算し、allowance(許容誤差)に基づいて、どこまで桁を生成すれば最短性が保証されるかを判断します。
    6. 小数部分の桁を計算し、同様にallowancemultiplier(10のべき乗)を用いて、桁の生成を停止するタイミングを決定します。
    7. 最後にadjustLastDigitを呼び出し、最終的な桁の調整を行います。
    8. 結果が不確実な場合はfalseを返し、呼び出し元にフォールバックを促します。
  • adjustLastDigit: この関数は、ShortestDecimalによって生成された10進数表現の最後の桁を微調整します。Grisu3は固定精度演算を使用するため、厳密な最短性を保証するために、最後の桁をインクリメントまたはデクリメントする必要がある場合があります。この調整は、currentDiff(現在の誤差)、targetDiff(目標誤差)、maxDiff(許容最大誤差)、ulpDecimal(10進数1桁の重み)、ulpBinary(バイナリULPの重み)といったパラメータを用いて行われます。これにより、生成される文字列が元の浮動小数点数を一意に識別できる最短の表現であることを保証します。

src/pkg/strconv/ftoa.go

このファイルは、Go言語の組み込み型を文字列に変換する機能を提供します。FormatFloat関数は、このファイルのgenericFtoa関数を内部的に呼び出します。

  • genericFtoaの変更: 既存のgenericFtoa関数に、Grisu3アルゴリズムを条件付きで適用するロジックが追加されました。
    • prec < 0(精度が負の場合、つまり最短表現が要求された場合)かつbitSize == 64float64型の場合)に、新しいGrisu3ベースのf.ShortestDecimal関数を呼び出します。
    • f.ShortestDecimaltrueを返した場合(Grisu3が成功した場合)、その結果を使用します。
    • f.ShortestDecimalfalseを返した場合(Grisu3が最短性を保証できなかった場合)、またはoptimizeフラグがfalseの場合、従来のより汎用的で正確だが遅いアルゴリズム(d.Assign, d.Shift, roundShortestなど)にフォールバックします。 このフォールバックメカニズムにより、Grisu3の「不完全性」が補完され、常に正確な結果が保証されます。

src/pkg/strconv/ftoa_test.go

このファイルは、strconvパッケージの浮動小数点数変換機能のテストとベンチマークを含みます。

  • TestFtoaRandom: このテストは、Grisu3アルゴリズムが導入された高速パスと、従来の遅いパスで生成される浮動小数点数の文字列表現が、ランダムな入力に対して常に一致することを確認します。これにより、Grisu3が性能を向上させつつも、既存の正確性要件を満たしていることが検証されます。

  • 新しいベンチマーク: BenchmarkFormatFloatNegExpBenchmarkAppendFloatNegExpは、特に小さな負の指数を持つ浮動小数点数(例: -5.11e-95)の変換性能を測定するために追加されました。コミットメッセージに示された83.5xという驚異的な速度向上は、このような極端な値の処理においてGrisu3が特に効果的であることを示しています。

これらの変更により、Go言語のstrconvパッケージは、浮動小数点数から文字列への変換において、世界トップクラスの性能と正確性を両立するようになりました。

関連リンク

参考にした情報源リンク