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

[インデックス 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 パッケージにおける浮動小数点数から文字列への変換処理のパフォーマンスを向上させることです。特に、FormatFloatAppendFloat といった関数が対象となっています。

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.Float32bitsmath.Float64bits といった関数です。strconv パッケージは、これらの内部表現を人間が読める十進数表記の文字列に変換する役割を担います。

strconv パッケージ

strconv パッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(数値、真偽値など)と文字列との間の変換機能を提供します。例えば、Atoi (ASCII to Integer)、Itoa (Integer to ASCII)、ParseFloatFormatFloat などがあります。これらの関数は、設定ファイルの読み込み、ユーザー入力の解析、データのシリアライズなど、様々な場面で利用されます。

ベンチマークの読み方

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/opold_ns/op よりも小さく、delta が負の値であることから、すべてのテストケースで性能が向上していることを示しています。

技術的詳細

このコミットの技術的な核心は、浮動小数点数から文字列への変換処理において、中間的な文字列アロケーションを極力排除し、バイトスライス([]byte)への直接書き込みに切り替えた点にあります。

  1. FormatFloatAppendFloat の統合と最適化:

    • 以前は FormatFloat が文字列を返し、AppendFloat がその文字列を既存のバイトスライスに append していました。
    • 変更後、FormatFloat は内部的に AppendFloat と同様のロジックを使用し、make([]byte, 0, 16) のように初期容量を持つ空のバイトスライスを生成し、そこに結果を書き込んでから string() に変換して返します。
    • AppendFloat は、渡された dst []byte に直接結果を追記し、拡張されたバイトスライスを返します。これにより、FormatFloat が生成した文字列を再度バイトスライスに変換するオーバーヘッドがなくなります。
  2. 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 float64bitSize を受け取り、内部で math.Float32bits または math.Float64bits を使用して bits uint64 を生成するようになりました。これにより、FormatFloatAppendFloat から直接 float64 値を渡せるようになり、呼び出し側のコードが簡素化されました。
    • 不正な bitSize が渡された場合には panic を発生させるようになりました。
    • Inf (無限大) や NaN (非数) の表現も、直接 dst に追記する形に変更されました。
  3. 内部ヘルパー関数 (fmtB, fmtE, fmtF) の変更:

    • これらの関数も、genericFtoa と同様に、結果を書き込む dst []byte を引数として受け取り、拡張された []byte を返すようにシグネチャが変更されました。
    • これにより、浮動小数点数の各フォーマット(指数表記、固定小数点表記、バイナリ表記)の生成過程で、中間的な文字列生成を避け、直接バイトスライスに文字を書き込むことが可能になりました。特に fmtEfmtF では、make([]byte, ...) で一時的なバッファを作成していた箇所が、直接 dstappend する形に置き換えられています。例えば、fmtE の指数部の桁数計算と書き込みロジックは、固定サイズの小さなバッファ [3]byte を利用し、それを最終的に dstappend する形に最適化されています。
  4. itoa.go の軽微な変更:

    • formatBits 関数の引数名 negativeneg に変更されました。これは機能的な変更ではなく、コードの可読性や一貫性を向上させるためのリファクタリングと考えられます。

これらの変更により、浮動小数点数変換のパイプライン全体でメモリ割り当てが削減され、ガベージコレクションの頻度が低下し、結果としてパフォーマンスが向上しました。

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

src/pkg/strconv/ftoa.goFormatFloatAppendFloat の変更

--- 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.gogenericFtoa の変更

--- 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.gofmtE の変更

--- 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)
+	}
+}

コアとなるコードの解説

FormatFloatAppendFloat の変更

以前の FormatFloat は、内部で genericFtoa を呼び出し、その結果の string を直接返していました。AppendFloat は、その FormatFloat の結果を append で既存のバイトスライスに追加していました。

変更後、FormatFloatgenericFtoa(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 float64bitSize を受け取るようになりました。これにより、呼び出し元は float32float64 の値をそのまま渡せるようになり、math.Float32bitsmath.Float64bits の呼び出しが genericFtoa 内部にカプセル化されました。

最も重要な変更は、戻り値が string から []byte になったことです。これにより、genericFtoa およびその内部で呼び出されるヘルパー関数 (fmtB, fmtE, fmtF) は、結果を直接 dst []byte に書き込むことができるようになりました。これは、Go言語における文字列操作のパフォーマンス最適化の典型的なパターンであり、新しい文字列の割り当てとコピーを避けることで、ガベージコレクションの負荷を軽減し、実行速度を向上させます。

例えば、InfNaN の処理では、以前は return "NaN" のように直接文字列を返していましたが、変更後は return append(dst, s...) のように、渡された dst スライスに直接文字列のバイト列を追記する形になっています。

fmtE の変更

fmtE は指数表記の浮動小数点数をフォーマットする関数です。以前は buf := make([]byte, ...) で一時的なバイトスライスを作成し、そこに文字を書き込んでから string(buf[0:w]) で文字列に変換して返していました。

変更後、fmtEdst []byte を引数として受け取り、直接そのスライスに文字を append するようになりました。これにより、fmtE 内部での一時的なバッファの割り当てと、その後の文字列変換が不要になります。特に、指数部の桁数を計算し、'0''+', '-' などの文字を書き込む部分では、固定サイズの小さな配列 var buf [3]byte を利用し、最後に append(dst, buf[i:]...)dst に追記する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法です。

ベンチマークの追加

ftoa_test.goBenchmarkAppendFloatX という新しいベンチマーク群が追加されました。これにより、AppendFloat 関数のパフォーマンスが独立して測定できるようになりました。既存の BenchmarkFtoa64XBenchmarkFormatFloatX に名前が変更され、FormatFloat のパフォーマンスを測定するようになりました。これらのベンチマークは、今回の変更が実際にパフォーマンス向上に寄与したことを数値で裏付ける重要な役割を果たしています。

関連リンク

参考にした情報源リンク

  • 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 関数の引数名 negativeneg に変更されるという軽微な修正が含まれています。これは、おそらくコードベース全体での命名規則の統一を目的としたリファクタリングと考えられます。

コミット

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 パッケージにおける浮動小数点数から文字列への変換処理のパフォーマンスを向上させることです。特に、FormatFloatAppendFloat といった関数は、数値データを文字列として表現する際に頻繁に利用されるため、これらの関数の効率性はアプリケーション全体のパフォーマンスに大きな影響を与えます。

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) uint32math.Float64bits(f float64) uint64 といった関数は、これらの浮動小数点数を構成するビット列を uint32uint64 として取得するために使用されます。strconv パッケージは、これらの内部バイナリ表現を、人間が読める十進数表記の文字列に正確かつ効率的に変換する役割を担います。この変換プロセスには、丸め処理や、指数表記、固定小数点表記などのフォーマットに応じた複雑なロジックが含まれます。

strconv パッケージ

strconv パッケージは、Go言語の標準ライブラリの一部であり、基本的なデータ型(数値、真偽値など)と文字列との間の変換機能を提供します。例えば、Atoi (ASCII to Integer)、Itoa (Integer to ASCII)、ParseFloatFormatFloat などがあります。これらの関数は、設定ファイルの読み込み、ユーザー入力の解析、データのシリアライズ、ログ出力など、様々な場面で利用されます。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/opold_ns/op よりも小さく、delta が負の値であることから、すべてのテストケースで性能が向上していることを明確に示しています。これは、コード変更が実際に意図したパフォーマンス改善をもたらしたことの強力な証拠となります。

技術的詳細

このコミットの技術的な核心は、浮動小数点数から文字列への変換処理において、中間的な文字列アロケーションを極力排除し、バイトスライス([]byte)への直接書き込みに切り替えた点にあります。これは、Go言語におけるパフォーマンス最適化の一般的なパターンであり、メモリ割り当ての削減とガベージコレクションの負荷軽減に直結します。

  1. FormatFloatAppendFloat のシグネチャ変更と内部ロジックの統合:

    • 旧シグネチャ:
      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 という共通の基盤関数を利用することで、コードの重複が減り、保守性も向上しています。
  2. 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 float64bitSize を受け取り、内部で math.Float32bits または math.Float64bits を使用して bits uint64 を生成するようになりました。これにより、FormatFloatAppendFloat から直接 float64 値を渡せるようになり、呼び出し側のコードが簡素化されました。 また、不正な bitSize が渡された場合には panic を発生させるようになりました。 Inf (無限大) や NaN (非数) の表現も、直接 dst に追記する形に変更され、中間的な文字列生成を避けています。
  3. 内部ヘルパー関数 (fmtB, fmtE, fmtF) の変更:

    • これらの関数も、genericFtoa と同様に、結果を書き込む dst []byte を引数として受け取り、拡張された []byte を返すようにシグネチャが変更されました。
    • これにより、浮動小数点数の各フォーマット(指数表記、固定小数点表記、バイナリ表記)の生成過程で、中間的な文字列生成を避け、直接バイトスライスに文字を書き込むことが可能になりました。
    • 特に fmtEfmtF では、以前は make([]byte, ...) で一時的なバッファを作成し、そこに文字を書き込んでから文字列に変換していましたが、変更後は直接 dstappend する形に置き換えられています。例えば、fmtE の指数部の桁数計算と書き込みロジックは、固定サイズの小さなバッファ [3]byte を利用し、それを最終的に dstappend する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法です。
  4. itoa.go の軽微な変更:

    • formatBits 関数の引数名 negativeneg に変更されました。これは機能的な変更ではなく、コードの可読性や一貫性を向上させるためのリファクタリングと考えられます。このような細かな変更は、大規模なコードベースにおける品質維持と開発効率向上に寄与します。

これらの変更は、Go言語の strconv パッケージが、数値と文字列間の変換において、より高速でメモリ効率の良い選択肢となることを確実にするものです。特に、大量の数値データを処理するアプリケーションや、低レイテンシが求められるシステムにおいて、これらの最適化は顕著なパフォーマンス向上をもたらします。

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

src/pkg/strconv/ftoa.goFormatFloatAppendFloat の変更

--- 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.gogenericFtoa の変更

--- 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.gofmtE の変更

--- 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)
+	}
+}

コアとなるコードの解説

FormatFloatAppendFloat の変更

この変更の最も重要な点は、FormatFloatAppendFloat の両方が、結果を直接バイトスライスに書き込む 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 float64bitSize を受け取るようになりました。これにより、呼び出し元は float32float64 の値をそのまま渡せるようになり、math.Float32bitsmath.Float64bits の呼び出しが genericFtoa 内部にカプセル化されました。これにより、APIの使いやすさが向上し、呼び出し側のコードが簡素化されます。また、不正な bitSize が渡された場合には panic を発生させることで、早期にエラーを検出できるようになりました。
  • 戻り値の変更: 最も重要な変更は、戻り値が string から []byte になったことです。これにより、genericFtoa およびその内部で呼び出されるヘルパー関数 (fmtB, fmtE, fmtF) は、結果を直接 dst []byte に書き込むことができるようになりました。これは、Go言語における文字列操作のパフォーマンス最適化の典型的なパターンであり、新しい文字列の割り当てとコピーを避けることで、ガベージコレクションの負荷を軽減し、実行速度を向上させます。例えば、InfNaN の処理では、以前は return "NaN" のように直接文字列を返していましたが、変更後は return append(dst, s...) のように、渡された dst スライスに直接文字列のバイト列を追記する形になっています。

fmtE の変更

fmtE は指数表記(例: 1.23e+05)の浮動小数点数をフォーマットする関数です。

  • 以前は buf := make([]byte, ...) で一時的なバイトスライスを作成し、そこに文字を書き込んでから string(buf[0:w]) で文字列に変換して返していました。この一時的なバッファの作成と、その後の文字列変換は、メモリ割り当てとコピーのオーバーヘッドを伴いました。
  • 変更後、fmtEdst []byte を引数として受け取り、直接そのスライスに文字を append するようになりました。これにより、fmtE 内部での一時的なバッファの割り当てと、その後の文字列変換が不要になります。特に、指数部の桁数を計算し、'0''+', '-' などの文字を書き込む部分では、固定サイズの小さな配列 var buf [3]byte を利用し、最後に append(dst, buf[i:]...)dst に追記する形に最適化されています。これは、小さな固定長のデータを効率的に処理するための一般的な手法であり、スタック上に確保されるためヒープ割り当てが発生しません。

ベンチマークの追加

ftoa_test.goBenchmarkAppendFloatX という新しいベンチマーク群が追加されました。これにより、AppendFloat 関数のパフォーマンスが独立して測定できるようになりました。既存の BenchmarkFtoa64XBenchmarkFormatFloatX に名前が変更され、FormatFloat のパフォーマンスを測定するようになりました。これらのベンチマークは、今回の変更が実際にパフォーマンス向上に寄与したことを数値で裏付ける重要な役割を果たします。ベンチマーク結果は、FormatFloatAppendFloat の両方で、特に DecimalBig のケースで顕著な性能向上が見られることを示しており、これはメモリ割り当ての削減が効果的であったことを示唆しています。

これらの変更は、Go言語の strconv パッケージが、数値と文字列間の変換において、より高速でメモリ効率の良い選択肢となることを確実にするものです。特に、大量の数値データを処理するアプリケーションや、低レイテンシが求められるシステムにおいて、これらの最適化は顕著なパフォーマンス向上をもたらします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (上記「関連リンク」に記載)
  • Go言語のブログ記事 (上記「関連リンク」に記載)
  • IEEE 754 浮動小数点数標準に関する一般的な情報 (例: Wikipedia)
  • Go言語のソースコード (コミット内容から直接読み取り)
  • Web検索結果: "Go strconv package performance optimization string byte slice"