[インデックス 10487] ファイルの概要
このコミットは、Go言語の標準ライブラリであるgo/printer
パッケージと、それを利用するフォーマッタツールgofmt
のパフォーマンス改善を目的としています。具体的には、中間出力にbytes.Buffer
を使用することで、gofmt
の実行速度を20%から30%向上させています。
コミット
commit a0e54aaffa3d67b3caf9a30ffa1d0b1f359d34b1
Author: Robert Griesemer <gri@golang.org>
Date: Tue Nov 22 15:12:34 2011 -0800
go/printer, gofmt: 20 to 30% faster gofmt
Buffer intermediate output via a bytes.Buffer and thus avoid
calling through the entire Writer stack for every item printed.
There is more opportunity for improvements along the same lines.
Before (best of 3 runs):
- printer.BenchmarkPrint 50 47959760 ns/op
- time gofmt -l $GOROOT/src real 0m11.517s
After (best of 3 runs):
- printer.BenchmarkPrint 50 32056640 ns/op (= -33%)
- time gofmt -l $GOROOT/src real 0m9.070s (= -21%)
R=r
CC=golang-dev
https://golang.org/cl/5432054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a0e54aaffa3d67b3caf9a30ffa1d0b1f359d34b1
元コミット内容
このコミットは、go/printer
パッケージとgofmt
のパフォーマンスを20%から30%向上させるものです。これは、中間出力をbytes.Buffer
経由でバッファリングすることで、出力される各項目ごとにio.Writer
スタック全体を呼び出すことを避けることによって実現されました。同様の改善の機会は他にも存在します。
変更前(3回の実行のうち最良の結果):
printer.BenchmarkPrint
: 50回実行で47959760 ns/optime gofmt -l $GOROOT/src
: リアルタイムで0m11.517s
変更後(3回の実行のうち最良の結果):
printer.BenchmarkPrint
: 50回実行で32056640 ns/op(-33%改善)time gofmt -l $GOROOT/src
: リアルタイムで0m9.070s(-21%改善)
変更の背景
go/printer
パッケージは、Goの抽象構文木(AST)を整形し、人間が読める形式のGoコードとして出力する役割を担っています。gofmt
ツールはこのgo/printer
パッケージを内部的に利用して、Goソースコードの自動整形を行います。
従来のgo/printer
の実装では、整形されたコードの小さな断片が生成されるたびに、直接io.Writer
インターフェースを通じて出力ストリームに書き込まれていました。io.Writer
は非常に汎用的なインターフェースであり、ファイル、ネットワークソケット、標準出力など、様々な出力先にデータを書き込むことができます。しかし、この汎用性ゆえに、特に頻繁に小さな書き込みが行われる場合、各書き込み操作には一定のオーバーヘッドが伴います。
特に、go/printer
がtabwriter.Writer
のような追加の処理層を介して出力を行う場合、このオーバーヘッドは顕著になります。tabwriter.Writer
は、タブ文字やスペースを適切に処理してカラムを揃えるためのロジックを持っており、その処理自体にもコストがかかります。小さなデータを頻繁にtabwriter.Writer
に渡すと、その都度内部バッファリングや整形ロジックが起動し、パフォーマンスのボトルネックとなる可能性がありました。
このコミットの背景には、gofmt
の実行速度を向上させ、開発者の生産性を高めるという明確な目標がありました。特に大規模なGoプロジェクトでは、gofmt
の実行時間が長くなると、コードの整形が開発ワークフローの妨げになることがあります。そのため、出力処理の効率化が求められていました。コミットメッセージにある// TODO(gri) Replace bottleneck []byte conversion // with writing into a bytes.Buffer.
というコメントからも、開発者がこのボトルネックを認識しており、bytes.Buffer
の導入が以前から検討されていたことが伺えます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が不可欠です。
-
io.Writer
インターフェース:- Go言語の
io
パッケージで定義されている基本的なインターフェースの一つです。 Write(p []byte) (n int, err error)
という単一のメソッドを持ちます。- このインターフェースを実装する型は、バイトスライス
p
を何らかの出力先に書き込むことができます。 - ファイル、ネットワーク接続、標準出力、メモリバッファなど、様々な出力先がこのインターフェースを実装しています。
go/printer
のようなライブラリがio.Writer
を受け取ることで、出力先を柔軟に選択できるようになります。
- Go言語の
-
bytes.Buffer
型:bytes
パッケージで提供される型で、可変長のバイトバッファをメモリ上に保持します。io.Writer
インターフェースを実装しており、Write
メソッドを通じてデータをバッファに追加できます。- また、
io.Reader
インターフェースも実装しているため、バッファからデータを読み出すことも可能です。 WriteString
、WriteByte
、Read
、Bytes
、String
などの便利なメソッドを提供します。- 特に、頻繁に小さなデータを書き込む必要がある場合に、直接
io.Writer
に書き込むよりも効率的です。これは、bytes.Buffer
が内部的にバイトスライスを効率的に拡張し、システムコールを最小限に抑えるためです。
-
go/printer
パッケージ:- Go言語のソースコードを整形するためのパッケージです。
- 抽象構文木(AST: Abstract Syntax Tree)を受け取り、それをGo言語の標準的なフォーマットに従って文字列として出力します。
gofmt
ツールはこのパッケージを基盤としています。
-
gofmt
ツール:- Go言語の公式なコードフォーマッタです。
- Goソースコードを自動的に整形し、Goコミュニティ全体で一貫したコーディングスタイルを強制します。
- 開発ワークフローにおいて、コードの可読性を高め、レビュープロセスを効率化するために広く利用されています。
-
パフォーマンス最適化の一般的な原則:
- バッファリング: 頻繁な小さなI/O操作はコストが高いため、データを一時的にメモリに蓄積(バッファリング)し、まとめて大きな塊として書き込むことで効率を向上させます。
- システムコールの削減: オペレーティングシステムへのシステムコールは、ユーザーモードからカーネルモードへのコンテキストスイッチを伴うため、比較的コストが高い操作です。これを減らすことはパフォーマンス向上に繋がります。
- メモリ割り当ての最適化: 頻繁なメモリ割り当てと解放はガベージコレクションの負荷を増大させ、パフォーマンスに影響を与えます。
bytes.Buffer
のように内部的にメモリを効率的に管理するデータ構造は、この問題を緩和します。
技術的詳細
このコミットの核心は、go/printer
パッケージにおける出力処理のアーキテクチャ変更にあります。
変更前は、printer
構造体が直接io.Writer
型のoutput
フィールドを持っていました。コードの整形中に生成される文字列の断片は、printer.write0
やprinter.write
といったメソッドを通じて、このoutput
フィールドに直接書き込まれていました。
// 変更前のprinter構造体の一部
type printer struct {
output io.Writer // 直接io.Writerに出力
// ...
written int // 書き込まれたバイト数を追跡
}
// 変更前のwrite0メソッド (削除された)
func (p *printer) write0(data string) {
if len(data) > 0 {
// TODO(gri) Replace bottleneck []byte conversion
// with writing into a bytes.Buffer.
// Will also simplify post-processing.
n, err := p.output.Write([]byte(data)) // ここで直接io.Writerに書き込み
p.written += n
if err != nil {
panic(printerError{err})
}
}
}
このアプローチの問題点は、go/printer
が非常に多くの小さな文字列(例えば、キーワード、識別子、句読点、スペースなど)を生成し、それらを個別にio.Writer
に書き込んでいた点です。特に、tabwriter.Writer
のような追加の処理層がio.Writer
チェーンに含まれる場合、各Write
呼び出しは、tabwriter
の内部ロジック(タブの展開、カラムの調整など)をトリガーし、これがパフォーマンスのボトルネックとなっていました。
このコミットでは、この問題を解決するために、printer
構造体のoutput
フィールドをio.Writer
からbytes.Buffer
に変更しました。
// 変更後のprinter構造体の一部
type printer struct {
Config
fset *token.FileSet
output bytes.Buffer // bytes.Bufferを内部バッファとして使用
}
これにより、printer
の内部では、整形されたコードの断片が直接io.Writer
に書き込まれるのではなく、まずbytes.Buffer
に蓄積されるようになりました。bytes.Buffer
はメモリ上で効率的にバイトを蓄積できるため、WriteString
などの操作は非常に高速です。
// 変更後のwriteメソッドの一部
func (p *printer) write(data string) {
// ...
// write segment ending in data[i]
p.output.WriteString(data[i0 : i+1]) // bytes.Bufferに書き込み
// ...
// write remaining segment
p.output.WriteString(data[i0:]) // bytes.Bufferに書き込み
// ...
}
そして、Config.fprint
メソッドの最後で、printer
が整形処理を完了した後、bytes.Buffer
に蓄積された全てのデータ(p.output.Bytes()
)が、一度に最終的なio.Writer
(trimmer
やtabwriter
を介して)に書き込まれるようになりました。
// 変更後のConfig.fprintメソッドの一部
func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{}, nodeSizes map[ast.Node]int) (err error) {
// ...
// print node (この中でp.output (bytes.Buffer) に書き込まれる)
var p printer
p.init(cfg, fset, nodeSizes)
if err = p.printNode(node); err != nil {
return
}
p.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF)
// ... (trimmer, tabwriterの設定)
// write printer result via tabwriter/trimmer to output
if _, err = output.Write(p.output.Bytes()); err != nil { // ここでbytes.Bufferの内容をまとめて最終出力に書き込み
return
}
// ... (tabwriterのフラッシュ)
return
}
この変更により、io.Writer
へのWrite
呼び出しの回数が劇的に減少しました。これにより、io.Writer
インターフェースのオーバーヘッドや、tabwriter
のような中間層の処理コストが大幅に削減され、結果としてgo/printer
およびgofmt
の全体的なパフォーマンスが向上しました。
また、エラーハンドリングの変更も行われています。以前はprinterError
というカスタムエラー型を定義し、panic
/recover
メカニズムを使用してエラーを伝播していましたが、このコミットでprintNode
メソッドが導入され、エラーを直接error
型として返すようになりました。これにより、よりGoらしいエラーハンドリングパターンに移行し、コードの可読性と保守性が向上しています。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、src/pkg/go/printer/printer.go
ファイルに集中しています。
-
printer
構造体の変更:output io.Writer
フィールドが削除され、代わりにoutput bytes.Buffer
フィールドが追加されました。written int
フィールドが削除されました。
-
printer.init
メソッドのシグネチャ変更:output io.Writer
引数が削除されました。
-
printer.write0
メソッドの削除:- このメソッドは、直接
io.Writer
に書き込むためのものでしたが、bytes.Buffer
への書き込みに置き換えられたため不要になりました。
- このメソッドは、直接
-
printer.write
メソッド内の変更:p.write0
への呼び出しが、p.output.WriteString
への呼び出しに置き換えられました。
-
printer.writeItem
メソッド内の変更:- デバッグ出力部分で、
p.write0
への呼び出しがfmt.Fprintf(&p.output, ...)
に置き換えられました。
- デバッグ出力部分で、
-
printer.writeCommentPrefix
メソッド内の変更:p.written == 0
という条件がp.output.Len() == 0
に置き換えられました。これは、bytes.Buffer
の長さでバッファが空かどうかを判断するためです。
-
printer.printNode
メソッドの新規追加:- ASTノードの型に基づいて適切な整形ロジックを呼び出すための新しいヘルパーメソッドが追加されました。以前は
Config.fprint
内にあったロジックが分離されました。
- ASTノードの型に基づいて適切な整形ロジックを呼び出すための新しいヘルパーメソッドが追加されました。以前は
-
Config.fprint
メソッドの変更:- 戻り値の型が
(written int, err error)
から(err error)
に変更されました。 printer
構造体の初期化時に、内部のoutput
フィールドがbytes.Buffer
として初期化されるようになりました。- 以前
Config.fprint
内にあったASTノードの型に応じた整形ロジックが、新しく追加されたp.printNode
メソッドの呼び出しに置き換えられました。 panic
/recover
によるエラーハンドリングが削除されました。- 最終的に、
p.output.Bytes()
(bytes.Buffer
の内容)が、output.Write()
を通じて実際のio.Writer
に書き込まれるようになりました。
- 戻り値の型が
-
Config.Fprint
メソッドの変更:- 戻り値の
written int
が常に0
を返すように変更されました。これは、内部バッファリングにより、このレベルでは書き込まれたバイト数が直接追跡されなくなったためです。
- 戻り値の
-
src/pkg/go/printer/nodes.go
の変更:cfg.fprint
の呼び出し箇所で、戻り値のwritten int
が不要になったため、その部分が削除されました。
コアとなるコードの解説
主要な変更はsrc/pkg/go/printer/printer.go
にあります。
printer
構造体の変更:
// 変更前
type printer struct {
// ...
output io.Writer
// ...
written int // number of bytes written
// ...
}
// 変更後
type printer struct {
// ...
fset *token.FileSet
output bytes.Buffer // io.Writerからbytes.Bufferに変更
// ...
}
この変更が最も重要です。printer
が直接外部のio.Writer
に書き込むのではなく、内部のbytes.Buffer
に書き込むようになりました。これにより、Write
呼び出しの頻度が大幅に減り、パフォーマンスが向上します。written
フィールドはbytes.Buffer
のLen()
メソッドで代替できるため削除されました。
printer.init
メソッドの変更:
// 変更前
func (p *printer) init(output io.Writer, cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) {
p.output = output
// ...
}
// 変更後
func (p *printer) init(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) {
// p.outputはbytes.Buffer型なので、ここでは初期化不要(ゼロ値で十分)
p.Config = *cfg
p.fset = fset
// ...
}
printer
の初期化時にio.Writer
を受け取らなくなりました。bytes.Buffer
は構造体のゼロ値で利用可能であり、明示的な初期化は不要です。
printer.write0
メソッドの削除とprinter.write
の変更:
write0
メソッドは、直接io.Writer
にバイトスライスを書き込む役割を担っていましたが、bytes.Buffer
の導入により不要になりました。
// 変更前のwrite0メソッド (削除された)
// func (p *printer) write0(data string) { ... }
// 変更後のwriteメソッドの一部
func (p *printer) write(data string) {
// ...
// 以前は p.write0(data[i0 : i+1]) だった部分
p.output.WriteString(data[i0 : i+1]) // bytes.Buffer.WriteStringを使用
// ...
// 以前は p.write0(data[i0:]) だった部分
p.output.WriteString(data[i0:]) // bytes.Buffer.WriteStringを使用
// ...
}
write
メソッド内で、以前p.write0
を呼び出していた箇所が、直接p.output.WriteString
を呼び出すように変更されました。これにより、文字列が直接bytes.Buffer
に追加され、中間的な[]byte
変換やio.Writer
への頻繁な呼び出しがなくなりました。
printer.printNode
メソッドの追加:
func (p *printer) printNode(node interface{}) error {
switch n := node.(type) {
case ast.Expr:
p.useNodeComments = true
p.expr(n, ignoreMultiLine)
// ... 他のASTノードタイプ
default:
return fmt.Errorf("go/printer: unsupported node type %T", n)
}
return nil
}
この新しいメソッドは、与えられたASTノードの型に応じて、適切な整形ロジック(p.expr
, p.stmt
, p.decl
など)を呼び出す役割を担います。以前はConfig.fprint
内に直接記述されていたこのロジックが分離され、コードの構造が整理されました。また、エラーをpanic
ではなくerror
として返すようになりました。
Config.fprint
メソッドの変更:
// 変更前
// func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{}, nodeSizes map[ast.Node]int) (written int, err error) {
// ...
// 変更後
func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{}, nodeSizes map[ast.Node]int) (err error) {
// print node
var p printer
p.init(cfg, fset, nodeSizes) // printerを初期化
if err = p.printNode(node); err != nil { // 新しいprintNodeを呼び出し
return
}
p.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF)
// ... (trimmer, tabwriterの設定)
// write printer result via tabwriter/trimmer to output
if _, err = output.Write(p.output.Bytes()); err != nil { // ここでbytes.Bufferの内容をまとめて出力
return
}
// ... (tabwriterのフラッシュ)
return
}
Config.fprint
は、go/printer
パッケージの主要なエントリポイントの一つです。このメソッド内で、printer
構造体が初期化され、printNode
メソッドが呼び出されてASTの整形が行われます。整形結果はp.output
(bytes.Buffer
)に蓄積されます。
最も重要な変更は、整形処理が完了した後、p.output.Bytes()
を呼び出してbytes.Buffer
の内容全体を取得し、それを一度だけ最終的なoutput io.Writer
に書き込んでいる点です。これにより、io.Writer
へのWrite
呼び出しが1回(またはtabwriter
のフラッシュを含めて数回)に削減され、大幅なパフォーマンス向上が実現されました。
また、以前はpanic
/recover
で処理されていたエラーが、printNode
からのerror
戻り値として直接処理されるようになり、よりGoらしいエラーハンドリングになりました。
関連リンク
- Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語の
go/printer
パッケージドキュメント: https://pkg.go.dev/go/printer - Go言語の
gofmt
ツールに関する公式ドキュメント: https://go.dev/blog/gofmt - Go言語の
tabwriter
パッケージドキュメント: https://pkg.go.dev/text/tabwriter
参考にした情報源リンク
- Go言語の
io.Writer
インターフェースとbytes.Buffer
のパフォーマンスに関する一般的な解説記事: - Go言語の
fmt.Sprintf
とbytes.Buffer
の比較に関する記事: - Go言語の
go/printer
パッケージの設計に関する情報(一般的な情報源として):- https://golang.bg/blog/go-printer-package (これは一般的な情報源であり、特定のコミットに直接関連するものではありませんが、パッケージの理解に役立ちます)