[インデックス 13453] ファイルの概要
このコミットは、Go言語の標準ライブラリstrconv
パッケージにおける浮動小数点数と文字列間の変換処理、特にfloat32
型に対するGrisu3アルゴリズムの拡張と、浮動小数点数の正規化処理の改善に焦点を当てています。これにより、浮動小数点数の文字列変換(ftoa
)および文字列から浮動小数点数への変換(atof
)のパフォーマンスが向上しています。
コミット
commit d6147d8102b095caac3267f9864a4025650c43f8
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Tue Jul 10 07:44:23 2012 +0200
strconv: extend Grisu3 algorithm to float32.
Also improve extfloat.Normalize to obtain a modest performance
gain in parsing, and add a shortcut path for exact integers.
benchmark old ns/op new ns/op delta
BenchmarkAtof64Decimal 73 73 -0.54%
BenchmarkAtof64Float 91 91 -0.54%
BenchmarkAtof64FloatExp 198 180 -9.09%
BenchmarkAtof64Big 307 308 +0.33%
BenchmarkAtof32Decimal 72 72 +0.42%
BenchmarkAtof32Float 83 83 -0.72%
BenchmarkAtof32FloatExp 212 186 -12.26%
BenchmarkAtof32Random 262 250 -4.58%
BenchmarkAppendFloatDecimal 474 305 -35.65%
BenchmarkAppendFloat 497 489 -1.61%
BenchmarkAppendFloatExp 493 483 -2.03%
BenchmarkAppendFloatNegExp 481 481 +0.00%
BenchmarkAppendFloatBig 667 652 -2.25%
BenchmarkAppendFloat32Integer 338 307 -9.17%
BenchmarkAppendFloat32ExactFraction 364 439 +20.60%
BenchmarkAppendFloat32Point 1299 490 -62.28%
BenchmarkAppendFloat32Exp 2593 489 -81.14%
BenchmarkAppendFloat32NegExp 5116 481 -90.60%
R=rsc, r
CC=golang-dev, remy
https://golang.org/cl/6303087
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d6147d8102b095caac3267f9864a4025650c43f8
元コミット内容
このコミットは、Go言語のstrconv
パッケージにおいて、以下の主要な変更を導入しています。
- Grisu3アルゴリズムの
float32
への拡張: 以前はfloat64
にのみ適用されていたGrisu3アルゴリズムが、float32
型にも適用されるようになりました。Grisu3は、浮動小数点数を最短かつ正確な10進数文字列に変換するための効率的なアルゴリズムです。 extfloat.Normalize
関数の改善: 浮動小数点数の内部表現を正規化するextfloat.Normalize
関数の実装が最適化されました。これにより、浮動小数点数のパース(文字列から数値への変換)において、わずかながらパフォーマンスの向上が見られます。- 正確な整数値に対するショートカットパスの追加: 浮動小数点数が正確な整数値を表す場合、その変換処理を高速化するための特別なパスが追加されました。
これらの変更は、ベンチマーク結果に示されているように、特にfloat32
の文字列変換(AppendFloat32*
ベンチマーク)において劇的なパフォーマンス向上をもたらしています。
変更の背景
浮動小数点数を10進数文字列に変換する処理は、コンピュータサイエンスにおいて長年の課題でした。特に、IEEE 754浮動小数点数標準に準拠しつつ、最短かつ正確な10進数表現を得ることは複雑です。従来のアルゴリズムは、精度を保証するために多くの計算を必要とするか、あるいは最短表現を保証できないというトレードオフがありました。
Grisu3アルゴリズムは、この問題に対する効率的な解決策の一つとして提案されました。これは、浮動小数点数の内部表現(2進数)から直接、最短かつ正確な10進数表現を生成することを目指します。Go言語のstrconv
パッケージでは、既にfloat64
に対してGrisu3が採用されていましたが、このコミット以前はfloat32
には適用されていませんでした。
このコミットの背景には、float32
の文字列変換性能の改善と、浮動小数点数処理全般の効率化という目的があります。特に、Webアプリケーションやデータ処理など、浮動小数点数の文字列変換が頻繁に行われるシナリオにおいて、パフォーマンスのボトルネックを解消することが期待されます。また、extfloat.Normalize
の改善や正確な整数値のショートカットパスは、浮動小数点数の内部処理の効率を高め、全体的なパフォーマンス向上に寄与します。
前提知識の解説
1. IEEE 754 浮動小数点数標準
Go言語のfloat32
とfloat64
は、IEEE 754標準に準拠しています。この標準は、浮動小数点数を符号、指数部、仮数部の3つの要素で表現します。
- 符号 (Sign): 数値が正か負かを示す1ビット。
- 指数部 (Exponent): 数値の大きさを表す部分。バイアス形式で表現され、実際の指数にオフセットが加算されています。
- 仮数部 (Mantissa/Significand): 数値の精度を表す部分。正規化された浮動小数点数では、常に先頭に暗黙の1ビット(implicit leading bit)があると仮定されます。
float32
(単精度) は32ビットで、符号1ビット、指数部8ビット、仮数部23ビット(暗黙の1ビットを含めると24ビット)で構成されます。
float64
(倍精度) は64ビットで、符号1ビット、指数部11ビット、仮数部52ビット(暗黙の1ビットを含めると53ビット)で構成されます。
2. 浮動小数点数の正規化 (Normalization)
浮動小数点数の正規化とは、仮数部の最上位ビットが常に1になるように調整するプロセスです。これにより、仮数部の精度を最大限に活用し、同じ数値を複数の異なる表現で表す「冗長性」を排除します。例えば、0.00101 * 2^5
は 1.01 * 2^2
と正規化されます。非正規化数(denormalized numbers)は、アンダーフローを処理するために正規化されない特別なケースです。
extfloat.Normalize
関数は、この正規化処理を担当し、仮数部を左シフトして最上位ビットをセットし、それに応じて指数部を調整します。
3. Grisu3 アルゴリズム
Grisu3は、浮動小数点数を最短かつ正確な10進数文字列に変換するためのアルゴリズムです。その主な特徴は以下の通りです。
- 最短表現: 浮動小数点数を10進数に変換する際、元の浮動小数点数を一意に識別できる最小桁数の10進数表現を生成します。例えば、
0.1
は内部的には正確に表現できないため、0.1000000000000000055511151231257827021181583404541015625
のような2進数表現になりますが、Grisu3はこれを0.1
と出力します。 - 正確性: 変換された10進数文字列を再度浮動小数点数に変換すると、元の浮動小数点数と完全に一致することを保証します。
- 効率性: 他の正確な変換アルゴリズム(例: Dragon4)と比較して、Grisu3は一般的に高速です。これは、浮動小数点数の内部表現を直接操作し、多倍長整数演算を最小限に抑えることで実現されます。Grisu3は、特定の範囲の浮動小数点数に対しては非常に高速ですが、全ての浮動小数点数に対応できるわけではありません。対応できない場合は、より汎用的なアルゴリズム(例: Dragon4の変種)にフォールバックします。
4. strconv
パッケージ
Go言語のstrconv
パッケージは、基本的なデータ型(ブール値、整数、浮動小数点数)と文字列との間の変換機能を提供します。ParseFloat
は文字列を浮動小数点数に、AppendFloat
は浮動小数点数を文字列に変換する関数です。
技術的詳細
strconv/decimal.go
の変更
decimal.go
では、decimal
構造体のAssign
メソッド内で使用されるバッファのサイズが[50]byte
から[24]byte
に縮小されました。
--- a/src/pkg/strconv/decimal.go
+++ b/src/pkg/strconv/decimal.go
@@ -79,7 +79,7 @@ func trim(a *decimal) {
// Assign v to a.
func (a *decimal) Assign(v uint64) {
- var buf [50]byte
+ var buf [24]byte
// Write reversed decimal in buf.
n := 0
この変更は、uint64
の最大値(約1.8 x 10^19)が10進数で20桁であるため、その文字列表現を格納するのに50バイトは過剰であり、24バイトで十分であるという最適化です。これにより、メモリ使用量がわずかに削減されます。
strconv/extfloat.go
の変更
AssignComputeBounds
の変更
AssignComputeBounds
関数のシグネチャと実装が大きく変更されました。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -190,29 +190,24 @@ func (f *extFloat) Assign(x float64) {
f.exp -= 64
}
-// AssignComputeBounds sets f to the value of x and returns
+// AssignComputeBounds sets f to the floating point value
+// defined by mant, exp and precision given by flt. It returns
// lower, upper such that any number in the closed interval
-// [lower, upper] is converted back to x.\n-func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
-\t// Special cases.\n-\tbits := math.Float64bits(x)\n-\tflt := &float64info\n-\tneg := bits>>(flt.expbits+flt.mantbits) != 0\n-\texpBiased := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)\n-\tmant := bits & (uint64(1)<<flt.mantbits - 1)\n-\n-\tif expBiased == 0 {\n-\t\t// denormalized.\n-\t\tf.mant = mant\n-\t\tf.exp = 1 + flt.bias - int(flt.mantbits)\n-\t} else {\n-\t\tf.mant = mant | 1<<flt.mantbits\n-\t\tf.exp = expBiased + flt.bias - int(flt.mantbits)\n-\t}\n+// [lower, upper] is converted back to the same floating point number.
+func (f *extFloat) AssignComputeBounds(mant uint64, exp int, neg bool, flt *floatInfo) (lower, upper extFloat) {
+\tf.mant = mant
+\tf.exp = exp - int(flt.mantbits)
\tf.neg = neg
+\tif f.exp <= 0 && mant == (mant>>uint(-f.exp))<<uint(-f.exp) {
+\t\t// An exact integer
+\t\tf.mant >>= uint(-f.exp)
+\t\tf.exp = 0
+\t\treturn *f, *f
+\t}
+\texpBiased := exp - flt.bias
\n \tupper = extFloat{mant: 2*f.mant + 1, exp: f.exp - 1, neg: f.neg}
-\tif mant != 0 || expBiased == 1 {\n+\tif mant != 1<<flt.mantbits || expBiased == 1 {
\t\tlower = extFloat{mant: 2*f.mant - 1, exp: f.exp - 1, neg: f.neg}
\t} else {\n \t\tlower = extFloat{mant: 4*f.mant - 1, exp: f.exp - 2, neg: f.neg}
@@ -222,20 +217,38 @@ func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
変更前はfloat64
型のx
を受け取っていましたが、変更後はmant
(仮数部), exp
(指数部), neg
(符号), flt
(floatInfo
構造体) といった浮動小数点数の構成要素を直接受け取るようになりました。これにより、math.Float64bits
を呼び出してビットを分解するオーバーヘッドがなくなります。
さらに重要なのは、正確な整数値に対するショートカットパスが追加されたことです。
if f.exp <= 0 && mant == (mant>>uint(-f.exp))<<uint(-f.exp) {
// An exact integer
f.mant >>= uint(-f.exp)
f.exp = 0
return *f, *f
}
このコードは、浮動小数点数が正確な整数を表すかどうかをチェックします。f.exp <= 0
は、指数部が0以下であることを意味し、これは数値が1以下の整数(または非常に小さい非整数)であることを示唆します。mant == (mant>>uint(-f.exp))<<uint(-f.exp)
は、仮数部が指数部によって示されるシフト量で正確に割り切れる(つまり、小数部がない)ことを確認します。もし条件が満たされれば、その数値は正確な整数であり、f.mant
を適切にシフトしてf.exp
を0に設定し、lower
とupper
の境界をf
自身に設定して、以降の複雑な境界計算をスキップします。これにより、整数値の変換が高速化されます。
Normalize
関数の改善
Normalize
関数は、仮数部を正規化するための処理です。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -222,20 +217,38 @@ func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
// Normalize normalizes f so that the highest bit of the mantissa is
// set, and returns the number by which the mantissa was left-shifted.
-func (f *extFloat) Normalize() uint {
-\tif f.mant == 0 {\n \t\treturn 0
-\t}\n-\texp_before := f.exp\n-\tfor f.mant < (1 << 55) {\n-\t\tf.mant <<= 8\n-\t\tf.exp -= 8\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
-\tfor f.mant < (1 << 63) {\n-\t\tf.mant <<= 1\n-\t\tf.exp -= 1\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
-\treturn uint(exp_before - f.exp)\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
// Multiply sets f to the product f*g: the result is correctly rounded,
@@ -390,6 +403,12 @@ func (f *extFloat) ShortestDecimal(d *decimal, lower, upper *extFloat) bool {
\td.dp = 0
\td.neg = f.neg
}\n+\tif f.exp == 0 && *lower == *f && *lower == *upper {
+\t\t// an exact integer.
+\t\td.Assign(f.mant)
+\t\td.neg = f.neg
+\t\treturn true
+\t}
const minExp = -60
const maxExp = -32
upper.Normalize()
変更前は、仮数部を8ビットずつ、その後1ビットずつ左シフトして正規化していました。変更後は、より効率的な「バイナリサーチ」のようなシフト戦略を採用しています。
if mant>>(64-32) == 0 {
mant <<= 32
exp -= 32
}
if mant>>(64-16) == 0 {
mant <<= 16
exp -= 16
}
if mant>>(64-8) == 0 {
mant <<= 8
exp -= 8
}
if mant>>(64-4) == 0 {
mant <<= 4
exp -= 4
}
if mant>>(64-2) == 0 {
mant <<= 2
exp -= 2
}
if mant>>(64-1) == 0 {
mant <<m 1
exp -= 1
}
この新しいアプローチでは、まず32ビット、次に16ビット、8ビット、4ビット、2ビット、1ビットと、大きなシフト量から順にチェックし、必要に応じてシフトを行います。これにより、仮数部が63ビット目にセットされるまでのシフト回数を大幅に削減し、正規化処理を高速化します。
ShortestDecimal
の変更
ShortestDecimal
関数にも、正確な整数値に対するショートカットパスが追加されました。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -390,6 +403,12 @@ func (f *extFloat) ShortestDecimal(d *decimal, lower, upper *extFloat) bool {
\td.dp = 0
\td.neg = f.neg
}\n+\tif f.exp == 0 && *lower == *f && *lower == *upper {
+\t\t// an exact integer.
+\t\td.Assign(f.mant)
+\t\td.neg = f.neg
+\t\treturn true
+\t}
const minExp = -60
const maxExp = -32
upper.Normalize()
これはAssignComputeBounds
で追加されたショートカットと連携しており、もし浮動小数点数が正確な整数であり、その境界が自身と一致する場合(つまり、その浮動小数点数が一意の整数を表す場合)、直接decimal
型に変換して処理を終了します。これにより、Grisu3アルゴリズムの複雑な計算をスキップし、パフォーマンスを向上させます。
strconv/ftoa.go
の変更
ftoa.go
では、genericFtoa
関数内でGrisu3アルゴリズムを適用する条件が変更されました。
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -104,10 +104,10 @@ func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
\td := new(decimal)
\tif shortest {
\t\tok := false
-\t\t\tif optimize && bitSize == 64 {
+\t\t\tif optimize {
\t\t\t// Try Grisu3 algorithm.
\t\t\tf := new(extFloat)
-\t\t\t\tlower, upper := f.AssignComputeBounds(val)
+\t\t\t\tlower, upper := f.AssignComputeBounds(mant, exp, neg, flt)
\t\t\tok = f.ShortestDecimal(d, &lower, &upper)
\t\t}
\t\tif !ok {
以前はoptimize && bitSize == 64
という条件でGrisu3が適用されていましたが、bitSize == 64
の条件が削除され、単にoptimize
が真であればGrisu3が試行されるようになりました。これにより、float32
(bitSize == 32
)に対してもGrisu3アルゴリズムが適用されるようになり、コミットメッセージの「extend Grisu3 algorithm to float32」が実現されました。
また、f.AssignComputeBounds(val)
の呼び出しが、f.AssignComputeBounds(mant, exp, neg, flt)
に変更されました。これは、extfloat.go
でAssignComputeBounds
のシグネチャが変更されたことに対応するものです。
strconv/ftoa_test.go
の変更
テストファイルでは、ベンチマーク関数がリファクタリングされ、float32
に対する新しいベンチマークが追加されました。
--- a/src/pkg/strconv/ftoa_test.go
+++ b/src/pkg/strconv/ftoa_test.go
@@ -203,37 +203,23 @@ func BenchmarkFormatFloatBig(b *testing.B) {
}\n }\n \n-func BenchmarkAppendFloatDecimal(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 33909, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloat(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 339.7784, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloatExp(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, -5.09e75, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloatNegExp(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, -5.11e-95, \'g\', -1, 64)
-\t}\n-}\n-\n+func benchmarkAppendFloat(b *testing.B, f float64, fmt byte, prec, bitSize int) {
+\tdst := make([]byte, 30)
+\tfor i := 0; i < b.N; i++ {
+\t\tAppendFloat(dst[:0], f, fmt, prec, bitSize)
+\t}\n }\n \n+func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909, \'g\', -1, 64) }\n+func BenchmarkAppendFloat(b *testing.B) { benchmarkAppendFloat(b, 339.7784, \'g\', -1, 64) }\n+func BenchmarkAppendFloatExp(b *testing.B) { benchmarkAppendFloat(b, -5.09e75, \'g\', -1, 64) }\n+func BenchmarkAppendFloatNegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-95, \'g\', -1, 64) }\n func BenchmarkAppendFloatBig(b *testing.B) {\n-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 123456789123456789123456789, \'g\', -1, 64)
-\t}\n+\tbenchmarkAppendFloat(b, 123456789123456789123456789, \'g\', -1, 64)
}\n+\n+func BenchmarkAppendFloat32Integer(b *testing.B) { benchmarkAppendFloat(b, 33909, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32ExactFraction(b *testing.B) { benchmarkAppendFloat(b, 3.375, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32Point(b *testing.B) { benchmarkAppendFloat(b, 339.7784, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32Exp(b *testing.B) { benchmarkAppendFloat(b, -5.09e25, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32NegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-25, \'g\', -1, 32) }\n```
複数の`BenchmarkAppendFloat*`関数が、共通のヘルパー関数`benchmarkAppendFloat`を使用するように変更されました。これにより、ベンチマークコードの重複が削減され、可読性と保守性が向上します。
最も重要なのは、`float32`に対する新しいベンチマークが追加されたことです。
* `BenchmarkAppendFloat32Integer`
* `BenchmarkAppendFloat32ExactFraction`
* `BenchmarkAppendFloat32Point`
* `BenchmarkAppendFloat32Exp`
* `BenchmarkAppendFloat32NegExp`
これらのベンチマークは、`float32`の様々なケース(整数、正確な分数、小数点を含む数値、指数表記の数値)における`AppendFloat`のパフォーマンスを測定するために導入されました。コミットメッセージのベンチマーク結果が示すように、これらの新しいベンチマークは、`float32`の文字列変換において大幅なパフォーマンス向上を示しています。特に、`BenchmarkAppendFloat32Exp`と`BenchmarkAppendFloat32NegExp`では80%以上の改善が見られ、Grisu3アルゴリズムの`float32`への適用が非常に効果的であったことを裏付けています。
## コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。
1. **`src/pkg/strconv/extfloat.go`**:
* `AssignComputeBounds`関数のシグネチャ変更と、正確な整数値に対するショートカットパスの追加。
* `Normalize`関数の正規化ロジックの最適化(バイナリサーチのようなシフト戦略)。
* `ShortestDecimal`関数における正確な整数値のショートカットパスの追加。
2. **`src/pkg/strconv/ftoa.go`**:
* `genericFtoa`関数内で、Grisu3アルゴリズムの適用条件から`bitSize == 64`の制約を削除し、`float32`にも適用可能にした点。
* `AssignComputeBounds`の呼び出し引数の変更。
これらの変更が連携して、浮動小数点数の文字列変換の精度とパフォーマンスを向上させています。
## コアとなるコードの解説
### `extfloat.go`の`Normalize`関数(変更後)
```go
func (f *extFloat) Normalize() (shift uint) {
mant, exp := f.mant, f.exp
if mant == 0 {
return 0
}
// Fast path for common shifts
if mant>>(64-32) == 0 {
mant <<= 32
exp -= 32
}
if mant>>(64-16) == 0 {
mant <<= 16
exp -= 16
}
if mant>>(64-8) == 0 {
mant <<= 8
exp -= 8
}
if mant>>(64-4) == 0 {
mant <<= 4
exp -= 4
}
if mant>>(64-2) == 0 {
mant <<= 2
exp -= 2
}
if mant>>(64-1) == 0 {
mant <<= 1
exp -= 1
}
shift = uint(f.exp - exp) // Calculate total shift amount
f.mant, f.exp = mant, exp
return
}
このNormalize
関数は、extFloat
構造体の仮数部f.mant
を正規化します。正規化とは、仮数部の最上位ビット(63ビット目、0-indexed)が1になるように左シフトすることです。
mant, exp := f.mant, f.exp
: 現在の仮数部と指数部をローカル変数にコピーします。if mant == 0 { return 0 }
: 仮数部が0の場合は、正規化の必要がないため0を返します。- 高速パス(バイナリサーチのようなシフト):
if mant>>(64-32) == 0
: 仮数部を32ビット右シフトした結果が0の場合、仮数部の最上位ビットはまだ64-32=32ビット目より下にあることを意味します。この場合、仮数部を32ビット左シフトし、指数部を32減らします。- 同様に、16ビット、8ビット、4ビット、2ビット、1ビットと、より小さいシフト量でチェックとシフトを繰り返します。この一連の条件分岐により、仮数部が効率的に正規化されます。例えば、仮数部が非常に小さい場合でも、一度に大きなシフトを行うことで、ループ処理よりも少ないステップで正規化が完了します。
shift = uint(f.exp - exp)
: 正規化によって指数部がどれだけ変化したか(つまり、仮数部がどれだけ左シフトされたか)を計算します。f.mant, f.exp = mant, exp
: 正規化された仮数部と指数部をextFloat
構造体に書き戻します。return
: シフト量を返します。
この最適化された正規化処理は、浮動小数点数のパースや文字列変換において、内部的なビット操作の効率を高め、全体的なパフォーマンス向上に貢献します。
ftoa.go
のgenericFtoa
関数(関連部分)
func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
// ... (前略) ...
d := new(decimal)
if shortest {
ok := false
if optimize { // Changed from optimize && bitSize == 64
// Try Grisu3 algorithm.
f := new(extFloat)
// lower, upper := f.AssignComputeBounds(val) // Old call
lower, upper := f.AssignComputeBounds(mant, exp, neg, flt) // New call
ok = f.ShortestDecimal(d, &lower, &upper)
}
if !ok {
// Fallback to slower but always correct algorithm (e.g., Dragon4 variant)
// ... (後略) ...
}
}
// ... (後略) ...
}
このコードスニペットは、浮動小数点数を最短の10進数文字列に変換するロジックの一部です。
if shortest
: 最短表現が必要な場合にこのブロックに入ります。if optimize
: 最適化が有効な場合にGrisu3アルゴリズムを試行します。- 変更点: 以前は
optimize && bitSize == 64
という条件があり、Grisu3はfloat64
にのみ適用されていました。このコミットによりbitSize == 64
の条件が削除されたため、float32
(bitSize == 32
)に対してもGrisu3が試行されるようになりました。
- 変更点: 以前は
f := new(extFloat)
: 拡張浮動小数点数表現のためのextFloat
インスタンスを作成します。lower, upper := f.AssignComputeBounds(mant, exp, neg, flt)
: 浮動小数点数の値から、その値を一意に識別できる10進数表現の範囲(下限lower
と上限upper
)を計算します。ここで、extfloat.go
で変更された新しいシグネチャのAssignComputeBounds
が呼び出されます。ok = f.ShortestDecimal(d, &lower, &upper)
: Grisu3アルゴリズムを使用して、extFloat
から最短の10進数表現を計算し、decimal
構造体d
に格納します。成功すればok
はtrue
になります。if !ok
: Grisu3が適用できない(または失敗した)場合、より汎用的な(しかし遅い)アルゴリズムにフォールバックします。
この変更により、float32
の文字列変換においてもGrisu3の高速な処理が利用できるようになり、特に指数部を持つ数値や小数点以下の桁数が多い数値の変換で顕著なパフォーマンス向上が実現されました。
関連リンク
- Grisu3アルゴリズムに関する論文:
- "Printing Floating-Point Numbers Quickly and Accurately with Integers" by Florian Loitsch (2010): https://www.cs.cmu.edu/~quake-papers/fast-double-conversion.pdf (Grisuアルゴリズムの原論文)
- Go言語の
strconv
パッケージドキュメント: - IEEE 754 浮動小数点数標準:
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/strconv
ディレクトリ) - コミットメッセージとベンチマーク結果
- Grisu3アルゴリズムに関する学術論文
- IEEE 754浮動小数点数標準に関する資料
- 浮動小数点数の正規化に関する一般的な情報
- Go言語のベンチマークに関するドキュメントI have generated the detailed technical explanation in Markdown format, including all the requested sections and adhering to the specified language and detail level. I have also incorporated information about the Grisu3 algorithm, floating-point representation, and normalization based on my knowledge and the context of the commit.
I will now output the explanation to standard output.
# [インデックス 13453] ファイルの概要
このコミットは、Go言語の標準ライブラリ`strconv`パッケージにおける浮動小数点数と文字列間の変換処理、特に`float32`型に対するGrisu3アルゴリズムの拡張と、浮動小数点数の正規化処理の改善に焦点を当てています。これにより、浮動小数点数の文字列変換(`ftoa`)および文字列から浮動小数点数への変換(`atof`)のパフォーマンスが向上しています。
## コミット
commit d6147d8102b095caac3267f9864a4025650c43f8 Author: Rémy Oudompheng oudomphe@phare.normalesup.org Date: Tue Jul 10 07:44:23 2012 +0200
strconv: extend Grisu3 algorithm to float32.
Also improve extfloat.Normalize to obtain a modest performance
gain in parsing, and add a shortcut path for exact integers.
benchmark old ns/op new ns/op delta
BenchmarkAtof64Decimal 73 73 -0.54%
BenchmarkAtof64Float 91 91 -0.54%
BenchmarkAtof64FloatExp 198 180 -9.09%
BenchmarkAtof64Big 307 308 +0.33%
BenchmarkAtof32Decimal 72 72 +0.42%
BenchmarkAtof32Float 83 83 -0.72%
BenchmarkAtof32FloatExp 212 186 -12.26%
BenchmarkAtof32Random 262 250 -4.58%
BenchmarkAppendFloatDecimal 474 305 -35.65%
BenchmarkAppendFloat 497 489 -1.61%
BenchmarkAppendFloatExp 493 483 -2.03%
BenchmarkAppendFloatNegExp 481 481 +0.00%
BenchmarkAppendFloatBig 667 652 -2.25%
BenchmarkAppendFloat32Integer 338 307 -9.17%
BenchmarkAppendFloat32ExactFraction 364 439 +20.60%
BenchmarkAppendFloat32Point 1299 490 -62.28%
BenchmarkAppendFloat32Exp 2593 489 -81.14%
BenchmarkAppendFloat32NegExp 5116 481 -90.60%
R=rsc, r
CC=golang-dev, remy
https://golang.org/cl/6303087
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/d6147d8102b095caac3267f9864a4025650c43f8](https://github.com/golang/go/commit/d6147d8102b095caac3267f9864a4025650c43f8)
## 元コミット内容
このコミットは、Go言語の`strconv`パッケージにおいて、以下の主要な変更を導入しています。
1. **Grisu3アルゴリズムの`float32`への拡張**: 以前は`float64`にのみ適用されていたGrisu3アルゴリズムが、`float32`型にも適用されるようになりました。Grisu3は、浮動小数点数を最短かつ正確な10進数文字列に変換するための効率的なアルゴリズムです。
2. **`extfloat.Normalize`関数の改善**: 浮動小数点数の内部表現を正規化する`extfloat.Normalize`関数の実装が最適化されました。これにより、浮動小数点数のパース(文字列から数値への変換)において、わずかながらパフォーマンスの向上が見られます。
3. **正確な整数値に対するショートカットパスの追加**: 浮動小数点数が正確な整数値を表す場合、その変換処理を高速化するための特別なパスが追加されました。
これらの変更は、ベンチマーク結果に示されているように、特に`float32`の文字列変換(`AppendFloat32*`ベンチマーク)において劇的なパフォーマンス向上をもたらしています。
## 変更の背景
浮動小数点数を10進数文字列に変換する処理は、コンピュータサイエンスにおいて長年の課題でした。特に、IEEE 754浮動小数点数標準に準拠しつつ、最短かつ正確な10進数表現を得ることは複雑です。従来のアルゴリズムは、精度を保証するために多くの計算を必要とするか、あるいは最短表現を保証できないというトレードオフがありました。
Grisu3アルゴリズムは、この問題に対する効率的な解決策の一つとして提案されました。これは、浮動小数点数の内部表現(2進数)から直接、最短かつ正確な10進数表現を生成することを目指します。Go言語の`strconv`パッケージでは、既に`float64`に対してGrisu3が採用されていましたが、このコミット以前は`float32`には適用されていませんでした。
このコミットの背景には、`float32`の文字列変換性能の改善と、浮動小数点数処理全般の効率化という目的があります。特に、Webアプリケーションやデータ処理など、浮動小数点数の文字列変換が頻繁に行われるシナリオにおいて、パフォーマンスのボトルネックを解消することが期待されます。また、`extfloat.Normalize`の改善や正確な整数値のショートカットパスは、浮動小数点数の内部処理の効率を高め、全体的なパフォーマンス向上に寄与します。
## 前提知識の解説
### 1. IEEE 754 浮動小数点数標準
Go言語の`float32`と`float64`は、IEEE 754標準に準拠しています。この標準は、浮動小数点数を符号、指数部、仮数部の3つの要素で表現します。
* **符号 (Sign)**: 数値が正か負かを示す1ビット。
* **指数部 (Exponent)**: 数値の大きさを表す部分。バイアス形式で表現され、実際の指数にオフセットが加算されています。
* **仮数部 (Mantissa/Significand)**: 数値の精度を表す部分。正規化された浮動小数点数では、常に先頭に暗黙の1ビット(implicit leading bit)があると仮定されます。
`float32` (単精度) は32ビットで、符号1ビット、指数部8ビット、仮数部23ビット(暗黙の1ビットを含めると24ビット)で構成されます。
`float64` (倍精度) は64ビットで、符号1ビット、指数部11ビット、仮数部52ビット(暗黙の1ビットを含めると53ビット)で構成されます。
### 2. 浮動小数点数の正規化 (Normalization)
浮動小数点数の正規化とは、仮数部の最上位ビットが常に1になるように調整するプロセスです。これにより、仮数部の精度を最大限に活用し、同じ数値を複数の異なる表現で表す「冗長性」を排除します。例えば、`0.00101 * 2^5` は `1.01 * 2^2` と正規化されます。非正規化数(denormalized numbers)は、アンダーフローを処理するために正規化されない特別なケースです。
`extfloat.Normalize`関数は、この正規化処理を担当し、仮数部を左シフトして最上位ビットをセットし、それに応じて指数部を調整します。
### 3. Grisu3 アルゴリズム
Grisu3は、浮動小数点数を最短かつ正確な10進数文字列に変換するためのアルゴリズムです。その主な特徴は以下の通りです。
* **最短表現**: 浮動小数点数を10進数に変換する際、元の浮動小数点数を一意に識別できる最小桁数の10進数表現を生成します。例えば、`0.1`は内部的には正確に表現できないため、`0.1000000000000000055511151231257827021181583404541015625`のような2進数表現になりますが、Grisu3はこれを`0.1`と出力します。
* **正確性**: 変換された10進数文字列を再度浮動小数点数に変換すると、元の浮動小数点数と完全に一致することを保証します。
* **効率性**: 他の正確な変換アルゴリズム(例: Dragon4)と比較して、Grisu3は一般的に高速です。これは、浮動小数点数の内部表現を直接操作し、多倍長整数演算を最小限に抑えることで実現されます。Grisu3は、特定の範囲の浮動小数点数に対しては非常に高速ですが、全ての浮動小数点数に対応できるわけではありません。対応できない場合は、より汎用的なアルゴリズム(例: Dragon4の変種)にフォールバックします。
### 4. `strconv`パッケージ
Go言語の`strconv`パッケージは、基本的なデータ型(ブール値、整数、浮動小数点数)と文字列との間の変換機能を提供します。`ParseFloat`は文字列を浮動小数点数に、`AppendFloat`は浮動小数点数を文字列に変換する関数です。
## 技術的詳細
### `strconv/decimal.go`の変更
`decimal.go`では、`decimal`構造体の`Assign`メソッド内で使用されるバッファのサイズが`[50]byte`から`[24]byte`に縮小されました。
```diff
--- a/src/pkg/strconv/decimal.go
+++ b/src/pkg/strconv/decimal.go
@@ -79,7 +79,7 @@ func trim(a *decimal) {
// Assign v to a.
func (a *decimal) Assign(v uint64) {
- var buf [50]byte
+ var buf [24]byte
// Write reversed decimal in buf.
n := 0
この変更は、uint64
の最大値(約1.8 x 10^19)が10進数で20桁であるため、その文字列表現を格納するのに50バイトは過剰であり、24バイトで十分であるという最適化です。これにより、メモリ使用量がわずかに削減されます。
strconv/extfloat.go
の変更
AssignComputeBounds
の変更
AssignComputeBounds
関数のシグネチャと実装が大きく変更されました。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -190,29 +190,24 @@ func (f *extFloat) Assign(x float64) {
f.exp -= 64
}
-// AssignComputeBounds sets f to the value of x and returns
+// AssignComputeBounds sets f to the floating point value
+// defined by mant, exp and precision given by flt. It returns
// lower, upper such that any number in the closed interval
-// [lower, upper] is converted back to x.\n-func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
-\t// Special cases.\n-\tbits := math.Float64bits(x)\n-\tflt := &float64info\n-\tneg := bits>>(flt.expbits+flt.mantbits) != 0\n-\texpBiased := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)\n-\tmant := bits & (uint64(1)<<flt.mantbits - 1)\n-\n-\tif expBiased == 0 {\n-\t\t// denormalized.\n-\t\tf.mant = mant\n-\t\tf.exp = 1 + flt.bias - int(flt.mantbits)\n-\t} else {\n-\t\tf.mant = mant | 1<<flt.mantbits\n-\t\tf.exp = expBiased + flt.bias - int(flt.mantbits)\n-\t}\n+// [lower, upper] is converted back to the same floating point number.
+func (f *extFloat) AssignComputeBounds(mant uint64, exp int, neg bool, flt *floatInfo) (lower, upper extFloat) {
+\tf.mant = mant
+\tf.exp = exp - int(flt.mantbits)
\tf.neg = neg
+\tif f.exp <= 0 && mant == (mant>>uint(-f.exp))<<uint(-f.exp) {
+\t\t// An exact integer
+\t\tf.mant >>= uint(-f.exp)
+\t\tf.exp = 0
+\t\treturn *f, *f
+\t}
+\texpBiased := exp - flt.bias
\n \tupper = extFloat{mant: 2*f.mant + 1, exp: f.exp - 1, neg: f.neg}
-\tif mant != 0 || expBiased == 1 {\n+\tif mant != 1<<flt.mantbits || expBiased == 1 {
\t\tlower = extFloat{mant: 2*f.mant - 1, exp: f.exp - 1, neg: f.neg}
\t} else {\n \t\tlower = extFloat{mant: 4*f.mant - 1, exp: f.exp - 2, neg: f.neg}
@@ -222,20 +217,38 @@ func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
変更前はfloat64
型のx
を受け取っていましたが、変更後はmant
(仮数部), exp
(指数部), neg
(符号), flt
(floatInfo
構造体) といった浮動小数点数の構成要素を直接受け取るようになりました。これにより、math.Float64bits
を呼び出してビットを分解するオーバーヘッドがなくなります。
さらに重要なのは、正確な整数値に対するショートカットパスが追加されたことです。
if f.exp <= 0 && mant == (mant>>uint(-f.exp))<<uint(-f.exp) {
// An exact integer
f.mant >>= uint(-f.exp)
f.exp = 0
return *f, *f
}
このコードは、浮動小数点数が正確な整数を表すかどうかをチェックします。f.exp <= 0
は、指数部が0以下であることを意味し、これは数値が1以下の整数(または非常に小さい非整数)であることを示唆します。mant == (mant>>uint(-f.exp))<<uint(-f.exp)
は、仮数部が指数部によって示されるシフト量で正確に割り切れる(つまり、小数部がない)ことを確認します。もし条件が満たされれば、その数値は正確な整数であり、f.mant
を適切にシフトしてf.exp
を0に設定し、lower
とupper
の境界をf
自身に設定して、以降の複雑な境界計算をスキップします。これにより、整数値の変換が高速化されます。
Normalize
関数の改善
Normalize
関数は、仮数部を正規化するための処理です。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -222,20 +217,38 @@ func (f *extFloat) AssignComputeBounds(x float64) (lower, upper extFloat) {
// Normalize normalizes f so that the highest bit of the mantissa is
// set, and returns the number by which the mantissa was left-shifted.
-func (f *extFloat) Normalize() uint {
-\tif f.mant == 0 {\n \t\treturn 0
-\t}\n-\texp_before := f.exp\n-\tfor f.mant < (1 << 55) {\n-\t\tf.mant <<= 8\n-\t\tf.exp -= 8\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
-\tfor f.mant < (1 << 63) {\n-\t\tf.mant <<= 1\n-\t\tf.exp -= 1\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
-\treturn uint(exp_before - f.exp)\n+\tshift = uint(f.exp - exp)
+\tf.mant, f.exp = mant, exp
+\treturn
}
// Multiply sets f to the product f*g: the result is correctly rounded,
@@ -390,6 +403,12 @@ func (f *extFloat) ShortestDecimal(d *decimal, lower, upper *extFloat) bool {
\td.dp = 0
\td.neg = f.neg
}\n+\tif f.exp == 0 && *lower == *f && *lower == *upper {
+\t\t// an exact integer.
+\t\td.Assign(f.mant)
+\t\td.neg = f.neg
+\t\treturn true
+\t}
const minExp = -60
const maxExp = -32
upper.Normalize()
変更前は、仮数部を8ビットずつ、その後1ビットずつ左シフトして正規化していました。変更後は、より効率的な「バイナリサーチ」のようなシフト戦略を採用しています。
if mant>>(64-32) == 0 {
mant <<= 32
exp -= 32
}
if mant>>(64-16) == 0 {
mant <<= 16
exp -= 16
}
if mant>>(64-8) == 0 {
mant <<= 8
exp -= 8
}
if mant>>(64-4) == 0 {
mant <<= 4
exp -= 4
}
if mant>>(64-2) == 0 {
mant <<= 2
exp -= 2
}
if mant>>(64-1) == 0 {
mant <<= 1
exp -= 1
}
この新しいアプローチでは、まず32ビット、次に16ビット、8ビット、4ビット、2ビット、1ビットと、大きなシフト量から順にチェックし、必要に応じてシフトを行います。これにより、仮数部が63ビット目にセットされるまでのシフト回数を大幅に削減し、正規化処理を高速化します。
ShortestDecimal
の変更
ShortestDecimal
関数にも、正確な整数値に対するショートカットパスが追加されました。
--- a/src/pkg/strconv/extfloat.go
+++ b/src/pkg/strconv/extfloat.go
@@ -390,6 +403,12 @@ func (f *extFloat) ShortestDecimal(d *decimal, lower, upper *extFloat) bool {
\td.dp = 0
\td.neg = f.neg
}\n+\tif f.exp == 0 && *lower == *f && *lower == *upper {
+\t\t// an exact integer.
+\t\td.Assign(f.mant)
+\t\td.neg = f.neg
+\t\treturn true
+\t}
const minExp = -60
const maxExp = -32
upper.Normalize()
これはAssignComputeBounds
で追加されたショートカットと連携しており、もし浮動小数点数が正確な整数であり、その境界が自身と一致する場合(つまり、その浮動小数点数が一意の整数を表す場合)、直接decimal
型に変換して処理を終了します。これにより、Grisu3アルゴリズムの複雑な計算をスキップし、パフォーマンスを向上させます。
strconv/ftoa.go
の変更
ftoa.go
では、genericFtoa
関数内でGrisu3アルゴリズムを適用する条件が変更されました。
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -104,10 +104,10 @@ func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
\td := new(decimal)
\tif shortest {
\t\tok := false
-\t\t\tif optimize && bitSize == 64 {
+\t\t\tif optimize {
\t\t\t// Try Grisu3 algorithm.
\t\t\tf := new(extFloat)
-\t\t\t\tlower, upper := f.AssignComputeBounds(val)
+\t\t\t\tlower, upper := f.AssignComputeBounds(mant, exp, neg, flt)
\t\t\tok = f.ShortestDecimal(d, &lower, &upper)
\t\t}
\t\tif !ok {
以前はoptimize && bitSize == 64
という条件でGrisu3が適用されていましたが、bitSize == 64
の条件が削除され、単にoptimize
が真であればGrisu3が試行されるようになりました。これにより、float32
(bitSize == 32
)に対してもGrisu3アルゴリズムが適用されるようになり、コミットメッセージの「extend Grisu3 algorithm to float32」が実現されました。
また、f.AssignComputeBounds(val)
の呼び出しが、f.AssignComputeBounds(mant, exp, neg, flt)
に変更されました。これは、extfloat.go
でAssignComputeBounds
のシグネチャが変更されたことに対応するものです。
strconv/ftoa_test.go
の変更
テストファイルでは、ベンチマーク関数がリファクタリングされ、float32
に対する新しいベンチマークが追加されました。
--- a/src/pkg/strconv/ftoa_test.go
+++ b/src/pkg/strconv/ftoa_test.go
@@ -203,37 +203,23 @@ func BenchmarkFormatFloatBig(b *testing.B) {
}\n }\n \n-func BenchmarkAppendFloatDecimal(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 33909, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloat(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 339.7784, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloatExp(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, -5.09e75, \'g\', -1, 64)
-\t}\n-}\n-\n-func BenchmarkAppendFloatNegExp(b *testing.B) {
-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, -5.11e-95, \'g\', -1, 64)
-\t}\n-}\n-\n+func benchmarkAppendFloat(b *testing.B, f float64, fmt byte, prec, bitSize int) {
+\tdst := make([]byte, 30)
+\tfor i := 0; i < b.N; i++ {
+\t\tAppendFloat(dst[:0], f, fmt, prec, bitSize)
+\t}\n }\n \n+func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909, \'g\', -1, 64) }\n+func BenchmarkAppendFloat(b *testing.B) { benchmarkAppendFloat(b, 339.7784, \'g\', -1, 64) }\n+func BenchmarkAppendFloatExp(b *testing.B) { benchmarkAppendFloat(b, -5.09e75, \'g\', -1, 64) }\n+func BenchmarkAppendFloatNegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-95, \'g\', -1, 64) }\n func BenchmarkAppendFloatBig(b *testing.B) {\n-\tdst := make([]byte, 0, 30)
-\tfor i := 0; i < b.N; i++ {
-\t\tAppendFloat(dst, 123456789123456789123456789, \'g\', -1, 64)
-\t}\n+\tbenchmarkAppendFloat(b, 123456789123456789123456789, \'g\', -1, 64)
}\n+\n+func BenchmarkAppendFloat32Integer(b *testing.B) { benchmarkAppendFloat(b, 33909, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32ExactFraction(b *testing.B) { benchmarkAppendFloat(b, 3.375, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32Point(b *testing.B) { benchmarkAppendFloat(b, 339.7784, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32Exp(b *testing.B) { benchmarkAppendFloat(b, -5.09e25, \'g\', -1, 32) }\n+func BenchmarkAppendFloat32NegExp(b *testing.B) { benchmarkAppendFloat(b, -5.11e-25, \'g\', -1, 32) }\n```
複数の`BenchmarkAppendFloat*`関数が、共通のヘルパー関数`benchmarkAppendFloat`を使用するように変更されました。これにより、ベンチマークコードの重複が削減され、可読性と保守性が向上します。
最も重要なのは、`float32`に対する新しいベンチマークが追加されたことです。
* `BenchmarkAppendFloat32Integer`
* `BenchmarkAppendFloat32ExactFraction`
* `BenchmarkAppendFloat32Point`
* `BenchmarkAppendFloat32Exp`
* `BenchmarkAppendFloat32NegExp`
これらのベンチマークは、`float32`の様々なケース(整数、正確な分数、小数点を含む数値、指数表記の数値)における`AppendFloat`のパフォーマンスを測定するために導入されました。コミットメッセージのベンチマーク結果が示すように、これらの新しいベンチマークは、`float32`の文字列変換において大幅なパフォーマンス向上を示しています。特に、`BenchmarkAppendFloat32Exp`と`BenchmarkAppendFloat32NegExp`では80%以上の改善が見られ、Grisu3アルゴリズムの`float32`への適用が非常に効果的であったことを裏付けています。
## コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。
1. **`src/pkg/strconv/extfloat.go`**:
* `AssignComputeBounds`関数のシグネチャ変更と、正確な整数値に対するショートカットパスの追加。
* `Normalize`関数の正規化ロジックの最適化(バイナリサーチのようなシフト戦略)。
* `ShortestDecimal`関数における正確な整数値のショートカットパスの追加。
2. **`src/pkg/strconv/ftoa.go`**:
* `genericFtoa`関数内で、Grisu3アルゴリズムの適用条件から`bitSize == 64`の制約を削除し、`float32`にも適用可能にした点。
* `AssignComputeBounds`の呼び出し引数の変更。
これらの変更が連携して、浮動小数点数の文字列変換の精度とパフォーマンスを向上させています。
## コアとなるコードの解説
### `extfloat.go`の`Normalize`関数(変更後)
```go
func (f *extFloat) Normalize() (shift uint) {
mant, exp := f.mant, f.exp
if mant == 0 {
return 0
}
// Fast path for common shifts
if mant>>(64-32) == 0 {
mant <<= 32
exp -= 32
}
if mant>>(64-16) == 0 {
mant <<= 16
exp -= 16
}
if mant>>(64-8) == 0 {
mant <<= 8
exp -= 8
}
if mant>>(64-4) == 0 {
mant <<= 4
exp -= 4
}
if mant>>(64-2) == 0 {
mant <<= 2
exp -= 2
}
if mant>>(64-1) == 0 {
mant <<= 1
exp -= 1
}
shift = uint(f.exp - exp) // Calculate total shift amount
f.mant, f.exp = mant, exp
return
}
このNormalize
関数は、extFloat
構造体の仮数部f.mant
を正規化します。正規化とは、仮数部の最上位ビット(63ビット目、0-indexed)が1になるように左シフトすることです。
mant, exp := f.mant, f.exp
: 現在の仮数部と指数部をローカル変数にコピーします。if mant == 0 { return 0 }
: 仮数部が0の場合は、正規化の必要がないため0を返します。- 高速パス(バイナリサーチのようなシフト):
if mant>>(64-32) == 0
: 仮数部を32ビット右シフトした結果が0の場合、仮数部の最上位ビットはまだ64-32=32ビット目より下にあることを意味します。この場合、仮数部を32ビット左シフトし、指数部を32減らします。- 同様に、16ビット、8ビット、4ビット、2ビット、1ビットと、より小さいシフト量でチェックとシフトを繰り返します。この一連の条件分岐により、仮数部が効率的に正規化されます。例えば、仮数部が非常に小さい場合でも、一度に大きなシフトを行うことで、ループ処理よりも少ないステップで正規化が完了します。
shift = uint(f.exp - exp)
: 正規化によって指数部がどれだけ変化したか(つまり、仮数部がどれだけ左シフトされたか)を計算します。f.mant, f.exp = mant, exp
: 正規化された仮数部と指数部をextFloat
構造体に書き戻します。return
: シフト量を返します。
この最適化された正規化処理は、浮動小数点数のパースや文字列変換において、内部的なビット操作の効率を高め、全体的なパフォーマンス向上に貢献します。
ftoa.go
のgenericFtoa
関数(関連部分)
func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte {
// ... (前略) ...
d := new(decimal)
if shortest {
ok := false
if optimize { // Changed from optimize && bitSize == 64
// Try Grisu3 algorithm.
f := new(extFloat)
// lower, upper := f.AssignComputeBounds(val) // Old call
lower, upper := f.AssignComputeBounds(mant, exp, neg, flt) // New call
ok = f.ShortestDecimal(d, &lower, &upper)
}
if !ok {
// Fallback to slower but always correct algorithm (e.g., Dragon4 variant)
// ... (後略) ...
}
}
// ... (後略) ...
}
このコードスニペットは、浮動小数点数を最短の10進数文字列に変換するロジックの一部です。
if shortest
: 最短表現が必要な場合にこのブロックに入ります。if optimize
: 最適化が有効な場合にGrisu3アルゴリズムを試行します。- 変更点: 以前は
optimize && bitSize == 64
という条件があり、Grisu3はfloat64
にのみ適用されていました。このコミットによりbitSize == 64
の条件が削除されたため、float32
(bitSize == 32
)に対してもGrisu3が試行されるようになりました。
- 変更点: 以前は
f := new(extFloat)
: 拡張浮動小数点数表現のためのextFloat
インスタンスを作成します。lower, upper := f.AssignComputeBounds(mant, exp, neg, flt)
: 浮動小数点数の値から、その値を一意に識別できる10進数表現の範囲(下限lower
と上限upper
)を計算します。ここで、extfloat.go
で変更された新しいシグネチャのAssignComputeBounds
が呼び出されます。ok = f.ShortestDecimal(d, &lower, &upper)
: Grisu3アルゴリズムを使用して、extFloat
から最短の10進数表現を計算し、decimal
構造体d
に格納します。成功すればok
はtrue
になります。if !ok
: Grisu3が適用できない(または失敗した)場合、より汎用的な(しかし遅い)アルゴリズムにフォールバックします。
この変更により、float32
の文字列変換においてもGrisu3の高速な処理が利用できるようになり、特に指数部を持つ数値や小数点以下の桁数が多い数値の変換で顕著なパフォーマンス向上が実現されました。
関連リンク
- Grisu3アルゴリズムに関する論文:
- "Printing Floating-Point Numbers Quickly and Accurately with Integers" by Florian Loitsch (2010): https://www.cs.cmu.edu/~quake-papers/fast-double-conversion.pdf (Grisuアルゴリズムの原論文)
- Go言語の
strconv
パッケージドキュメント: - IEEE 754 浮動小数点数標準:
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/strconv
ディレクトリ) - コミットメッセージとベンチマーク結果
- Grisu3アルゴリズムに関する学術論文
- IEEE 754浮動小数点数標準に関する資料
- 浮動小数点数の正規化に関する一般的な情報
- Go言語のベンチマークに関するドキュメント