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

[インデックス 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点が挙げられています。

  1. Stringerインターフェースなどのチェックの最適化: fmtパッケージは、値を文字列に変換する際に、その値がStringererrorなどの特定のインターフェースを実装しているかどうかをチェックします。これらのチェックは、リフレクションを伴う場合があり、コストが高い処理です。コミットメッセージでは、「組み込み型でない場合にのみStringerなどのチェックが発火する」という特性を利用し、組み込み型であることが分かっている場合にはこの高コストなチェックをスキップすることで、無駄な処理を削減しようとしています。
  2. パディング処理の高速化: フォーマット指定子(例: %5s)によって文字列や数値にパディング(余白)を追加する処理も、頻繁に実行されるため、その効率が全体のパフォーマンスに影響します。このコミットでは、パディングが不要なケース(幅指定がない、または幅が0の場合)に高速パスを導入することで、処理のオーバーヘッドを削減しています。

これらの最適化により、fmtパッケージを利用するアプリケーション全体のパフォーマンス向上が期待されます。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とfmtパッケージの内部動作に関する知識が必要です。

  1. fmtパッケージ: Go言語の標準ライブラリの一つで、C言語のprintf/scanfに似た書式付きI/O機能を提供します。fmt.Sprintffmt.Printffmt.Fprintなどの関数が含まれ、様々な型の値を整形して文字列に出力したり、文字列から値を読み取ったりするために使用されます。
  2. Stringerインターフェース: fmtパッケージが値を文字列に変換する際に利用する重要なインターフェースです。
    type Stringer interface {
        String() string
    }
    
    任意の型がString() stringメソッドを実装している場合、fmtパッケージはその型の値をフォーマットする際に、自動的にこのString()メソッドを呼び出して文字列表現を取得します。これにより、カスタム型を人間が読める形式で出力できるようになります。
  3. リフレクション (Reflection): Go言語のリフレクションは、プログラムの実行時に型情報や値の情報を検査・操作する機能です。reflectパッケージを通じて提供されます。fmtパッケージは、Stringerインターフェースの実装チェックや、カスタム型のフィールドへのアクセスなど、コンパイル時には型が不明な値を扱う際にリフレクションを内部的に使用します。リフレクションは非常に強力ですが、通常の直接的なメソッド呼び出しやフィールドアクセスに比べて実行時コストが高いという特性があります。
  4. パディング (Padding): fmtパッケージのフォーマット指定子(例: %5d, %10s)には、出力される値の最小幅を指定する機能があります。指定された幅よりも値の文字列表現が短い場合、残りのスペースは空白などの文字で埋められます。これをパディングと呼びます。左寄せ(デフォルト)や右寄せ(-フラグ)などのオプションもあります。
  5. pp構造体とfmt構造体: fmtパッケージの内部では、フォーマット処理の状態を管理するためにpp(printer)やfmt(formatter)といった内部構造体が使われています。これらの構造体は、出力バッファ、フォーマットフラグ、幅、精度などの情報を保持し、実際のフォーマットロジックを実行します。

技術的詳細

このコミットによるパフォーマンス改善は、主に以下の2つの技術的変更によって実現されています。

  1. print.goにおけるhandleMethods呼び出しの最適化: src/pkg/fmt/print.gopp.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)
    

    この変更により、boolintstringといった頻繁にフォーマットされる組み込み型に対しては、不要なhandleMethodsの呼び出し(およびそれに伴うリフレクション)がスキップされるようになり、パフォーマンスが向上します。handleMethodsは、型が組み込み型ではない場合にのみ呼び出されるようになりました。

  2. format.goにおけるpadおよびpadString関数の高速パス導入: src/pkg/fmt/format.gofmt.padfmt.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.Printfmt.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ケース(つまり、fieldbool, int, string, []byteなどの組み込み型ではない場合)に移動されました。
  • これにより、fieldが組み込み型である場合には、高コストなp.handleMethodsの呼び出しが完全にスキップされるようになり、パフォーマンスが向上します。組み込み型は頻繁にフォーマットされるため、この変更は全体的な速度向上に大きく貢献します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: fmtパッケージ (https://pkg.go.dev/fmt)
  • Go言語の公式ドキュメント: reflectパッケージ (https://pkg.go.dev/reflect)
  • Go言語の公式ブログや関連する技術記事 (一般的なGo言語のパフォーマンス最適化に関する知識)