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

[インデックス 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=.コマンドで実行でき、関数の実行時間やメモリ割り当てなどを測定し、パフォーマンスの回帰を検出したり、最適化の効果を評価したりするのに役立ちます。

技術的詳細

このコミットにおける技術的な最適化は、主に以下の点に集約されます。

  1. whiteSpace型の変更:

    • type whiteSpace int から type whiteSpace byte へと変更されました。
    • whiteSpace型は、空白文字の種類(無視、スペース、タブ、改行など)を表すために使用されます。これらの値は非常に小さいため、int型ではなく1バイトのbyte型を使用することで、メモリ使用量をわずかに削減し、データアクセスを高速化できます。これはマイクロ最適化の一例です。
  2. printer構造体のフィールドの合理化:

    • litbuf bytes.Bufferフィールドが削除されました。このフィールドは、エスケープされたリテラルやコメントを一時的に構築するために使用されていました。
    • escape関数も削除されました。この関数はlitbufを使用して文字列をエスケープし、tabwriter.Escapeバイトで囲んでいました。
    • これらの変更は、中間的なbytes.Bufferの割り当てと、それに伴う文字列変換のオーバーヘッドを排除することを目的としています。
  3. 出力処理の再設計と効率化:

    • 従来のwrite(data string)関数は、文字列の書き込み、改行処理、インデントの挿入、tabwriter.Escape文字の処理など、複数の役割を担っていました。この関数は、内部で文字列のインデックス操作や部分文字列の生成を行っており、効率的ではありませんでした。
    • これを、より粒度の高い以下の関数に分割・最適化しました。
      • writeByte(ch byte): 単一のバイトをp.outputに書き込み、p.pos(現在の出力位置)を更新します。特に改行文字が書き込まれた際には、適切なインデントを挿入するロジックが含まれています。これにより、単一文字の書き込みが非常に効率的になりました。
      • writeString(s string, isLit bool): 文字列sp.outputに書き込みます。isLit(is Literal)フラグが導入され、これがtrueの場合、tabwriter.Escapeバイトが文字列の前後に直接p.outputに書き込まれます。これにより、tabwriterが文字列の内容を解釈しないように保護しつつ、中間的な文字列変換を完全に回避できます。
    • この変更により、文字列のコピーや一時的なバッファの利用が大幅に削減され、特に頻繁に呼び出される文字出力パスでのパフォーマンスが向上しました。
  4. 既存関数のwriteからwriteByte/writeStringへの移行:

    • writeItemwriteCommentPrefixwriteCommentwriteCommentSuffixintersperseCommentswriteWhitespaceprintといった、文字列や文字を出力する既存の関数が、新しいwriteBytewriteString関数を使用するように変更されました。
    • 例えば、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 のフィールド定義(outputlitbufの変更)
  • escape 関数の削除
  • write 関数の削除と、それに代わる writeBytewriteNewlineswriteString 関数の追加
  • writeItem 関数のシグネチャと実装の変更
  • writeCommentPrefixwriteCommentwriteCommentSuffixintersperseCommentswriteWhitespaceprint 関数内の出力ロジックの変更

コアとなるコードの解説

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.BufferReused 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

参考にした情報源リンク

  • 特になし (提供された情報とコード差分のみで解説を生成しました)