[インデックス 13202] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージのパフォーマンス改善を目的としています。具体的には、以下の3つのファイルが変更されています。
src/pkg/fmt/fmt_test.go
: ベンチマークテストが追加されています。src/pkg/fmt/format.go
: フォーマット処理におけるパディング(余白埋め)のロジックが変更されています。src/pkg/fmt/print.go
: 値の出力処理において、Stringer
インターフェースなどのメソッドハンドリングの順序が変更されています。
コミット
- コミットハッシュ:
53bc19442d570802c0966d9b0c623151e78e5875
- 作者: Rob Pike r@golang.org
- コミット日時: 2012年5月29日 火曜日 15:08:08 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53bc19442d570802c0966d9b0c623151e78e5875
元コミット内容
fmt: speed up 10-20%
The check for Stringer etc. can only fire if the test is not a builtin, so avoid
the expensive check if we know there's no chance.
Also put in a fast path for pad, which saves a more modest amount.
benchmark old ns/op new ns/op delta
BenchmarkSprintfEmpty 148 152 +2.70%
BenchmarkSprintfString 585 497 -15.04%
BenchmarkSprintfInt 441 396 -10.20%
BenchmarkSprintfIntInt 718 603 -16.02%
BenchmarkSprintfPrefixedInt 676 621 -8.14%
BenchmarkSprintfFloat 1003 953 -4.99%
BenchmarkManyArgs 2945 2312 -21.49%
BenchmarkScanInts 1704152 1734441 +1.78%
BenchmarkScanRecursiveInt 1837397 1828920 -0.46%
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6245068
変更の背景
このコミットの主な目的は、Go言語のfmt
パッケージにおけるフォーマット処理のパフォーマンスを向上させることです。コミットメッセージに記載されているベンチマーク結果からわかるように、特に文字列や整数、複数の引数を扱うSprintf
系の処理において、10%から20%程度の速度向上が見込まれています。
パフォーマンス改善の具体的な背景としては、以下の2点が挙げられています。
Stringer
インターフェースなどのチェックの最適化:fmt
パッケージは、値を文字列に変換する際に、その値がStringer
やerror
などの特定のインターフェースを実装しているかどうかをチェックします。これらのチェックは、リフレクションを伴う場合があり、コストが高い処理です。コミットメッセージでは、「組み込み型でない場合にのみStringer
などのチェックが発火する」という特性を利用し、組み込み型であることが分かっている場合にはこの高コストなチェックをスキップすることで、無駄な処理を削減しようとしています。- パディング処理の高速化: フォーマット指定子(例:
%5s
)によって文字列や数値にパディング(余白)を追加する処理も、頻繁に実行されるため、その効率が全体のパフォーマンスに影響します。このコミットでは、パディングが不要なケース(幅指定がない、または幅が0の場合)に高速パスを導入することで、処理のオーバーヘッドを削減しています。
これらの最適化により、fmt
パッケージを利用するアプリケーション全体のパフォーマンス向上が期待されます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とfmt
パッケージの内部動作に関する知識が必要です。
fmt
パッケージ: Go言語の標準ライブラリの一つで、C言語のprintf
/scanf
に似た書式付きI/O機能を提供します。fmt.Sprintf
、fmt.Printf
、fmt.Fprint
などの関数が含まれ、様々な型の値を整形して文字列に出力したり、文字列から値を読み取ったりするために使用されます。Stringer
インターフェース:fmt
パッケージが値を文字列に変換する際に利用する重要なインターフェースです。
任意の型がtype Stringer interface { String() string }
String() string
メソッドを実装している場合、fmt
パッケージはその型の値をフォーマットする際に、自動的にこのString()
メソッドを呼び出して文字列表現を取得します。これにより、カスタム型を人間が読める形式で出力できるようになります。- リフレクション (Reflection): Go言語のリフレクションは、プログラムの実行時に型情報や値の情報を検査・操作する機能です。
reflect
パッケージを通じて提供されます。fmt
パッケージは、Stringer
インターフェースの実装チェックや、カスタム型のフィールドへのアクセスなど、コンパイル時には型が不明な値を扱う際にリフレクションを内部的に使用します。リフレクションは非常に強力ですが、通常の直接的なメソッド呼び出しやフィールドアクセスに比べて実行時コストが高いという特性があります。 - パディング (Padding):
fmt
パッケージのフォーマット指定子(例:%5d
,%10s
)には、出力される値の最小幅を指定する機能があります。指定された幅よりも値の文字列表現が短い場合、残りのスペースは空白などの文字で埋められます。これをパディングと呼びます。左寄せ(デフォルト)や右寄せ(-
フラグ)などのオプションもあります。 pp
構造体とfmt
構造体:fmt
パッケージの内部では、フォーマット処理の状態を管理するためにpp
(printer)やfmt
(formatter)といった内部構造体が使われています。これらの構造体は、出力バッファ、フォーマットフラグ、幅、精度などの情報を保持し、実際のフォーマットロジックを実行します。
技術的詳細
このコミットによるパフォーマンス改善は、主に以下の2つの技術的変更によって実現されています。
-
print.go
におけるhandleMethods
呼び出しの最適化:src/pkg/fmt/print.go
のpp.printField
関数は、与えられたfield
(値)をフォーマットして出力する中心的なロジックを担っています。変更前は、まずp.handleMethods
を呼び出してStringer
などのインターフェース実装をチェックしていました。しかし、このチェックはリフレクションを伴うため、コストが高いです。変更後は、まず
field
の型がbool
,int
,string
,[]byte
などの組み込み型(simple type)であるかどうかをswitch
文で直接チェックし、これらの型であればリフレクションを伴うhandleMethods
を呼び出すことなく、直接フォーマット処理を行います。// 変更前 // if wasString, handled := p.handleMethods(verb, plus, goSyntax, depth); handled { // return wasString // } // ... // default: // simple typeではない場合 // // Need to use reflection // return p.printReflectValue(reflect.ValueOf(field), verb, plus, goSyntax, depth) // 変更後 // default: // simple typeではない場合 // // If the type is not simple, it might have methods. // if wasString, handled := p.handleMethods(verb, plus, goSyntax, depth); handled { // return wasString // } // // Need to use reflection // return p.printReflectValue(reflect.ValueOf(field), verb, plus, goSyntax, depth)
この変更により、
bool
やint
、string
といった頻繁にフォーマットされる組み込み型に対しては、不要なhandleMethods
の呼び出し(およびそれに伴うリフレクション)がスキップされるようになり、パフォーマンスが向上します。handleMethods
は、型が組み込み型ではない場合にのみ呼び出されるようになりました。 -
format.go
におけるpad
およびpadString
関数の高速パス導入:src/pkg/fmt/format.go
のfmt.pad
とfmt.padString
関数は、それぞれバイトスライスと文字列のパディング処理を担当しています。変更前は、f.widPresent
(幅が指定されているか)とf.wid != 0
(幅が0でないか)のチェックをif
文で行い、その内部でf.computePadding
を呼び出してパディング情報を計算していました。変更後は、
if !f.widPresent || f.wid == 0
という条件を最初にチェックし、パディングが不要なケース(幅指定がない、または幅が0の場合)には、直接バッファに書き込みを行い、関数を即座に終了するように変更されました。// 変更前 (pad関数の例) // func (f *fmt) pad(b []byte) { // var padding []byte // var left, right int // if f.widPresent && f.wid != 0 { // padding, left, right = f.computePadding(len(b)) // } // // ... パディング処理 ... // 変更後 (pad関数の例) func (f *fmt) pad(b []byte) { if !f.widPresent || f.wid == 0 { f.buf.Write(b) return } padding, left, right := f.computePadding(len(b)) // ... パディング処理 ...
この「高速パス」の導入により、パディングが不要な場合に
computePadding
の呼び出しや、その後のパディングロジックの実行を避けることができ、わずかながらもパフォーマンスが向上します。特に、幅指定をしないfmt.Print
やfmt.Println
のような関数が頻繁に呼び出される場合に効果を発揮します。
これらの変更は、Go言語の標準ライブラリが、頻繁に実行されるコードパスにおいて、いかに小さな最適化を積み重ねて全体的なパフォーマンスを向上させているかを示す良い例です。
コアとなるコードの変更箇所
diff --git a/src/pkg/fmt/fmt_test.go b/src/pkg/fmt/fmt_test.go
index de0342967c..a7632de8ee 100644
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -527,6 +527,14 @@ func BenchmarkSprintfFloat(b *testing.B) {
}\n}\n\n+func BenchmarkManyArgs(b *testing.B) {\n+\tvar buf bytes.Buffer\n+\tfor i := 0; i < b.N; i++ {\n+\t\tbuf.Reset()\n+\t\tFprintf(&buf, "%2d/%2d/%2d %d:%d:%d %s %s\\n", 3, 4, 5, 11, 12, 13, "hello", "world")\n+\t}\n+}\n+\n var mallocBuf bytes.Buffer\n \n var mallocTest = []struct {
diff --git a/src/pkg/fmt/format.go b/src/pkg/fmt/format.go
index caf900d5c3..3c9cd0de69 100644
--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -110,11 +110,11 @@ func (f *fmt) writePadding(n int, padding []byte) {
// Append b to f.buf, padded on left (w > 0) or right (w < 0 or f.minus)
// clear flags afterwards.
func (f *fmt) pad(b []byte) {
- var padding []byte
- var left, right int
- if f.widPresent && f.wid != 0 {
- padding, left, right = f.computePadding(len(b))
- }
+ if !f.widPresent || f.wid == 0 {
+ f.buf.Write(b)
+ return
+ }
+ padding, left, right := f.computePadding(len(b))
if left > 0 {
f.writePadding(left, padding)
}
@@ -127,11 +127,11 @@ func (f *fmt) pad(s string) {
// append s to buf, padded on left (w > 0) or right (w < 0 or f.minus).
// clear flags afterwards.
func (f *fmt) padString(s string) {
- var padding []byte
- var left, right int
- if f.widPresent && f.wid != 0 {
- padding, left, right = f.computePadding(utf8.RuneCountInString(s))
- }
+ if !f.widPresent || f.wid == 0 {
+ f.buf.WriteString(s)
+ return
+ }
+ padding, left, right := f.computePadding(utf8.RuneCountInString(s))
if left > 0 {
f.writePadding(left, padding)
}
diff --git a/src/pkg/fmt/print.go b/src/pkg/fmt/print.go
index 13438243cd..c730b18e9f 100644
--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -734,10 +734,6 @@ func (p *pp) printField(field interface{}, verb rune, plus, goSyntax bool, depth
return false
}
- if wasString, handled := p.handleMethods(verb, plus, goSyntax, depth); handled {
- return wasString
- }
-
// Some types can be done without reflection.
switch f := field.(type) {
case bool:
@@ -779,6 +775,10 @@ func (p *pp) printField(field interface{}, verb rune, plus, goSyntax bool, depth
p.fmtBytes(f, verb, goSyntax, depth)
wasString = verb == 's'
default:
+ // If the type is not simple, it might have methods.
+ if wasString, handled := p.handleMethods(verb, plus, goSyntax, depth); handled {
+ return wasString
+ }
// Need to use reflection
return p.printReflectValue(reflect.ValueOf(field), verb, plus, goSyntax, depth)
}
コアとなるコードの解説
src/pkg/fmt/fmt_test.go
BenchmarkManyArgs
という新しいベンチマーク関数が追加されています。- このベンチマークは、複数の異なる型の引数(整数、文字列)を
Fprintf
関数に渡してフォーマットする際のパフォーマンスを測定します。 - これは、
fmt
パッケージが様々な引数を処理する際のオーバーヘッドを評価し、今回の最適化が多引数ケースにどれだけ効果があるかを確認するために導入されました。コミットメッセージのベンチマーク結果で-21.49%
と最も大きな改善が見られたのがこのケースです。
src/pkg/fmt/format.go
func (f *fmt) pad(b []byte)
およびfunc (f *fmt) padString(s string)
関数が変更されています。- 変更前は、
f.widPresent && f.wid != 0
という条件でパディングが必要かどうかをチェックし、その内部でf.computePadding
を呼び出していました。 - 変更後は、
if !f.widPresent || f.wid == 0
という条件が追加され、パディングが不要な場合(幅指定がないか、幅が0の場合)には、すぐにf.buf.Write(b)
またはf.buf.WriteString(s)
でバッファに書き込み、return
で関数を終了する「高速パス」が導入されました。 - これにより、パディングが不要なケースで
f.computePadding
の呼び出しや、その後のパディングロジックの実行をスキップできるようになり、処理のオーバーヘッドが削減されます。
src/pkg/fmt/print.go
func (p *pp) printField(field interface{}, verb rune, plus, goSyntax bool, depth int)
関数が変更されています。- 変更前は、関数の冒頭で
p.handleMethods
を呼び出し、Stringer
などのインターフェース実装をチェックしていました。このチェックはリフレクションを伴うため、コストが高いです。 - 変更後は、
p.handleMethods
の呼び出しが、switch f := field.(type)
ブロックのdefault
ケース(つまり、field
がbool
,int
,string
,[]byte
などの組み込み型ではない場合)に移動されました。 - これにより、
field
が組み込み型である場合には、高コストなp.handleMethods
の呼び出しが完全にスキップされるようになり、パフォーマンスが向上します。組み込み型は頻繁にフォーマットされるため、この変更は全体的な速度向上に大きく貢献します。
関連リンク
- Go CL (Code Review) へのリンク: https://golang.org/cl/6245068
参考にした情報源リンク
- Go言語の公式ドキュメント:
fmt
パッケージ (https://pkg.go.dev/fmt) - Go言語の公式ドキュメント:
reflect
パッケージ (https://pkg.go.dev/reflect) - Go言語の公式ブログや関連する技術記事 (一般的なGo言語のパフォーマンス最適化に関する知識)