[インデックス 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) |
---|---|---|---|
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 |
これらの結果は、特に指数部を持つ大きな数や小さな数、そして一般的な浮動小数点数の変換において、劇的な速度向上が達成されたことを示しています。
変更の背景
浮動小数点数を文字列に変換する処理は、多くのアプリケーションで頻繁に行われる操作であり、その性能はアプリケーション全体の応答性に大きな影響を与えます。特に、科学技術計算、データ処理、ログ出力など、浮動小数点数を扱う場面では、正確性と速度の両方が求められます。
従来の浮動小数点数から文字列への変換アルゴリズム(例えば、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進数文字列に変換する際には、以下の課題があります。
- 正確性 (Accuracy): 変換された文字列を再度浮動小数点数に変換したときに、元の値と完全に一致すること(または最も近い値になること)が求められます。これは「正確なラウンドトリップ」と呼ばれます。
- 最短性 (Shortest Representation): 可能な限り少ない桁数で、元の浮動小数点数を一意に識別できる文字列を生成することが求められます。例えば、
0.1
はバイナリでは正確に表現できませんが、10進数では0.1
が最短表現です。 - 効率性 (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
パッケージに統合するために、いくつかの新しい関数とロジックが導入されています。
extFloat
構造体の拡張:extFloat
は、浮動小数点数を内部的に拡張精度で扱うための構造体です。AssignComputeBounds(x float64) (lower, upper extFloat)
: この関数は、与えられたfloat64
値x
をextFloat
に変換し、さらにx
を正確に表現できる10進数区間の下限lower
と上限upper
を計算して返します。これはGrisu3アルゴリズムの重要なステップです。
- 10のべき乗によるスケーリング:
frexp10(expMin, expMax int) (exp10, index int)
:math.Frexp
の10進数版アナログです。extFloat
の値を約10のべき乗でスケーリングし、その結果のバイナリ指数がexpMin
とexpMax
の範囲に収まるように調整します。これにより、10進数の桁を抽出しやすい形にデータを変換します。frexp10Many(expMin, expMax int, a, b, c *extFloat) (exp10 int)
: 複数のextFloat
(lower
,f
,upper
)に対して共通の10のべき乗スケーリングを適用します。
- Grisu3アルゴリズムの実装:
ShortestDecimal(d *decimal, lower, upper *extFloat) bool
: この関数がGrisu3アルゴリズムの主要な実装です。f
の最短10進数表現をd
に格納しようとします。lower
とupper
は、f
が属する開区間(lower, upper)
を定義します。- この関数は、
f.mant == 0
(ゼロの場合)の特殊処理から始まります。 minExp
とmaxExp
という定数で指数範囲を制限します。upper.Normalize()
で正規化を行います。frexp10Many
を使って、lower
,f
,upper
の指数を統一し、10進数スケーリングを適用します。upper.mant++
とlower.mant--
で安全マージンを取ります。- 整数部分と小数部分の桁を順に計算し、
allowance
(許容誤差)に基づいてどこまで桁を生成するかを決定します。 adjustLastDigit
関数を呼び出して、最後の桁を調整し、最短性を確保します。- 結果が不確実な場合は
false
を返します。
- この関数は、
- 最後の桁の調整:
adjustLastDigit(d *decimal, currentDiff, targetDiff, maxDiff, ulpDecimal, ulpBinary uint64) bool
: Grisu3の重要な部分で、生成された10進数表現の最後の桁を調整し、最短かつ正確な表現を保証します。これは、浮動小数点数の丸め誤差と10進数表現の間の関係を考慮して行われます。
ftoa.go
の変更点
strconv/ftoa.go
のgenericFtoa
関数が変更され、prec < 0
(最短表現モード)かつbitSize == 64
(float64
)の場合に、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が成功しなかった場合(ok
がfalse
の場合)、従来のより遅いが確実に正確な方法にフォールバックします。これにより、Grisu3の「不完全性」がカバーされ、常に正確な結果が保証されます。
ftoa_test.go
の変更点
テストファイルには、Grisu3の導入による性能向上と正確性を検証するための新しいベンチマークとテストが追加されています。
TestFtoaRandom
: ランダムなfloat64
値に対して、高速なGrisu3パスと従来の遅いパスで生成される文字列が一致するかどうかを検証します。これにより、Grisu3が正確な結果を生成していることを確認します。BenchmarkFormatFloatNegExp
とBenchmarkAppendFloatNegExp
: 非常に小さな負の指数を持つ浮動小数点数の変換性能を測定するためのベンチマークが追加されました。これは、コミットメッセージに示された83.5x
という劇的な改善を裏付けるものです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
src/pkg/strconv/extfloat.go
:AssignComputeBounds
関数の追加 (行191-221)frexp10
関数の追加 (行339-370)frexp10Many
関数の追加 (行372-378)ShortestDecimal
関数の追加 (行380-469) - Grisu3アルゴリズムの主要な実装adjustLastDigit
関数の追加 (行471-495) - Grisu3の最後の桁調整ロジック
src/pkg/strconv/ftoa.go
:genericFtoa
関数内で、prec < 0
かつbitSize == 64
の場合にGrisu3アルゴリズム(f.ShortestDecimal
)を呼び出すロジックの追加と、フォールバック処理の導入 (行101-129)
src/pkg/strconv/ftoa_test.go
:TestFtoaRandom
テスト関数の追加 (行154-173)BenchmarkFormatFloatNegExp
ベンチマーク関数の追加 (行188-192)BenchmarkAppendFloatNegExp
ベンチマーク関数の追加 (行215-219)
コアとなるコードの解説
src/pkg/strconv/extfloat.go
このファイルは、浮動小数点数の内部表現を操作するためのユーティリティ関数を提供します。Grisu3アルゴリズムは、浮動小数点数をバイナリ表現から10進数表現に変換する際に、その内部構造を深く利用するため、このファイルに多くの新しい関数が追加されました。
-
AssignComputeBounds
: この関数は、与えられたfloat64
値x
に対して、そのextFloat
表現を設定し、さらにx
を正確に表現できる10進数区間の下限lower
と上限upper
を計算します。浮動小数点数は、厳密にはある範囲の実数を表すため、この区間計算はGrisu3が最短表現を見つけるための基礎となります。特に、非正規化数(denormalized numbers)の処理や、仮数部と指数部のビット操作が含まれます。 -
frexp10
およびfrexp10Many
: これらの関数は、浮動小数点数を10のべき乗でスケーリングする役割を担います。Grisu3は、浮動小数点数を10進数の桁に変換するために、内部的に値を適切な10のべき乗で乗算(または除算)して、そのバイナリ指数を特定の範囲に収める必要があります。frexp10
は単一のextFloat
に対して、frexp10Many
は複数のextFloat
(lower
,f
,upper
)に対してこのスケーリングを適用し、共通の指数調整を行います。これにより、後の桁抽出処理が簡素化されます。 -
ShortestDecimal
: これがGrisu3アルゴリズムの心臓部です。- まず、ゼロ値の特殊ケースを処理します。
lower
とupper
の正規化と指数部の統一を行います。frexp10Many
を呼び出して、lower
,f
,upper
を適切な10進数スケールに調整します。upper.mant++
とlower.mant--
によって、計算上の丸め誤差に対する安全マージンを設けます。- 整数部分の桁を計算し、
allowance
(許容誤差)に基づいて、どこまで桁を生成すれば最短性が保証されるかを判断します。 - 小数部分の桁を計算し、同様に
allowance
とmultiplier
(10のべき乗)を用いて、桁の生成を停止するタイミングを決定します。 - 最後に
adjustLastDigit
を呼び出し、最終的な桁の調整を行います。 - 結果が不確実な場合は
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 == 64
(float64
型の場合)に、新しいGrisu3ベースのf.ShortestDecimal
関数を呼び出します。f.ShortestDecimal
がtrue
を返した場合(Grisu3が成功した場合)、その結果を使用します。f.ShortestDecimal
がfalse
を返した場合(Grisu3が最短性を保証できなかった場合)、またはoptimize
フラグがfalse
の場合、従来のより汎用的で正確だが遅いアルゴリズム(d.Assign
,d.Shift
,roundShortest
など)にフォールバックします。 このフォールバックメカニズムにより、Grisu3の「不完全性」が補完され、常に正確な結果が保証されます。
src/pkg/strconv/ftoa_test.go
このファイルは、strconv
パッケージの浮動小数点数変換機能のテストとベンチマークを含みます。
-
TestFtoaRandom
: このテストは、Grisu3アルゴリズムが導入された高速パスと、従来の遅いパスで生成される浮動小数点数の文字列表現が、ランダムな入力に対して常に一致することを確認します。これにより、Grisu3が性能を向上させつつも、既存の正確性要件を満たしていることが検証されます。 -
新しいベンチマーク:
BenchmarkFormatFloatNegExp
とBenchmarkAppendFloatNegExp
は、特に小さな負の指数を持つ浮動小数点数(例:-5.11e-95
)の変換性能を測定するために追加されました。コミットメッセージに示された83.5x
という驚異的な速度向上は、このような極端な値の処理においてGrisu3が特に効果的であることを示しています。
これらの変更により、Go言語のstrconv
パッケージは、浮動小数点数から文字列への変換において、世界トップクラスの性能と正確性を両立するようになりました。
関連リンク
- Go言語の
strconv
パッケージドキュメント: https://pkg.go.dev/strconv - Gerrit Code Review for this commit: https://golang.org/cl/5502079
参考にした情報源リンク
- F. Loitsch, ``Printing Floating-Point Numbers Quickly and Accurately with Integers'', Proceedings of the ACM, 2010.
- このコミットで導入されたGrisu3アルゴリズムの原典論文です。
- ACM Digital Library: https://dl.acm.org/doi/10.1145/1806596.1806607 (アクセスには購読が必要な場合があります)
- 著者のウェブサイトなどで公開されている場合もあります。
- Grisu3アルゴリズムに関する解説記事:
- Grisu3 algorithm - Wikipedia: https://en.wikipedia.org/wiki/Grisu3_algorithm
- 各種プログラミング言語やライブラリにおけるGrisu3の実装に関するブログ記事やドキュメント。
- Chrome V8
double-conversion
ライブラリ:- Grisu3アルゴリズムを実装しており、このコミットのインスピレーション元の一つです。
- GitHubリポジトリ: https://github.com/google/double-conversion
- 浮動小数点数表現に関する一般的な情報:
- IEEE 754 - Wikipedia: https://ja.wikipedia.org/wiki/IEEE_754
- 浮動小数点数 - Wikipedia: https://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0