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

[インデックス 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では、この測定を「入力ソースコード」の行情報に基づいて行っていました。しかし、整形処理自体が改行を挿入したり、既存の改行を削除したりするため、入力ソースコードの行情報と整形後の出力の行情報が一致しないことが多々あります。この不一致が、複雑な計算や非効率な処理を引き起こす原因となっていました。

「生成された出力」での測定の利点

このコミットで導入された「生成された出力」での行数測定は、この問題を解決します。整形処理中に、実際に出力されるコードの行数をリアルタイムで追跡し、その情報に基づいて改行やインデントの判断を行います。これにより、以下のような利点が得られます。

  1. 正確性: 整形後の最終的なレイアウトに基づいて判断するため、より正確な整形結果が得られます。
  2. 効率性: 入力ソースコードの行情報と出力の行情報の間の複雑なマッピングや変換が不要になり、計算が単純化されます。これにより、パフォーマンスが向上します。
  3. シンプルさ: コードのロジックがより直感的になり、保守性が向上します。

技術的詳細

このコミットの主要な変更点は、go/printerがコード構造の行数を測定する際に、元のソースコードの行情報ではなく、整形後の出力の行情報を使用するように変更されたことです。

具体的には、以下の変更が行われました。

  1. isMultiLine関数の削除: src/pkg/go/printer/nodes.goから、p.isMultiLine(n ast.Node) bool関数が削除されました。この関数は、ASTノードnが元のソースコード上で複数行にわたるかどうかをp.lineFor(n.End())-p.lineFor(n.Pos()) > 0という形で判断していました。この関数が削除されたことで、元のソースコードの行情報に基づく判断が不要になりました。

  2. printer構造体へのフィールド追加: src/pkg/go/printer/printer.goprinter構造体に、linePtr *intという新しいフィールドが追加されました。このポインタは、次に非空白トークンが出力される際の出力行番号を記録するためのものです。

  3. recordLine関数の追加: src/pkg/go/printer/printer.gorecordLine(linePtr *int)関数が追加されました。この関数は、p.linePtrフィールドに引数で渡されたlinePtrを設定します。これにより、printerは次にトークンが出力される際に、そのトークンの出力行番号を*linePtrに記録する準備ができます。

  4. linesFrom関数の追加: src/pkg/go/printer/printer.golinesFrom(line int) int関数が追加されました。この関数は、現在の出力行番号(p.out.Line)と引数で渡されたlineの差分を返します。これは、特定の構造体や要素が整形後の出力で何行にわたるかを計算するために使用されます。保留中の空白やコメントは無視されます。

  5. printメソッドでの行番号記録: src/pkg/go/printer/printer.goprintメソッド(GoのASTノードを整形して出力する主要なメソッド)内で、p.linePtrが設定されている場合に、現在の出力行番号(p.out.Line)を*p.linePtrに記録し、p.linePtrnilにリセットする処理が追加されました。これにより、各トークンが出力される直前に、そのトークンが開始する出力行番号が正確に記録されるようになります。

  6. fieldListstmtListgenDeclでのrecordLinelinesFromの利用: src/pkg/go/printer/nodes.go内のfieldList(構造体やインターフェースのフィールドリストを処理)、stmtList(ステートメントリストを処理)、genDecl(一般的な宣言(const, var, type)を処理)といった主要な整形ロジックにおいて、isMultiLineの代わりにrecordLinelinesFromが使用されるようになりました。

    • newSectionフラグの代わりに、var line intを宣言し、各要素の整形前にp.recordLine(&line)を呼び出し、改行の判断にp.linesFrom(line) > 0を使用しています。これにより、各要素が整形後の出力で複数行にわたる場合に、適切な改行が挿入されるようになります。
  7. ラベル付きステートメントの行数計算の修正: 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.goldendeclarations.inputには、新しいアライメントルールをテストするためのコメントが追加されています。

コアとなるコードの解説

src/pkg/go/printer/nodes.goの変更

このファイルでは、ASTノードの具体的な整形ロジックが定義されています。主要な変更は、isMultiLine関数が削除され、代わりにrecordLinelinesFromの組み合わせが使用されるようになった点です。

変更前(例: 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によって行われます。これは、前の要素が整形後の出力で複数行にわたったかどうかを正確に判断します。

同様の変更がstmtListgenDeclといった他の整形ロジックにも適用されています。

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.linePtrnilにリセットされ、次の記録要求に備えます。

これらの変更により、go/printerは、元のソースコードの行情報に依存することなく、整形後の出力の行情報を基に、より効率的かつ正確な整形判断を行うことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go Code Review 69260045: https://golang.org/cl/69260045
  • Go言語のgo/printerパッケージに関する一般的な情報(Goのドキュメントなど)
  • 抽象構文木(AST)に関する一般的な情報
  • token.Positionに関するGoのドキュメント