[インデックス 10644] ファイルの概要
このコミットでは、Go言語の標準ライブラリである strconv
パッケージ内の浮動小数点数から文字列への変換(ftoa
)に関するパフォーマンス改善が行われています。具体的には、以下のファイルが変更されました。
src/pkg/strconv/ftoa.go
: 浮動小数点数変換の主要ロジックが含まれるファイル。FormatFloat
およびAppendFloat
関数、ならびにそれらが内部で利用するヘルパー関数のシグネチャと実装が変更されました。src/pkg/strconv/ftoa_test.go
:ftoa.go
の変更に伴い、AppendFloat
のパフォーマンスを測定するための新しいベンチマークが追加されました。既存のベンチマーク名も変更されています。src/pkg/strconv/itoa.go
: 整数変換に関するファイルですが、formatBits
関数の引数名がnegative
からneg
に変更されるという軽微な修正が含まれています。これは、おそらくコードベース全体での命名規則の統一を目的としたものです。
コミット
commit 127b5a66b1e350ab6a3626a81cd4a7cc7fcaf100
Author: Robert Griesemer <gri@golang.org>
Date: Wed Dec 7 10:30:27 2011 -0800
strconv: faster float conversion
- added AppendFloatX benchmarks
- 2% to 13% better performance
- check for illegal bitSize
benchmark old ns/op new ns/op delta
strconv_test.BenchmarkFormatFloatDecimal 2993 2733 -8.69%
strconv_test.BenchmarkFormatFloat 3384 3141 -7.18%
strconv_test.BenchmarkFormatFloatExp 9192 9010 -1.98%
strconv_test.BenchmarkFormatFloatBig 3279 3207 -2.20%
strconv_test.BenchmarkAppendFloatDecimal 2837 2478 -12.65%
strconv_test.BenchmarkAppendFloat 3196 2928 -8.39%
strconv_test.BenchmarkAppendFloatExp 9028 8773 -2.82%
strconv_test.BenchmarkAppendFloatBig 3151 2782 -11.71%
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5448122
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/127b5a66b1e350ab6a3626a81cd4a7cc7fcaf100
元コミット内容
strconv: faster float conversion
- added AppendFloatX benchmarks
- 2% to 13% better performance
- check for illegal bitSize
benchmark old ns/op new ns/op delta
strconv_test.BenchmarkFormatFloatDecimal 2993 2733 -8.69%
strconv_test.BenchmarkFormatFloat 3384 3141 -7.18%
strconv_test.BenchmarkFormatFloatExp 9192 9010 -1.98%
strconv_test.BenchmarkFormatFloatBig 3279 3207 -2.20%
strconv_test.BenchmarkAppendFloatDecimal 2837 2478 -12.65%
strconv_test.BenchmarkAppendFloat 3196 2928 -8.39%
strconv_test.BenchmarkAppendFloatExp 9028 8773 -2.82%
strconv_test.BenchmarkAppendFloatBig 3151 2782 -11.71%
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5448122
変更の背景
このコミットの主な目的は、Go言語の strconv
パッケージにおける浮動小数点数から文字列への変換処理のパフォーマンスを向上させることです。特に、FormatFloat
や AppendFloat
といった関数が対象となっています。
Go言語では、文字列(string
)は不変なバイトシーケンスであり、バイトスライス([]byte
)は可変なバイトシーケンスです。文字列の結合や変更は、多くの場合、新しい文字列の割り当てとコピーを伴うため、パフォーマンスに影響を与える可能性があります。特に、数値から文字列への変換のような頻繁に呼び出される関数では、わずかなアロケーションの削減でも全体的なアプリケーションの応答性やスループットに大きな改善をもたらすことがあります。
このコミット以前の strconv
パッケージの浮動小数点数変換関数は、おそらく内部で文字列を生成し、それを返すか、既存の文字列に結合していました。このアプローチは、特に大量の変換を行う場合に、ガベージコレクションの負荷を増大させ、パフォーマンスのボトルネックとなる可能性がありました。
コミットメッセージに示されているベンチマーク結果は、この変更によって FormatFloat
および AppendFloat
の両方で2%から13%の性能向上が見られたことを明確に示しており、この最適化が成功したことを裏付けています。
前提知識の解説
Go言語における string
と []byte
Go言語において、string
型と []byte
型はバイトのシーケンスを表しますが、その性質は大きく異なります。
string
: 不変(immutable)なバイトシーケンスです。一度作成されると、その内容は変更できません。文字列の結合や部分文字列の抽出などの操作は、新しい文字列のメモリ割り当てとデータのコピーを伴います。これは、文字列がハッシュマップのキーとして安全に使用できるなど、多くの利点をもたらしますが、頻繁な変更や結合が必要な場合にはパフォーマンスオーバーヘッドとなることがあります。[]byte
: 可変(mutable)なバイトスライスです。その内容は変更可能であり、既存のメモリ領域を再利用したり、必要に応じて拡張したりすることができます。append
関数を使用することで、効率的にデータを追加していくことが可能です。パフォーマンスが重要な場面では、文字列の代わりにバイトスライスを操作し、最終的に必要な場合にのみstring()
変換を行うことが推奨されます。
このコミットの変更は、まさにこの string
と []byte
の特性を活かし、[]byte
を直接操作することでメモリ割り当てとコピーのオーバーヘッドを削減することを目的としています。
浮動小数点数の表現(IEEE 754)
コンピュータにおける浮動小数点数は、通常、IEEE 754標準に従って表現されます。Go言語の float32
は単精度(32ビット)、float64
は倍精度(64ビット)の浮動小数点数に対応します。
- 符号 (Sign): 数値が正か負かを示す1ビット。
- 指数部 (Exponent): 数値のスケール(桁)を示す部分。
- 仮数部 (Mantissa/Fraction): 数値の精度(有効数字)を示す部分。
これらのビット列を操作して浮動小数点数を表現し、またその逆の変換を行うのが math.Float32bits
や math.Float64bits
といった関数です。strconv
パッケージは、これらの内部表現を人間が読める十進数表記の文字列に変換する役割を担います。
strconv
パッケージ
strconv
パッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(数値、真偽値など)と文字列との間の変換機能を提供します。例えば、Atoi
(ASCII to Integer)、Itoa
(Integer to ASCII)、ParseFloat
、FormatFloat
などがあります。これらの関数は、設定ファイルの読み込み、ユーザー入力の解析、データのシリアライズなど、様々な場面で利用されます。
ベンチマークの読み方
Go言語のベンチマークは、testing
パッケージを使用して記述され、go test -bench=.
コマンドで実行されます。ベンチマーク結果は通常、以下の形式で表示されます。
benchmark_name old_ns/op new_ns/op delta
benchmark_name
: ベンチマーク関数の名前。old_ns/op
: 変更前の1操作あたりのナノ秒(ns)。new_ns/op
: 変更後の1操作あたりのナノ秒(ns)。delta
: 性能変化の割合。負の値は性能向上を示します。
このコミットのベンチマーク結果は、new_ns/op
が old_ns/op
よりも小さく、delta
が負の値であることから、すべてのテストケースで性能が向上していることを示しています。
技術的詳細
このコミットの技術的な核心は、浮動小数点数から文字列への変換処理において、中間的な文字列アロケーションを極力排除し、バイトスライス([]byte
)への直接書き込みに切り替えた点にあります。
-
FormatFloat
とAppendFloat
の統合と最適化:- 以前は
FormatFloat
が文字列を返し、AppendFloat
がその文字列を既存のバイトスライスにappend
していました。 - 変更後、
FormatFloat
は内部的にAppendFloat
と同様のロジックを使用し、make([]byte, 0, 16)
のように初期容量を持つ空のバイトスライスを生成し、そこに結果を書き込んでからstring()
に変換して返します。 AppendFloat
は、渡されたdst []byte
に直接結果を追記し、拡張されたバイトスライスを返します。これにより、FormatFloat
が生成した文字列を再度バイトスライスに変換するオーバーヘッドがなくなります。
- 以前は
-
genericFtoa
関数のシグネチャ変更と役割の拡大:- 旧:
func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string
bits
(uint64): 浮動小数点数のビット表現。- 戻り値:
string
- 新:
func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte
dst
([]byte): 結果を追記するバイトスライス。val
(float64): 変換対象の浮動小数点数値。bitSize
(int): 変換対象がfloat32
(32) かfloat64
(64) かを示す。- 戻り値: 拡張されたバイトスライス。
genericFtoa
は、val float64
とbitSize
を受け取り、内部でmath.Float32bits
またはmath.Float64bits
を使用してbits uint64
を生成するようになりました。これにより、FormatFloat
やAppendFloat
から直接float64
値を渡せるようになり、呼び出し側のコードが簡素化されました。- 不正な
bitSize
が渡された場合にはpanic
を発生させるようになりました。 Inf
(無限大) やNaN
(非数) の表現も、直接dst
に追記する形に変更されました。
- 旧:
-
内部ヘルパー関数 (
fmtB
,fmtE
,fmtF
) の変更:- これらの関数も、
genericFtoa
と同様に、結果を書き込むdst []byte
を引数として受け取り、拡張された[]byte
を返すようにシグネチャが変更されました。 - これにより、浮動小数点数の各フォーマット(指数表記、固定小数点表記、バイナリ表記)の生成過程で、中間的な文字列生成を避け、直接バイトスライスに文字を書き込むことが可能になりました。特に
fmtE
やfmtF
では、make([]byte, ...)
で一時的なバッファを作成していた箇所が、直接dst
にappend
する形に置き換えられています。例えば、fmtE
の指数部の桁数計算と書き込みロジックは、固定サイズの小さなバッファ[3]byte
を利用し、それを最終的にdst
にappend
する形に最適化されています。
- これらの関数も、
-
itoa.go
の軽微な変更:formatBits
関数の引数名negative
がneg
に変更されました。これは機能的な変更ではなく、コードの可読性や一貫性を向上させるためのリファクタリングと考えられます。
これらの変更により、浮動小数点数変換のパイプライン全体でメモリ割り当てが削減され、ガベージコレクションの頻度が低下し、結果としてパフォーマンスが向上しました。
コアとなるコードの変更箇所
src/pkg/strconv/ftoa.go
の FormatFloat
と AppendFloat
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -45,20 +45,30 @@ var float64info = floatInfo{52, 11, -1023}
// Ftoa32(f) is not the same as Ftoa64(float32(f)),
// because correct rounding and the number of digits
// needed to identify f depend on the precision of the representation.
-func FormatFloat(f float64, fmt byte, prec int, n int) string {
- if n == 32 {
- return genericFtoa(uint64(math.Float32bits(float32(f))), fmt, prec, &float32info)
- }
- return genericFtoa(math.Float64bits(f), fmt, prec, &float64info)
+func FormatFloat(f float64, fmt byte, prec, bitSize int) string {
+ return string(genericFtoa(make([]byte, 0, 16), f, fmt, prec, bitSize))
}
// AppendFloat appends the string form of the floating-point number f,
// as generated by FormatFloat, to dst and returns the extended buffer.
-func AppendFloat(dst []byte, f float64, fmt byte, prec int, n int) []byte {
- return append(dst, FormatFloat(f, fmt, prec, n)...)
+func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte {
+ return genericFtoa(dst, f, fmt, prec, bitSize)
}
src/pkg/strconv/ftoa.go
の genericFtoa
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -66,13 +76,16 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
// Pick off easy binary format.
if fmt == 'b' {
- return fmtB(neg, mant, exp, flt)
+ return fmtB(dst, neg, mant, exp, flt)
}
// Create exact decimal representation.
@@ -127,9 +140,9 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
switch fmt {
case 'e', 'E':
- return fmtE(neg, d, prec, fmt)
+ return fmtE(dst, neg, d, prec, fmt)
case 'f':
- return fmtF(neg, d, prec)
+ return fmtF(dst, neg, d, prec)
case 'g', 'G':
// trailing fractional zeros in 'e' form will be trimmed.
eprec := prec
@@ -147,15 +160,16 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
if prec > d.nd {
prec = d.nd
}
- return fmtE(neg, d, prec-1, fmt+'e'-'g')
+ return fmtE(dst, neg, d, prec-1, fmt+'e'-'g')
}
if prec > d.dp {
prec = d.nd
}
- return fmtF(neg, d, max(prec-d.dp, 0))
+ return fmtF(dst, neg, d, max(prec-d.dp, 0))
}
- return "%" + string(fmt)
+ // unknown format
+ return append(dst, '%', fmt)
}
src/pkg/strconv/ftoa.go
の fmtE
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -250,121 +264,103 @@ func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) {
}
// %e: -d.ddddde±dd
-func fmtE(neg bool, d *decimal, prec int, fmt byte) string {
- buf := make([]byte, 3+max(prec, 0)+30) // "-0." + prec digits + exp
- w := 0 // write index
-
+func fmtE(dst []byte, neg bool, d *decimal, prec int, fmt byte) []byte {
// sign
if neg {
- buf[w] = '-'
- w++
+ dst = append(dst, '-')
}
// first digit
if d.nd == 0 {
- buf[w] = '0'
+ ch := byte('0')
+ if d.nd != 0 {
+ ch = d.d[0]
+ }
+ dst = append(dst, ch)
} else {
- buf[w] = d.d[0]
+ dst = append(dst, d.d[0])
}
- w++
// .moredigits
if prec > 0 {
- buf[w] = '.'
- w++
- for i := 0; i < prec; i++ {
- if 1+i < d.nd {
- buf[w] = d.d[1+i]
- } else {
- buf[w] = '0'
+ dst = append(dst, '.')
+ for i := 1; i <= prec; i++ {
+ ch := byte('0')
+ if i < d.nd {
+ ch = d.d[i]
}
- w++
+ dst = append(dst, ch)
}
}
// e±
- buf[w] = fmt
- w++
+ dst = append(dst, fmt)
exp := d.dp - 1
if d.nd == 0 { // special case: 0 has exponent 0
exp = 0
}
if exp < 0 {
- buf[w] = '-'
+ ch := byte('-')
+ if exp < 0 {
+ ch = '-'
+ exp = -exp
+ } else {
+ ch = '+'
+ }
+ dst = append(dst, ch)
+ } else {
+ dst = append(dst, '+')
+ }
+
+ // dddd
+ var buf [3]byte
+ i := len(buf)
+ for exp >= 10 {
+ i--
+ buf[i] = byte(exp%10 + '0')
+ exp /= 10
+ }
+ // exp < 10
+ i--
+ buf[i] = byte(exp + '0')
+
+ // leading zeroes
+ if i > len(buf)-2 {
+ i--
+ buf[i] = '0'
+ }
+
+ return append(dst, buf[i:]...)
+}
src/pkg/strconv/ftoa_test.go
のベンチマーク追加
--- a/src/pkg/strconv/ftoa_test.go
+++ b/src/pkg/strconv/ftoa_test.go
@@ -149,26 +149,54 @@ func TestFtoa(t *testing.T) {
}
}
-func BenchmarkFtoa64Decimal(b *testing.B) {
+func BenchmarkFormatFloatDecimal(b *testing.B) {
for i := 0; i < b.N; i++ {
FormatFloat(33909, 'g', -1, 64)
}
}
-func BenchmarkFtoa64Float(b *testing.B) {
+func BenchmarkFormatFloat(b *testing.B) {
for i := 0; i < b.N; i++ {
FormatFloat(339.7784, 'g', -1, 64)
}
}
-func BenchmarkFtoa64FloatExp(b *testing.B) {
+func BenchmarkFormatFloatExp(b *testing.B) {
for i := 0; i < b.N; i++ {
FormatFloat(-5.09e75, 'g', -1, 64)
}
}
-func BenchmarkFtoa64Big(b *testing.B) {
+func BenchmarkFormatFloatBig(b *testing.B) {
for i := 0; i < b.N; i++ {
FormatFloat(123456789123456789123456789, 'g', -1, 64)
}
}
+
+func BenchmarkAppendFloatDecimal(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {
+ AppendFloat(dst, 33909, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloat(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {
+ AppendFloat(dst, 339.7784, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloatExp(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {
+ AppendFloat(dst, -5.09e75, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloatBig(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {
+ AppendFloat(dst, 123456789123456789123456789, 'g', -1, 64)
+ }
+}
コアとなるコードの解説
FormatFloat
と AppendFloat
の変更
以前の FormatFloat
は、内部で genericFtoa
を呼び出し、その結果の string
を直接返していました。AppendFloat
は、その FormatFloat
の結果を append
で既存のバイトスライスに追加していました。
変更後、FormatFloat
は genericFtoa(make([]byte, 0, 16), f, fmt, prec, bitSize)
を呼び出します。ここで重要なのは、make([]byte, 0, 16)
です。これは、長さ0で容量16のバイトスライスを新しく作成します。genericFtoa
はこのスライスに直接結果を書き込み、最終的に string()
で文字列に変換して返します。これにより、genericFtoa
内部での中間的な文字列生成が不要になり、アロケーションが削減されます。
AppendFloat
はさらにシンプルになり、直接 genericFtoa(dst, f, fmt, prec, bitSize)
を呼び出すようになりました。これにより、FormatFloat
を介して一度文字列を生成し、それを再度バイトスライスに変換するという無駄なステップが完全に排除され、直接 dst
バイトスライスに結果が追記されるため、最も効率的なパスが実現されます。
genericFtoa
の変更
genericFtoa
は、浮動小数点数のビット表現 (bits uint64
) を直接受け取る代わりに、val float64
と bitSize
を受け取るようになりました。これにより、呼び出し元は float32
や float64
の値をそのまま渡せるようになり、math.Float32bits
や math.Float64bits
の呼び出しが genericFtoa
内部にカプセル化されました。
最も重要な変更は、戻り値が string
から []byte
になったことです。これにより、genericFtoa
およびその内部で呼び出されるヘルパー関数 (fmtB
, fmtE
, fmtF
) は、結果を直接 dst []byte
に書き込むことができるようになりました。これは、Go言語における文字列操作のパフォーマンス最適化の典型的なパターンであり、新しい文字列の割り当てとコピーを避けることで、ガベージコレクションの負荷を軽減し、実行速度を向上させます。
例えば、Inf
や NaN
の処理では、以前は return "NaN"
のように直接文字列を返していましたが、変更後は return append(dst, s...)
のように、渡された dst
スライスに直接文字列のバイト列を追記する形になっています。
fmtE
の変更
fmtE
は指数表記の浮動小数点数をフォーマットする関数です。以前は buf := make([]byte, ...)
で一時的なバイトスライスを作成し、そこに文字を書き込んでから string(buf[0:w])
で文字列に変換して返していました。
変更後、fmtE
は dst []byte
を引数として受け取り、直接そのスライスに文字を append
するようになりました。これにより、fmtE
内部での一時的なバッファの割り当てと、その後の文字列変換が不要になります。特に、指数部の桁数を計算し、'0'
や '+'
, '-'
などの文字を書き込む部分では、固定サイズの小さな配列 var buf [3]byte
を利用し、最後に append(dst, buf[i:]...)
で dst
に追記する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法です。
ベンチマークの追加
ftoa_test.go
に BenchmarkAppendFloatX
という新しいベンチマーク群が追加されました。これにより、AppendFloat
関数のパフォーマンスが独立して測定できるようになりました。既存の BenchmarkFtoa64X
は BenchmarkFormatFloatX
に名前が変更され、FormatFloat
のパフォーマンスを測定するようになりました。これらのベンチマークは、今回の変更が実際にパフォーマンス向上に寄与したことを数値で裏付ける重要な役割を果たしています。
関連リンク
- Go言語の
strconv
パッケージのドキュメント: https://pkg.go.dev/strconv - Go言語の
math
パッケージのドキュメント: https://pkg.go.dev/math - Go言語における文字列とバイトスライスに関する一般的な情報:
- Go Slices: usage and internals: https://go.dev/blog/slices
- Strings, bytes, runes and characters in Go: https://go.dev/blog/strings
参考にした情報源リンク
- Go言語の公式ドキュメント (上記「関連リンク」に記載)
- Go言語のブログ記事 (上記「関連リンク」に記載)
- IEEE 754 浮動小数点数標準に関する一般的な情報 (例: Wikipedia)
- Go言語のソースコード (コミット内容から直接読み取り)
[インデックス 10644] ファイルの概要
このコミットは、Go言語の標準ライブラリである strconv
パッケージにおける浮動小数点数から文字列への変換処理のパフォーマンス改善を目的としています。具体的には、FormatFloat
および AppendFloat
関数の内部実装が最適化され、中間的な文字列アロケーションを削減することで、処理速度の向上が図られています。
変更されたファイルは以下の通りです。
src/pkg/strconv/ftoa.go
: 浮動小数点数変換の主要ロジックが含まれるファイルです。FormatFloat
およびAppendFloat
関数のシグネチャと実装、ならびにそれらが内部で利用するヘルパー関数(genericFtoa
,fmtB
,fmtE
,fmtF
)のシグネチャと実装が変更されました。主な変更点は、文字列を直接返すのではなく、バイトスライス ([]byte
) に結果を追記する形式に統一されたことです。src/pkg/strconv/ftoa_test.go
:ftoa.go
の変更に伴い、AppendFloat
のパフォーマンスを測定するための新しいベンチマークが追加されました。既存のベンチマーク名も、BenchmarkFtoa64X
からBenchmarkFormatFloatX
へと変更され、FormatFloat
のベンチマークであることが明確化されています。src/pkg/strconv/itoa.go
: 整数変換に関するファイルですが、formatBits
関数の引数名negative
がneg
に変更されるという軽微な修正が含まれています。これは、おそらくコードベース全体での命名規則の統一を目的としたリファクタリングと考えられます。
コミット
commit 127b5a66b1e350ab6a3626a81cd4a7cc7fcaf100
Author: Robert Griesemer <gri@golang.org>
Date: Wed Dec 7 10:30:27 2011 -0800
strconv: faster float conversion
- added AppendFloatX benchmarks
- 2% to 13% better performance
- check for illegal bitSize
benchmark old ns/op new ns/op delta
strconv_test.BenchmarkFormatFloatDecimal 2993 2733 -8.69%
strconv_test.BenchmarkFormatFloat 3384 3141 -7.18%
strconv_test.BenchmarkFormatFloatExp 9192 9010 -1.98%
strconv_test.BenchmarkFormatFloatBig 3279 3207 -2.20%
strconv_test.BenchmarkAppendFloatDecimal 2837 2478 -12.65%
strconv_test.BenchmarkAppendFloat 3196 2928 -8.39%
strconv_test.BenchmarkAppendFloatExp 9028 8773 -2.82%
strconv_test.BenchmarkAppendFloatBig 3151 2782 -11.71%
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5448122
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/127b5a66b1e350ab6a3626a81cd4a7cc7fcaf100
元コミット内容
strconv: faster float conversion
- added AppendFloatX benchmarks
- 2% to 13% better performance
- check for illegal bitSize
benchmark old ns/op new ns/op delta
strconv_test.BenchmarkFormatFloatDecimal 2993 2733 -8.69%
strconv_test.BenchmarkFormatFloat 3384 3141 -7.18%
strconv_test.BenchmarkFormatFloatExp 9192 9010 -1.98%
strconv_test.BenchmarkFormatFloatBig 3279 3207 -2.20%
strconv_test.BenchmarkAppendFloatDecimal 2837 2478 -12.65%
strconv_test.BenchmarkAppendFloat 3196 2928 -8.39%
strconv_test.BenchmarkAppendFloatExp 9028 8773 -2.82%
strconv_test.BenchmarkAppendFloatBig 3151 2782 -11.71%
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5448122
変更の背景
このコミットの主要な動機は、Go言語の strconv
パッケージにおける浮動小数点数から文字列への変換処理のパフォーマンスを向上させることです。特に、FormatFloat
や AppendFloat
といった関数は、数値データを文字列として表現する際に頻繁に利用されるため、これらの関数の効率性はアプリケーション全体のパフォーマンスに大きな影響を与えます。
Go言語では、string
型は不変なバイトシーケンスであり、[]byte
型は可変なバイトシーケンスです。文字列の結合や変更は、多くの場合、新しい文字列のメモリ割り当てとデータのコピーを伴います。これは、特に大量の変換を行う場合に、ガベージコレクションの負荷を増大させ、パフォーマンスのボトルネックとなる可能性があります。
このコミット以前の strconv
パッケージの浮動小数点数変換関数は、おそらく内部で文字列を生成し、それを返すか、既存の文字列に結合していました。このアプローチは、頻繁なメモリ割り当てとコピーを引き起こし、特に高負荷な環境下では性能低下の原因となっていました。
コミットメッセージに示されているベンチマーク結果は、この変更によって FormatFloat
および AppendFloat
の両方で2%から13%の性能向上が見られたことを明確に示しており、この最適化が成功したことを裏付けています。この改善は、Goアプリケーションが数値データを効率的に文字列化する能力を高め、全体的な応答性とスループットの向上に貢献します。
前提知識の解説
Go言語における string
と []byte
Go言語において、string
型と []byte
型はバイトのシーケンスを表しますが、その性質は根本的に異なります。
string
: 不変(immutable)なバイトシーケンスです。一度作成されると、その内容は変更できません。文字列の結合(+
演算子やfmt.Sprintf
など)や部分文字列の抽出などの操作は、新しい文字列のメモリ割り当てとデータのコピーを伴います。この不変性は、文字列がハッシュマップのキーとして安全に使用できる、並行処理において競合状態を心配する必要がない、といった多くの利点をもたらします。しかし、頻繁な変更や結合が必要な場合には、メモリ割り当てとコピーのオーバーヘッドがパフォーマンスに影響を与える可能性があります。[]byte
: 可変(mutable)なバイトスライスです。その内容は変更可能であり、既存のメモリ領域を再利用したり、必要に応じてappend
関数などを用いて拡張したりすることができます。パフォーマンスが重要な場面では、文字列の代わりにバイトスライスを操作し、最終的に必要な場合にのみstring()
変換を行うことが推奨されます。strconv
パッケージのAppend
系関数は、この[]byte
の可変性を活用して、効率的なデータ構築を可能にします。
このコミットの変更は、まさにこの string
と []byte
の特性を深く理解し、[]byte
を直接操作することでメモリ割り当てとコピーのオーバーヘッドを削減するという、Go言語におけるパフォーマンス最適化の典型的なアプローチを適用しています。
浮動小数点数の表現(IEEE 754)
コンピュータにおける浮動小数点数は、通常、IEEE 754標準に従って表現されます。Go言語の float32
は単精度(32ビット)、float64
は倍精度(64ビット)の浮動小数点数に対応します。これらの数値は、以下の3つの要素で構成されるバイナリ形式で格納されます。
- 符号 (Sign Bit): 数値が正(0)か負(1)かを示す1ビット。
- 指数部 (Exponent): 数値のスケール(桁)を示す部分。基数2の何乗かを表します。
- 仮数部 (Mantissa/Fraction): 数値の精度(有効数字)を示す部分。正規化された形式では、常に1.xxxx...という形になり、1の前の部分は暗黙的に表現されます。
math.Float32bits(f float32) uint32
や math.Float64bits(f float64) uint64
といった関数は、これらの浮動小数点数を構成するビット列を uint32
や uint64
として取得するために使用されます。strconv
パッケージは、これらの内部バイナリ表現を、人間が読める十進数表記の文字列に正確かつ効率的に変換する役割を担います。この変換プロセスには、丸め処理や、指数表記、固定小数点表記などのフォーマットに応じた複雑なロジックが含まれます。
strconv
パッケージ
strconv
パッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(数値、真偽値など)と文字列との間の変換機能を提供します。例えば、Atoi
(ASCII to Integer)、Itoa
(Integer to ASCII)、ParseFloat
、FormatFloat
などがあります。これらの関数は、設定ファイルの読み込み、ユーザー入力の解析、データのシリアライズ、ログ出力など、様々な場面で利用されます。strconv
パッケージは、fmt
パッケージと比較して、より高速でメモリ効率の良い変換を提供することを目的としています。これは、fmt
パッケージがより汎用的なフォーマット機能を提供し、リフレクションなどのオーバーヘッドを持つためです。
Go言語のベンチマーク
Go言語のベンチマークは、testing
パッケージを使用して記述され、go test -bench=.
コマンドで実行されます。ベンチマーク関数は BenchmarkXxx(*testing.B)
というシグネチャを持ち、b.N
回のループ内で測定対象の処理を実行します。ベンチマーク結果は通常、以下の形式で表示されます。
benchmark_name old_ns/op new_ns/op delta
benchmark_name
: ベンチマーク関数の名前。old_ns/op
: 変更前の1操作あたりのナノ秒(ns)。new_ns/op
: 変更後の1操作あたりのナノ秒(ns)。delta
: 性能変化の割合。負の値は性能向上を示します。
このコミットのベンチマーク結果は、new_ns/op
が old_ns/op
よりも小さく、delta
が負の値であることから、すべてのテストケースで性能が向上していることを明確に示しています。これは、コード変更が実際に意図したパフォーマンス改善をもたらしたことの強力な証拠となります。
技術的詳細
このコミットの技術的な核心は、浮動小数点数から文字列への変換処理において、中間的な文字列アロケーションを極力排除し、バイトスライス([]byte
)への直接書き込みに切り替えた点にあります。これは、Go言語におけるパフォーマンス最適化の一般的なパターンであり、メモリ割り当ての削減とガベージコレクションの負荷軽減に直結します。
-
FormatFloat
とAppendFloat
のシグネチャ変更と内部ロジックの統合:- 旧シグネチャ:
以前のfunc FormatFloat(f float64, fmt byte, prec int, n int) string func AppendFloat(dst []byte, f float64, fmt byte, prec int, n int) []byte
FormatFloat
は文字列を返し、AppendFloat
はその文字列を既存のバイトスライスにappend
していました。このアプローチでは、FormatFloat
が文字列を生成する際にメモリ割り当てが発生し、さらにAppendFloat
がその文字列をバイトスライスに変換する際に再度コピーが発生する可能性がありました。 - 新シグネチャ:
変更後、func FormatFloat(f float64, fmt byte, prec, bitSize int) string func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte
FormatFloat
は内部的にgenericFtoa
を呼び出す際に、make([]byte, 0, 16)
のように初期容量を持つ空のバイトスライスを生成し、そこに結果を書き込んでからstring()
に変換して返します。これにより、FormatFloat
の内部処理で中間的な文字列生成が不要になります。AppendFloat
は、渡されたdst []byte
に直接結果を追記し、拡張されたバイトスライスを返します。これにより、FormatFloat
が生成した文字列を再度バイトスライスに変換するオーバーヘッドが完全に排除されます。両関数がgenericFtoa
という共通の基盤関数を利用することで、コードの重複が減り、保守性も向上しています。
- 旧シグネチャ:
-
genericFtoa
関数のシグネチャ変更と役割の拡大:- 旧:
func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string
bits
(uint64) は浮動小数点数のビット表現を直接受け取っていました。戻り値はstring
でした。 - 新:
func genericFtoa(dst []byte, val float64, fmt byte, prec, bitSize int) []byte
dst
([]byte) は結果を追記するバイトスライス、val
(float64) は変換対象の浮動小数点数値、bitSize
(int) は変換対象がfloat32
(32) かfloat64
(64) かを示します。戻り値は拡張されたバイトスライスです。genericFtoa
は、val float64
とbitSize
を受け取り、内部でmath.Float32bits
またはmath.Float64bits
を使用してbits uint64
を生成するようになりました。これにより、FormatFloat
やAppendFloat
から直接float64
値を渡せるようになり、呼び出し側のコードが簡素化されました。 また、不正なbitSize
が渡された場合にはpanic
を発生させるようになりました。Inf
(無限大) やNaN
(非数) の表現も、直接dst
に追記する形に変更され、中間的な文字列生成を避けています。
- 旧:
-
内部ヘルパー関数 (
fmtB
,fmtE
,fmtF
) の変更:- これらの関数も、
genericFtoa
と同様に、結果を書き込むdst []byte
を引数として受け取り、拡張された[]byte
を返すようにシグネチャが変更されました。 - これにより、浮動小数点数の各フォーマット(指数表記、固定小数点表記、バイナリ表記)の生成過程で、中間的な文字列生成を避け、直接バイトスライスに文字を書き込むことが可能になりました。
- 特に
fmtE
やfmtF
では、以前はmake([]byte, ...)
で一時的なバッファを作成し、そこに文字を書き込んでから文字列に変換していましたが、変更後は直接dst
にappend
する形に置き換えられています。例えば、fmtE
の指数部の桁数計算と書き込みロジックは、固定サイズの小さなバッファ[3]byte
を利用し、それを最終的にdst
にappend
する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法です。
- これらの関数も、
-
itoa.go
の軽微な変更:formatBits
関数の引数名negative
がneg
に変更されました。これは機能的な変更ではなく、コードの可読性や一貫性を向上させるためのリファクタリングと考えられます。このような細かな変更は、大規模なコードベースにおける品質維持と開発効率向上に寄与します。
これらの変更は、Go言語の strconv
パッケージが、数値と文字列間の変換において、より高速でメモリ効率の良い選択肢となることを確実にするものです。特に、大量の数値データを処理するアプリケーションや、低レイテンシが求められるシステムにおいて、これらの最適化は顕著なパフォーマンス向上をもたらします。
コアとなるコードの変更箇所
src/pkg/strconv/ftoa.go
の FormatFloat
と AppendFloat
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -45,20 +45,30 @@ var float64info = floatInfo{52, 11, -1023}
// Ftoa32(f) is not the same as Ftoa64(float32(f)),
// because correct rounding and the number of digits
// needed to identify f depend on the precision of the representation.
-func FormatFloat(f float64, fmt byte, prec int, n int) string {
- if n == 32 {
- return genericFtoa(uint64(math.Float32bits(float32(f))), fmt, prec, &float32info)
- }
- return genericFtoa(math.Float64bits(f), fmt, prec, &float64info)
+func FormatFloat(f float64, fmt byte, prec, bitSize int) string {
+ return string(genericFtoa(make([]byte, 0, 16), f, fmt, prec, bitSize))
}
// AppendFloat appends the string form of the floating-point number f,
// as generated by FormatFloat, to dst and returns the extended buffer.
-func AppendFloat(dst []byte, f float64, fmt byte, prec int, n int) []byte {
- return append(dst, FormatFloat(f, fmt, prec, n)...)
+func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte {
+ return genericFtoa(dst, f, fmt, prec, bitSize)
}
src/pkg/strconv/ftoa.go
の genericFtoa
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -66,13 +76,16 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
// Pick off easy binary format.
if fmt == 'b' {
- return fmtB(neg, mant, exp, flt)
+ return fmtB(dst, neg, mant, exp, flt)
}
// Create exact decimal representation.
@@ -127,9 +140,9 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
switch fmt {
case 'e', 'E':
- return fmtE(neg, d, prec, fmt)
+ return fmtE(dst, neg, d, prec, fmt)
case 'f':
- return fmtF(neg, d, prec)
+ return fmtF(dst, neg, d, prec)
case 'g', 'G':
// trailing fractional zeros in 'e' form will be trimmed.
eprec := prec
@@ -147,15 +160,16 @@ func genericFtoa(bits uint64, fmt byte, prec int, flt *floatInfo) string {
if prec > d.nd {
prec = d.nd
}
- return fmtE(neg, d, prec-1, fmt+'e'-'g')
+ return fmtE(dst, neg, d, prec-1, fmt+'e'-'g')
}
if prec > d.dp {
prec = d.nd
}
- return fmtF(neg, d, max(prec-d.dp, 0))
+ return fmtF(dst, neg, d, max(prec-d.dp, 0))
}
- return "%" + string(fmt)
+ // unknown format
+ return append(dst, '%', fmt)
}
src/pkg/strconv/ftoa.go
の fmtE
の変更
--- a/src/pkg/strconv/ftoa.go
+++ b/src/pkg/strconv/ftoa.go
@@ -250,121 +264,103 @@ func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) {
}
// %e: -d.ddddde±dd
-func fmtE(neg bool, d *decimal, prec int, fmt byte) string {
- buf := make([]byte, 3+max(prec, 0)+30) // "-0." + prec digits + exp
- w := 0 // write index
-
+func fmtE(dst []byte, neg bool, d *decimal, prec int, fmt byte) []byte {
// sign
if neg {
- buf[w] = '-'
- w++
+ dst = append(dst, '-')
}
// first digit
if d.nd == 0 {
- buf[w] = '0'
+ ch := byte('0')
+ if d.nd != 0 {
+ ch = d.d[0]
+ }
+ dst = append(dst, ch)
} else {
- buf[w] = d.d[0]
+ dst = append(dst, d.d[0])
}
- w++
// .moredigits
if prec > 0 {
- buf[w] = '.'
- w++
- for i := 0; i < prec; i++ {
- if 1+i < d.nd {
- buf[w] = d.d[1+i]
- } else {
- buf[w] = '0'
+ dst = append(dst, '.')
+ for i := 1; i <= prec; i++ {
+ ch := byte('0')
+ if i < d.nd {
+ ch = d.d[i]
}
- w++
+ dst = append(dst, ch)
}
}
// e±
- buf[w] = fmt
- w++
+ dst = append(dst, fmt)
exp := d.dp - 1
if d.nd == 0 { // special case: 0 has exponent 0
exp = 0
}
if exp < 0 {
- buf[w] = '-'
+ ch := byte('-')
+ if exp < 0 {
+ ch = '-'
+ exp = -exp
+ } else {
+ ch = '+'
+ }
+ dst = append(dst, ch)
+ } else {
+ dst = append(dst, '+')
+ }
+
+ // dddd
+ var buf [3]byte
+ i := len(buf)
+ for exp >= 10 {
+ i--
+ buf[i] = byte(exp%10 + '0')
+ exp /= 10
+ }
+ // exp < 10
+ i--
+ buf[i] = byte(exp + '0')
+
+ // leading zeroes
+ if i > len(buf)-2 {
+ i--
+ buf[i] = '0'
+ }
+
+ return append(dst, buf[i:]...)
+}
src/pkg/strconv/ftoa_test.go
のベンチマーク追加
--- a/src/pkg/strconv/ftoa_test.go
+++ b/src/pkg/strconv/ftoa_test.go
@@ -149,26 +149,54 @@ func TestFtoa(t *testing.T) {
}
}
-func BenchmarkFtoa64Decimal(b *testing.B) {
+func BenchmarkFormatFloatDecimal(b *testing.B) {
for i := 0; i < b.N; i++ {\n FormatFloat(33909, 'g', -1, 64)
}
}
-func BenchmarkFtoa64Float(b *testing.B) {
+func BenchmarkFormatFloat(b *testing.B) {
for i := 0; i < b.N; i++ {\n FormatFloat(339.7784, 'g', -1, 64)
}
}
-func BenchmarkFtoa64FloatExp(b *testing.B) {
+func BenchmarkFormatFloatExp(b *testing.B) {
for i := 0; i < b.N; i++ {\n FormatFloat(-5.09e75, 'g', -1, 64)
}
}
-func BenchmarkFtoa64Big(b *testing.B) {
+func BenchmarkFormatFloatBig(b *testing.B) {
for i := 0; i < b.N; i++ {\n FormatFloat(123456789123456789123456789, 'g', -1, 64)
}
}
+
+func BenchmarkAppendFloatDecimal(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {\n AppendFloat(dst, 33909, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloat(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {\n AppendFloat(dst, 339.7784, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloatExp(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {\n AppendFloat(dst, -5.09e75, 'g', -1, 64)
+ }
+}
+
+func BenchmarkAppendFloatBig(b *testing.B) {
+ dst := make([]byte, 0, 30)
+ for i := 0; i < b.N; i++ {\n AppendFloat(dst, 123456789123456789123456789, 'g', -1, 64)
+ }
+}
コアとなるコードの解説
FormatFloat
と AppendFloat
の変更
この変更の最も重要な点は、FormatFloat
と AppendFloat
の両方が、結果を直接バイトスライスに書き込む genericFtoa
関数を内部で利用するように統一されたことです。
FormatFloat
: 以前はgenericFtoa
が返したstring
をそのまま返していました。変更後は、make([]byte, 0, 16)
で初期容量16の新しいバイトスライスを作成し、これをgenericFtoa
に渡します。genericFtoa
はこのスライスに結果を書き込み、最終的にstring()
で文字列に変換して返します。これにより、genericFtoa
内部での中間的な文字列生成が不要になり、アロケーションが削減されます。初期容量を適切に設定することで、スライスの再割り当て回数を減らし、効率を高めることができます。AppendFloat
: 以前はFormatFloat
を呼び出して文字列を取得し、それをappend
で既存のバイトスライスに追加していました。この方法は、FormatFloat
が文字列を生成する際のオーバーヘッドと、その文字列をバイトスライスに変換する際の追加のコピーオーバーヘッドが発生していました。変更後は、直接genericFtoa(dst, f, fmt, prec, bitSize)
を呼び出すようになりました。これにより、FormatFloat
を介した無駄なステップが完全に排除され、渡されたdst
バイトスライスに直接結果が追記されるため、最も効率的なパスが実現されます。
genericFtoa
の変更
genericFtoa
は、浮動小数点数から文字列への変換ロジックの中核を担う関数です。
- 引数の変更: 以前は浮動小数点数のビット表現 (
bits uint64
) を直接受け取っていましたが、変更後はval float64
とbitSize
を受け取るようになりました。これにより、呼び出し元はfloat32
やfloat64
の値をそのまま渡せるようになり、math.Float32bits
やmath.Float64bits
の呼び出しがgenericFtoa
内部にカプセル化されました。これにより、APIの使いやすさが向上し、呼び出し側のコードが簡素化されます。また、不正なbitSize
が渡された場合にはpanic
を発生させることで、早期にエラーを検出できるようになりました。 - 戻り値の変更: 最も重要な変更は、戻り値が
string
から[]byte
になったことです。これにより、genericFtoa
およびその内部で呼び出されるヘルパー関数 (fmtB
,fmtE
,fmtF
) は、結果を直接dst []byte
に書き込むことができるようになりました。これは、Go言語における文字列操作のパフォーマンス最適化の典型的なパターンであり、新しい文字列の割り当てとコピーを避けることで、ガベージコレクションの負荷を軽減し、実行速度を向上させます。例えば、Inf
やNaN
の処理では、以前はreturn "NaN"
のように直接文字列を返していましたが、変更後はreturn append(dst, s...)
のように、渡されたdst
スライスに直接文字列のバイト列を追記する形になっています。
fmtE
の変更
fmtE
は指数表記(例: 1.23e+05
)の浮動小数点数をフォーマットする関数です。
- 以前は
buf := make([]byte, ...)
で一時的なバイトスライスを作成し、そこに文字を書き込んでからstring(buf[0:w])
で文字列に変換して返していました。この一時的なバッファの作成と、その後の文字列変換は、メモリ割り当てとコピーのオーバーヘッドを伴いました。 - 変更後、
fmtE
はdst []byte
を引数として受け取り、直接そのスライスに文字をappend
するようになりました。これにより、fmtE
内部での一時的なバッファの割り当てと、その後の文字列変換が不要になります。特に、指数部の桁数を計算し、'0'
や'+'
,'-'
などの文字を書き込む部分では、固定サイズの小さな配列var buf [3]byte
を利用し、最後にappend(dst, buf[i:]...)
でdst
に追記する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法であり、スタック上に確保されるためヒープ割り当てが発生しません。
ベンチマークの追加
ftoa_test.go
に BenchmarkAppendFloatX
という新しいベンチマーク群が追加されました。これにより、AppendFloat
関数のパフォーマンスが独立して測定できるようになりました。既存の BenchmarkFtoa64X
は BenchmarkFormatFloatX
に名前が変更され、FormatFloat
のパフォーマンスを測定するようになりました。これらのベンチマークは、今回の変更が実際にパフォーマンス向上に寄与したことを数値で裏付ける重要な役割を果たします。ベンチマーク結果は、FormatFloat
と AppendFloat
の両方で、特に Decimal
と Big
のケースで顕著な性能向上が見られることを示しており、これはメモリ割り当ての削減が効果的であったことを示唆しています。
これらの変更は、Go言語の strconv
パッケージが、数値と文字列間の変換において、より高速でメモリ効率の良い選択肢となることを確実にするものです。特に、大量の数値データを処理するアプリケーションや、低レイテンシが求められるシステムにおいて、これらの最適化は顕著なパフォーマンス向上をもたらします。
関連リンク
- Go言語の
strconv
パッケージのドキュメント: https://pkg.go.dev/strconv - Go言語の
math
パッケージのドキュメント: https://pkg.go.dev/math - Go言語における文字列とバイトスライスに関する一般的な情報:
- Go Slices: usage and internals: https://go.dev/blog/slices
- Strings, bytes, runes and characters in Go: https://go.dev/blog/strings
- Go言語の
strings.Builder
について (文字列結合の効率化): https://pkg.go.dev/strings#Builder
参考にした情報源リンク
- Go言語の公式ドキュメント (上記「関連リンク」に記載)
- Go言語のブログ記事 (上記「関連リンク」に記載)
- IEEE 754 浮動小数点数標準に関する一般的な情報 (例: Wikipedia)
- Go言語のソースコード (コミット内容から直接読み取り)
- Web検索結果: "Go strconv package performance optimization string byte slice"