[インデックス 18682] ファイルの概要
このコミットは、Go言語のgo/printer
パッケージにおける変更を扱っています。go/printer
は、Goのソースコードを整形(フォーマット)するためのツールであり、gofmt
コマンドの基盤となっています。この変更の主な目的は、コード整形時のパフォーマンスを向上させることです。具体的には、整形後の出力における行数や構造の測定方法を変更することで、処理速度の改善を図っています。
コミット
- コミットハッシュ:
28cc8aa89eb1830c71c8cf5c39f7ce4a0ceb4899
- 作者: Robert Griesemer gri@golang.org
- コミット日時: 2014年2月27日 木曜日 11:35:53 -0800
- コミットメッセージ:
go/printer: measure lines/construct in generated output rather than incoming source No change to $GOROOT/src, misc formatting. Nice side-effect: almost 3% faster runs because it's much faster to compute line number differences in the generated output than the incoming source. Benchmark run, best of 5 runs, before and after: BenchmarkPrint 200 12347587 ns/op BenchmarkPrint 200 11999061 ns/op Fixes #4504. LGTM=adonovan R=golang-codereviews, adonovan CC=golang-codereviews https://golang.org/cl/69260045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/28cc8aa89eb1830c71c_cf5c39f7ce4a0ceb4899
元コミット内容
上記の「コミット」セクションに記載されているコミットメッセージが元コミット内容です。
変更の背景
この変更の主な背景は、Go言語のコードフォーマッタであるgo/printer
のパフォーマンス改善です。従来のgo/printer
は、入力されたソースコードの行情報に基づいて、整形後の出力における改行やインデントの判断を行っていました。しかし、この方法では、特に複雑なAST(抽象構文木)構造を持つコードの場合、行番号の差分計算に時間がかかっていました。
コミットメッセージにもあるように、このアプローチはBenchmarkPrint
の実行時間において、約3%の速度低下を引き起こしていました。このパフォーマンスのボトルネックを解消し、より高速なコード整形を実現するために、整形後の「生成された出力」の行情報を基準に改行や構造の測定を行うように変更されました。これにより、計算がより効率的になり、全体的な処理速度が向上しました。
また、このコミットはGo issue #4504を修正するものです。このissueの具体的な内容は不明ですが、おそらくgo/printer
のパフォーマンスや、特定のコード構造における整形結果に関する問題であったと推測されます。
前提知識の解説
go/printer
パッケージ
go/printer
パッケージは、Go言語のソースコードを整形(フォーマット)するためのライブラリです。Goの公式フォーマッタであるgofmt
コマンドの内部で利用されており、GoのAST(抽象構文木)を受け取り、整形されたGoのソースコードを出力します。このパッケージは、Goのコードスタイルガイドラインに沿った一貫したフォーマットを保証するために不可欠な役割を担っています。
AST (Abstract Syntax Tree)
AST(抽象構文木)は、ソースコードの抽象的な構文構造を木構造で表現したものです。Goコンパイラやツール(go/printer
など)は、ソースコードを解析してASTを生成し、そのASTを操作することで、コードの分析、変換、整形などを行います。ASTの各ノードは、変数宣言、関数呼び出し、制御構造などのコード要素に対応しています。
token.Position
token.Position
は、Goのソースコード内の特定の位置(ファイル名、行番号、列番号、オフセット)を表す構造体です。go/parser
パッケージによってソースコードが解析されASTが構築される際に、ASTの各ノードには、それがソースコードのどの部分に対応するかを示すtoken.Pos
(位置のオフセット)が関連付けられます。token.FileSet
と組み合わせることで、token.Pos
から人間が読めるtoken.Position
(行番号、列番号など)に変換できます。
コード整形における行数測定の重要性
コード整形において、改行やインデントの挿入は、コードの可読性を大きく左右します。特に、構造体や関数の引数リスト、宣言ブロックなど、複数の要素が並ぶ箇所では、各要素が複数行にわたるかどうかによって、整形結果が大きく変わることがあります。例えば、短い要素であれば1行にまとめるが、長い要素やコメントを含む場合は複数行に分割するといったルールが適用されます。
このような整形ルールを適用するためには、各コード要素が「何行にわたるか」を正確に測定する必要があります。従来のgo/printer
では、この測定を「入力ソースコード」の行情報に基づいて行っていました。しかし、整形処理自体が改行を挿入したり、既存の改行を削除したりするため、入力ソースコードの行情報と整形後の出力の行情報が一致しないことが多々あります。この不一致が、複雑な計算や非効率な処理を引き起こす原因となっていました。
「生成された出力」での測定の利点
このコミットで導入された「生成された出力」での行数測定は、この問題を解決します。整形処理中に、実際に出力されるコードの行数をリアルタイムで追跡し、その情報に基づいて改行やインデントの判断を行います。これにより、以下のような利点が得られます。
- 正確性: 整形後の最終的なレイアウトに基づいて判断するため、より正確な整形結果が得られます。
- 効率性: 入力ソースコードの行情報と出力の行情報の間の複雑なマッピングや変換が不要になり、計算が単純化されます。これにより、パフォーマンスが向上します。
- シンプルさ: コードのロジックがより直感的になり、保守性が向上します。
技術的詳細
このコミットの主要な変更点は、go/printer
がコード構造の行数を測定する際に、元のソースコードの行情報ではなく、整形後の出力の行情報を使用するように変更されたことです。
具体的には、以下の変更が行われました。
-
isMultiLine
関数の削除:src/pkg/go/printer/nodes.go
から、p.isMultiLine(n ast.Node) bool
関数が削除されました。この関数は、ASTノードn
が元のソースコード上で複数行にわたるかどうかをp.lineFor(n.End())-p.lineFor(n.Pos()) > 0
という形で判断していました。この関数が削除されたことで、元のソースコードの行情報に基づく判断が不要になりました。 -
printer
構造体へのフィールド追加:src/pkg/go/printer/printer.go
のprinter
構造体に、linePtr *int
という新しいフィールドが追加されました。このポインタは、次に非空白トークンが出力される際の出力行番号を記録するためのものです。 -
recordLine
関数の追加:src/pkg/go/printer/printer.go
にrecordLine(linePtr *int)
関数が追加されました。この関数は、p.linePtr
フィールドに引数で渡されたlinePtr
を設定します。これにより、printer
は次にトークンが出力される際に、そのトークンの出力行番号を*linePtr
に記録する準備ができます。 -
linesFrom
関数の追加:src/pkg/go/printer/printer.go
にlinesFrom(line int) int
関数が追加されました。この関数は、現在の出力行番号(p.out.Line
)と引数で渡されたline
の差分を返します。これは、特定の構造体や要素が整形後の出力で何行にわたるかを計算するために使用されます。保留中の空白やコメントは無視されます。 -
print
メソッドでの行番号記録:src/pkg/go/printer/printer.go
のprint
メソッド(GoのASTノードを整形して出力する主要なメソッド)内で、p.linePtr
が設定されている場合に、現在の出力行番号(p.out.Line
)を*p.linePtr
に記録し、p.linePtr
をnil
にリセットする処理が追加されました。これにより、各トークンが出力される直前に、そのトークンが開始する出力行番号が正確に記録されるようになります。 -
fieldList
、stmtList
、genDecl
でのrecordLine
とlinesFrom
の利用:src/pkg/go/printer/nodes.go
内のfieldList
(構造体やインターフェースのフィールドリストを処理)、stmtList
(ステートメントリストを処理)、genDecl
(一般的な宣言(const
,var
,type
)を処理)といった主要な整形ロジックにおいて、isMultiLine
の代わりにrecordLine
とlinesFrom
が使用されるようになりました。newSection
フラグの代わりに、var line int
を宣言し、各要素の整形前にp.recordLine(&line)
を呼び出し、改行の判断にp.linesFrom(line) > 0
を使用しています。これにより、各要素が整形後の出力で複数行にわたる場合に、適切な改行が挿入されるようになります。
-
ラベル付きステートメントの行数計算の修正:
stmtList
関数内で、ラベル付きステートメント(L: stmt
のような形式)の行数計算が修正されました。以前はunlabeledStmt
関数を使用してラベルを取り除いた後のステートメントの行数を計算していましたが、新しいアプローチでは、ラベルの数だけ行数をインクリメントすることで、ラベルが複数行にわたる場合でも正確な行数を反映するように変更されました。これに伴い、unlabeledStmt
関数は削除されました。
これらの変更により、go/printer
は、整形後の出力の特性をより直接的に考慮して改行やインデントの判断を行うようになり、結果としてパフォーマンスが向上しました。
パフォーマンスへの影響
コミットメッセージに記載されているベンチマーク結果は、この変更がパフォーマンスに与えた好影響を明確に示しています。
- 変更前:
BenchmarkPrint 200 12347587 ns/op
- 変更後:
BenchmarkPrint 200 11999061 ns/op
これは約2.8%の速度向上に相当し、go/printer
のような頻繁に利用されるツールにとっては大きな改善です。
既知の回帰
このコミットは、提出直後にwindows-386-ec2
ビルダで回帰(regression)を引き起こしたことが報告されています。これは、変更が特定の環境やアーキテクチャで予期せぬ副作用をもたらしたことを示唆しています。このような回帰は、大規模なプロジェクトにおけるコード変更では珍しくなく、その後のコミットで修正されることが一般的です。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更箇所は以下の通りです。
-
src/pkg/go/printer/nodes.go
:isMultiLine
関数の削除。fieldList
関数内でnewSection
変数の代わりにline
変数を導入し、p.recordLine(&line)
とp.linesFrom(line) > 0
を使用するように変更。stmtList
関数内でmultiLine
変数の代わりにline
変数を導入し、p.recordLine(&line)
とp.linesFrom(line) > 0
を使用するように変更。stmtList
関数内のラベル付きステートメントの行数計算ロジックを修正。unlabeledStmt
関数の削除。genDecl
関数内でnewSection
変数の代わりにline
変数を導入し、p.recordLine(&line)
とp.linesFrom(line) > 0
を使用するように変更。
-
src/pkg/go/printer/printer.go
:printer
構造体にlinePtr *int
フィールドを追加。internalError
関数の定義位置を移動(機能的な変更なし)。recordLine(linePtr *int)
関数の追加。linesFrom(line int) int
関数の追加。print
メソッド内でp.linePtr
が設定されている場合に、出力行番号を記録するロジックを追加。
-
src/pkg/go/printer/testdata/comments2.golden
-
src/pkg/go/printer/testdata/comments2.input
-
src/pkg/go/printer/testdata/declarations.golden
-
src/pkg/go/printer/testdata/declarations.input
- テストデータが更新され、新しい整形ロジックに対応するように変更されています。特に
declarations.golden
とdeclarations.input
には、新しいアライメントルールをテストするためのコメントが追加されています。
- テストデータが更新され、新しい整形ロジックに対応するように変更されています。特に
コアとなるコードの解説
src/pkg/go/printer/nodes.go
の変更
このファイルでは、ASTノードの具体的な整形ロジックが定義されています。主要な変更は、isMultiLine
関数が削除され、代わりにrecordLine
とlinesFrom
の組み合わせが使用されるようになった点です。
変更前(例: fieldList
関数の一部):
newSection := false
for i, f := range list {
if i > 0 {
p.linebreak(p.lineFor(f.Pos()), 1, ignore, newSection)
}
// ...
newSection = p.isMultiLine(f)
}
ここでは、newSection
というブール値のフラグが、次の要素の前に新しいセクション(改行)を挿入するかどうかを制御していました。このフラグは、現在の要素が元のソースコード上で複数行にわたるかどうか(p.isMultiLine(f)
)に基づいて設定されていました。
変更後(例: fieldList
関数の一部):
var line int
for i, f := range list {
if i > 0 {
p.linebreak(p.lineFor(f.Pos()), 1, ignore, p.linesFrom(line) > 0)
}
p.setComment(f.Doc)
p.recordLine(&line) // ここで次のトークンの出力行番号を記録する準備
// ...
}
変更後では、newSection
フラグの代わりにvar line int
が導入されました。各要素の整形処理の直前にp.recordLine(&line)
が呼び出されます。これにより、その要素の最初のトークンが出力される際に、その出力行番号がline
変数に記録されます。そして、次の要素の前に改行を挿入するかどうかの判断は、p.linesFrom(line) > 0
によって行われます。これは、前の要素が整形後の出力で複数行にわたったかどうかを正確に判断します。
同様の変更がstmtList
やgenDecl
といった他の整形ロジックにも適用されています。
src/pkg/go/printer/printer.go
の変更
このファイルはgo/printer
のコアとなるロジックを含んでいます。
printer
構造体への追加:
type printer struct {
// ...
linePtr *int // if set, record out.Line for the next token in *linePtr
}
linePtr
は、recordLine
関数によって設定され、print
メソッドによって利用されます。
recordLine
関数の追加:
func (p *printer) recordLine(linePtr *int) {
p.linePtr = linePtr
}
この関数は、nodes.go
の整形ロジックから呼び出され、printer
に「次にトークンが出力されたら、その行番号をこのポインタに記録してほしい」と指示します。
linesFrom
関数の追加:
func (p *printer) linesFrom(line int) int {
return p.out.Line - line
}
この関数は、nodes.go
の整形ロジックから呼び出され、line
に記録された行番号から現在の出力行番号までの差分を返します。これにより、整形中の要素が何行にわたるかを正確に知ることができます。
print
メソッドの変更:
func (p *printer) print(args ...interface{}) {
// ...
// the next token starts now - record its line number if requested
if p.linePtr != nil {
*p.linePtr = p.out.Line
p.linePtr = nil
}
// ...
}
print
メソッドは、実際にトークンを整形して出力する際に呼び出されます。この変更により、p.linePtr
が設定されている場合(つまり、recordLine
が呼び出されていた場合)、現在の出力行番号(p.out.Line
)が*p.linePtr
に代入されます。これにより、nodes.go
で必要とされていた行番号が正確に取得されます。その後、p.linePtr
はnil
にリセットされ、次の記録要求に備えます。
これらの変更により、go/printer
は、元のソースコードの行情報に依存することなく、整形後の出力の行情報を基に、より効率的かつ正確な整形判断を行うことができるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/28cc8aa89eb1830c71c_cf5c39f7ce4a0ceb4899
- Go Code Review 69260045: https://golang.org/cl/69260045
参考にした情報源リンク
- Go Code Review 69260045: https://golang.org/cl/69260045
- Go言語の
go/printer
パッケージに関する一般的な情報(Goのドキュメントなど) - 抽象構文木(AST)に関する一般的な情報
token.Position
に関するGoのドキュメント