[インデックス 10493] ファイルの概要
このコミットは、Go言語の標準ライブラリであるgo/printer
パッケージ内のprinter.go
ファイルに対する変更を扱っています。go/printer
パッケージは、Goの抽象構文木(AST)を整形されたGoのソースコードとして出力する役割を担っています。このファイルは、gofmt
ツールなど、Goコードの自動整形を行うツールの中核をなす部分です。
コミット
このコミットは、go/printer
パッケージとgofmt
ツールにおけるパフォーマンスのさらなる改善を目的としています。具体的には、不要な文字列変換を削減し、ボトルネックとなっていた出力インターフェースを合理化することで、ASTの整形処理を約6%高速化しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b3923a27dd80592ec4cd21ca04ea2a736578c9ad
元コミット内容
commit b3923a27dd80592ec4cd21ca04ea2a736578c9ad
Author: Robert Griesemer <gri@golang.org>
Date: Wed Nov 23 09:27:38 2011 -0800
go/printer, gofmt: more performance tweaks
Removed more string conversions and streamlined bottleneck
printing interface by removing unnecessary tests where possible.
About 6% faster AST printing.
Before:
- printer.BenchmarkPrint 50 32056640 ns/op
After:
- printer.BenchmarkPrint 50 30138440 ns/op (-6%)
R=r
CC=golang-dev
https://golang.org/cl/5431047
変更の背景
この変更の主な背景は、go/printer
パッケージ、ひいてはgofmt
ツールのパフォーマンス向上です。コードの整形処理は、開発ワークフローにおいて頻繁に実行される操作であり、その速度は開発者の生産性に直結します。特に、大規模なコードベースやCI/CDパイプラインにおいて、整形処理のわずかな遅延も積み重なると大きな影響を及ぼします。
以前のバージョンでは、文字列の変換や、出力処理における冗長なチェックがパフォーマンスのボトルネックとなっていました。このコミットは、これらの非効率な部分を特定し、より直接的で効率的なデータ操作に置き換えることで、処理速度の改善を目指しています。コミットメッセージに記載されているベンチマーク結果(約6%の高速化)は、この最適化が成功したことを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの概念を理解しておく必要があります。
go/printer
パッケージ: Go言語の標準ライブラリの一部で、Goのソースコードを抽象構文木(AST)から整形して出力する機能を提供します。gofmt
ツールはこのパッケージを利用しています。gofmt
: Go言語の公式なコード整形ツールです。Goのソースコードを標準的なスタイルに自動的に整形します。開発者がコードスタイルについて議論する時間を削減し、一貫性のあるコードベースを維持するのに役立ちます。- 抽象構文木 (AST): プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやリンタ、フォーマッタなどのツールは、ソースコードをASTに変換してから処理を行います。
token.FileSet
:go/token
パッケージの一部で、ソースファイルの位置情報(ファイル名、行番号、列番号など)を管理するための構造体です。ASTノードは、ソースコード内の対応する位置への参照としてtoken.Pos
を持ち、これをFileSet
と組み合わせることで具体的な位置情報を取得できます。bytes.Buffer
:bytes
パッケージの一部で、可変長のバイトシーケンスを扱うためのバッファです。効率的なバイト列の構築や操作に適しており、文字列の連結などで頻繁に利用されます。tabwriter
パッケージ:text/tabwriter
パッケージの一部で、テキストをタブ区切りで整形するためのライターです。列の幅を自動調整し、きれいに揃った出力を生成するのに使われます。tabwriter.Escape
は、tabwriter
が特別な意味を持つ文字をエスケープするために使用するバイトです。- パフォーマンスベンチマーク (Go): Go言語には、
testing
パッケージにベンチマークテストを記述するための機能が組み込まれています。go test -bench=.
コマンドで実行でき、関数の実行時間やメモリ割り当てなどを測定し、パフォーマンスの回帰を検出したり、最適化の効果を評価したりするのに役立ちます。
技術的詳細
このコミットにおける技術的な最適化は、主に以下の点に集約されます。
-
whiteSpace
型の変更:type whiteSpace int
からtype whiteSpace byte
へと変更されました。whiteSpace
型は、空白文字の種類(無視、スペース、タブ、改行など)を表すために使用されます。これらの値は非常に小さいため、int
型ではなく1バイトのbyte
型を使用することで、メモリ使用量をわずかに削減し、データアクセスを高速化できます。これはマイクロ最適化の一例です。
-
printer
構造体のフィールドの合理化:litbuf bytes.Buffer
フィールドが削除されました。このフィールドは、エスケープされたリテラルやコメントを一時的に構築するために使用されていました。escape
関数も削除されました。この関数はlitbuf
を使用して文字列をエスケープし、tabwriter.Escape
バイトで囲んでいました。- これらの変更は、中間的な
bytes.Buffer
の割り当てと、それに伴う文字列変換のオーバーヘッドを排除することを目的としています。
-
出力処理の再設計と効率化:
- 従来の
write(data string)
関数は、文字列の書き込み、改行処理、インデントの挿入、tabwriter.Escape
文字の処理など、複数の役割を担っていました。この関数は、内部で文字列のインデックス操作や部分文字列の生成を行っており、効率的ではありませんでした。 - これを、より粒度の高い以下の関数に分割・最適化しました。
writeByte(ch byte)
: 単一のバイトをp.output
に書き込み、p.pos
(現在の出力位置)を更新します。特に改行文字が書き込まれた際には、適切なインデントを挿入するロジックが含まれています。これにより、単一文字の書き込みが非常に効率的になりました。writeString(s string, isLit bool)
: 文字列s
をp.output
に書き込みます。isLit
(is Literal)フラグが導入され、これがtrue
の場合、tabwriter.Escape
バイトが文字列の前後に直接p.output
に書き込まれます。これにより、tabwriter
が文字列の内容を解釈しないように保護しつつ、中間的な文字列変換を完全に回避できます。
- この変更により、文字列のコピーや一時的なバッファの利用が大幅に削減され、特に頻繁に呼び出される文字出力パスでのパフォーマンスが向上しました。
- 従来の
-
既存関数の
write
からwriteByte
/writeString
への移行:writeItem
、writeCommentPrefix
、writeComment
、writeCommentSuffix
、intersperseComments
、writeWhitespace
、print
といった、文字列や文字を出力する既存の関数が、新しいwriteByte
やwriteString
関数を使用するように変更されました。- 例えば、
p.write(" ")
のような単一スペースの書き込みはp.writeByte(' ')
に、p.write(string(ch))
はp.writeByte(byte(ch))
に置き換えられています。 *ast.BasicLit
(基本リテラル、例: 文字列リテラル、数値リテラル)の処理では、以前はp.escape(x.Value)
でエスケープ処理を行っていましたが、新しいwriteString
関数にisLit: true
を渡すことで、このエスケープ処理がより効率的に行われるようになりました。
これらの変更は、Goのプリンタが大量の文字や文字列を処理するという性質を考慮した、典型的なマイクロ最適化です。不要なメモリ割り当てを減らし、関数呼び出しのオーバーヘッドを削減することで、全体的な実行速度を向上させています。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/pkg/go/printer/printer.go
ファイルに集中しています。
特に以下の部分が変更されています。
type whiteSpace
の定義type pmode
の定数定義type printer struct
のフィールド定義(output
、litbuf
の変更)escape
関数の削除write
関数の削除と、それに代わるwriteByte
、writeNewlines
、writeString
関数の追加writeItem
関数のシグネチャと実装の変更writeCommentPrefix
、writeComment
、writeCommentSuffix
、intersperseComments
、writeWhitespace
、print
関数内の出力ロジックの変更
コアとなるコードの解説
type whiteSpace byte
-type whiteSpace int
+type whiteSpace byte
whiteSpace
型がint
からbyte
に変更されました。これは、この型が取りうる値が非常に小さく、1バイトで十分表現できるため、メモリ効率とアクセス速度を向上させるための最適化です。
printer
構造体の変更
type printer struct {
// Configuration (does not change after initialization)
Config
- fset *token.FileSet
- output bytes.Buffer
+ fset *token.FileSet
// Current state
- indent int // current indentation
- mode pmode // current printer mode
- lastTok token.Token // the last token printed (token.ILLEGAL if it's whitespace)
-
- // Reused buffers
- wsbuf []whiteSpace // delayed white space
- litbuf bytes.Buffer // for creation of escaped literals and comments
+ output bytes.Buffer // raw printer result
+ indent int // current indentation
+ mode pmode // current printer mode
+ lastTok token.Token // the last token printed (token.ILLEGAL if it's whitespace)
+ wsbuf []whiteSpace // delayed white space
}
litbuf bytes.Buffer
が削除され、output bytes.Buffer
がReused buffers
セクションからCurrent state
セクションに移動しました。litbuf
の削除は、エスケープ処理がインライン化され、一時的なバッファが不要になったことを示しています。
escape
関数の削除
-// escape escapes string s by bracketing it with tabwriter.Escape.
-// Escaped strings pass through tabwriter unchanged. (Note that
-// valid Go programs cannot contain tabwriter.Escape bytes since
-// they do not appear in legal UTF-8 sequences).\n-//
-func (p *printer) escape(s string) string {
- p.litbuf.Reset()
- p.litbuf.WriteByte(tabwriter.Escape)
- p.litbuf.WriteString(s)
- p.litbuf.WriteByte(tabwriter.Escape)
- return p.litbuf.String()
-}
escape
関数が完全に削除されました。この関数は、文字列をtabwriter.Escape
バイトで囲むためにlitbuf
を使用していましたが、この処理は新しいwriteString
関数に統合され、より効率的な方法で直接p.output
に書き込まれるようになりました。
write
関数の置き換え (writeByte
, writeNewlines
, writeString
)
writeByte(ch byte)
// writeByte writes a single byte to p.output and updates p.pos.
func (p *printer) writeByte(ch byte) {
p.output.WriteByte(ch)
p.pos.Offset++
p.pos.Column++
if ch == '\n' || ch == '\f' {
// write indentation
// use "hard" htabs - indentation columns
// must not be discarded by the tabwriter
const htabs = "\t\t\t\t\t\t\t\t"
j := p.indent
for j > len(htabs) {
p.output.WriteString(htabs)
j -= len(htabs)
}
p.output.WriteString(htabs[0:j])
// update p.pos
p.pos.Line++
p.pos.Offset += p.indent
p.pos.Column = 1 + p.indent
}
}
この関数は、単一のバイトをbytes.Buffer
に書き込むための最も基本的なプリミティブです。改行文字が検出された場合、適切なインデントを自動的に挿入します。これにより、単一文字の書き込みが非常に効率的になり、不要な文字列変換がなくなりました。
writeNewlines(n int, nl byte)
// writeNewlines writes up to n newlines to p.output and updates p.pos.
// The actual number of newlines written is limited by nlines.
// nl must be one of '\n' or '\f'.
//
func (p *printer) writeNewlines(n int, nl byte) {
for n = p.nlines(n, 0); n > 0; n-- {
p.writeByte(nl)
}
}
この関数は、指定された回数だけ改行文字(\n
または\f
)を書き込みます。内部でwriteByte
を呼び出すことで、効率的な改行処理を実現しています。
writeString(s string, isLit bool)
// writeString writes the string s to p.output and updates p.pos.
// If isLit is set, s is escaped w/ tabwriter.Escape characters
// to protect s from being interpreted by the tabwriter.
//
// Note: writeString is only used to write Go tokens, literals, and
// comments, all of which must be written literally. Thus, it is correct
// to always set isLit = true. However, setting it explicitly only when
// needed (i.e., when we don't know that s contains no tabs or line breaks)
// avoids processing extra escape characters and reduces run time of the
// printer benchmark by up to 10%.
//
func (p *printer) writeString(s string, isLit bool) {
if isLit {
// Protect s such that is passes through the tabwriter
// unchanged. Note that valid Go programs cannot contain
// tabwriter.Escape bytes since they do not appear in legal
// UTF-8 sequences.
p.output.WriteByte(tabwriter.Escape)
}
p.output.WriteString(s)
// update p.pos
nlines := 0
column := p.pos.Column + len(s)
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
nlines++
column = len(s) - i
}
}
p.pos.Offset += len(s)
p.pos.Line += nlines
p.pos.Column = column
if isLit {
p.output.WriteByte(tabwriter.Escape)
}
}
この関数は、文字列をbytes.Buffer
に書き込むための主要な関数です。isLit
フラグがtrue
の場合、tabwriter.Escape
バイトを文字列の前後に直接書き込みます。これにより、以前のescape
関数で行っていた中間的な文字列変換が不要になり、パフォーマンスが大幅に向上しました。また、p.pos
の更新ロジックもこの関数内に統合されています。
writeItem
関数の変更
-func (p *printer) writeItem(pos token.Position, data string) {
+func (p *printer) writeItem(pos token.Position, data string, isLit bool) {
// ...
- p.write(data)
+ p.writeString(data, isLit)
p.last = p.pos
}
writeItem
関数は、isLit
引数を新しく受け取るようになり、内部でp.write
の代わりにp.writeString
を呼び出すようになりました。これにより、リテラル文字列の整形時に適切なエスケープ処理が効率的に適用されます。
これらの変更は、go/printer
の出力パスにおけるボトルネックを特定し、文字列の割り当てと変換を最小限に抑えることで、全体的なパフォーマンスを向上させるという明確な目標を持って行われています。
関連リンク
- Go Change-Id:
https://golang.org/cl/5431047
参考にした情報源リンク
- 特になし (提供された情報とコード差分のみで解説を生成しました)