[インデックス 10462] ファイルの概要
このコミットは、Go言語の標準ライブラリである go/printer
パッケージにおける、string
型と []byte
型間の不要な変換を削除し、コードのクリーンアップとパフォーマンスの改善を図るものです。具体的には、コメントの処理に関連する関数において、[]byte
を直接扱う代わりに string
を使用するように変更されています。これにより、わずかながらも測定可能なパフォーマンス向上(約1.5%)が達成されました。
コミット
commit 17e493a2b11939f294bcf5e2d9b8fa0738ddf51e
Author: Robert Griesemer <gri@golang.org>
Date: Fri Nov 18 19:10:45 2011 -0800
go/printer: remove gratuitous string/[]byte conversions
Cleanup and slight performance improvement (1.5%).
Before (best of 3 runs):
printer.BenchmarkPrint 50 47377420 ns/op
After (best of 3 runs):
printer.BenchmarkPrint 50 46707180 ns/op
R=rsc
CC=golang-dev
https://golang.org/cl/5416049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/17e493a2b11939f294bcf5e2d9b8fa0738ddf51e
元コミット内容
go/printer: remove gratuitous string/[]byte conversions
このコミットは、go/printer
パッケージから不必要な string
と []byte
の変換を削除します。
クリーンアップとわずかなパフォーマンス改善(1.5%)をもたらします。
(ベンチマーク結果)
変更前(3回の実行で最良値):
printer.BenchmarkPrint 50 47377420 ns/op
変更後(3回の実行で最良値):
printer.BenchmarkPrint 50 46707180 ns/op
レビュー担当者: rsc CC: golang-dev 変更リスト: https://golang.org/cl/5416049
変更の背景
Go言語において、string
型と []byte
型は密接に関連していますが、異なる特性を持っています。string
は不変なバイト列であり、[]byte
は可変なバイトスライスです。これら二つの型間の変換は、多くの場合、新しいメモリ割り当てとデータのコピーを伴います。特に、頻繁に変換が行われるようなコードパスでは、これらの変換がパフォーマンスのボトルネックとなる可能性があります。
go/printer
パッケージは、Goのソースコードを整形(フォーマット)する役割を担っています。このパッケージは、コンパイラやツールチェーンの一部として頻繁に利用されるため、そのパフォーマンスはGo開発全体のユーザーエクスペリエンスに影響を与えます。
このコミットの背景には、go/printer
パッケージ内でコメント処理を行う際に、string
と []byte
の間で不必要に変換が行われている箇所が存在するという認識がありました。以前のコードには、// TODO(gri): It should be possible to convert the code below from using // []byte to string and in the process eliminate some conversions.
というコメントがあり、このコミットはそのTODOを解決するものです。不要な変換を排除することで、メモリ割り当てとコピーのオーバーヘッドを削減し、全体的なパフォーマンスを向上させることが目的でした。
前提知識の解説
Go言語における string
と []byte
string
型: Go言語のstring
型は、UTF-8でエンコードされたバイト列の不変なシーケンスです。文字列は一度作成されると内容を変更できません。内部的には、文字列は読み取り専用のバイトスライスとして実装されており、そのデータは通常、ヒープ上に割り当てられます。[]byte
型 (バイトスライス):[]byte
は、Go言語における可変長のバイトシーケンスです。これはスライスの一種であり、基になる配列の一部を参照します。[]byte
の内容は変更可能であり、必要に応じて拡張・縮小できます。
string
と []byte
の変換コスト
string
と []byte
の間の変換は、Goランタイムが新しいメモリを割り当て、元のデータを新しいメモリ領域にコピーする必要があるため、コストがかかります。
string
から[]byte
への変換 ([]byte(myString)
):新しいバイトスライスが作成され、myString
の内容がそこにコピーされます。[]byte
からstring
への変換 (string(myBytes)
):新しい文字列が作成され、myBytes
の内容がそこにコピーされます。
これらの変換は、特に大きなデータや頻繁な操作の場合に、ガベージコレクションの負荷を増やし、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
go/printer
パッケージ
go/printer
パッケージは、Goの抽象構文木(AST: Abstract Syntax Tree)を受け取り、それをGoのソースコードとして整形(pretty-print)する機能を提供します。このパッケージは、go fmt
コマンドや go doc
コマンドなど、Goツールチェーンの多くの部分で利用されています。コードの整形は、コメントの処理、インデントの調整、空白の挿入など、多くのテキスト操作を伴います。
Goのベンチマーク
Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマークツールがあります。go test -bench=.
コマンドを実行すると、Benchmark
プレフィックスを持つ関数が実行され、操作あたりの時間(ns/op)や割り当てられたメモリ量(B/op)、割り当て回数(allocs/op)などのメトリクスが出力されます。これにより、コード変更がパフォーマンスに与える影響を定量的に評価できます。
技術的詳細
このコミットの主要な技術的変更は、go/printer
パッケージ内の複数の関数において、[]byte
型の引数や戻り値を string
型に変更したことです。これにより、関数内部での string
と []byte
間の不必要な変換が排除されました。
具体的には、以下の関数が変更されました。
-
split
関数:- 変更前:
func split(text []byte) [][]byte
- 変更後:
func split(text string) []string
- この関数はコメントテキストを改行で分割する役割を担っています。変更前は
[]byte
を受け取り[][]byte
を返していましたが、変更後はstring
を受け取り[]string
を返すようになりました。これにより、関数内部でのバイトスライス操作が文字列操作に変わり、string(c)
やstring(text[i:j])
のような変換が不要になりました。 - 興味深い点として、
strings.Split(text, "\n")
を使用するよりも、カスタムのループで分割する方がこの特定の目的では高速であるというコメントが追加されています。これは、strings.Split
がより汎用的な処理を行うため、オーバーヘッドがあることを示唆しています。
- 変更前:
-
isBlank
関数:- 変更前:
func isBlank(s []byte) bool
- 変更後:
func isBlank(s string) bool
- この関数は、与えられたバイト列(または文字列)が空白文字のみで構成されているかをチェックします。引数を
string
に変更することで、内部でのs[i]
アクセスが直接文字列のバイトにアクセスできるようになり、string(b)
のような変換が不要になりました。
- 変更前:
-
commonPrefix
関数:- 変更前:
func commonPrefix(a, b []byte) []byte
- 変更後:
func commonPrefix(a, b string) string
- この関数は、二つのバイト列(または文字列)の共通のプレフィックスを特定します。引数と戻り値を
string
に変更することで、スライス操作が直接文字列に対して行われるようになり、変換が不要になりました。
- 変更前:
-
stripCommonPrefix
関数:- 変更前:
func stripCommonPrefix(lines [][]byte)
- 変更後:
func stripCommonPrefix(lines []string)
- この関数は、コメント行から共通のプレフィックスを削除する処理を行います。
lines
の型が[][]byte
から[]string
に変更されたことで、内部でのbytes.Index
やbytes.HasSuffix
の呼び出しがstrings.Index
やstrings.HasSuffix
に置き換えられました。 strings.HasSuffix(prefix, string(suffix))
のように、suffix
が[]byte
のままでstring()
に変換されている箇所が一つ残っています。これは、suffix
が[]byte{'*', '/'}
のようなリテラルで定義されており、strings.HasSuffix
がstring
型の引数を期待するため、この変換は「不必要な」変換とは見なされなかったか、あるいは避けられない変換であったことを示唆しています。
- 変更前:
-
writeComment
関数:- この関数は、コメントを実際にプリンタに書き出す役割を担っています。
strconv.Atoi(string(pos[i+1:]))
がstrconv.Atoi(pos[i+1:])
に変更され、string()
変換が削除されました。p.pos.Filename = string(file)
がp.pos.Filename = file
に変更され、string()
変換が削除されました。lines := split([]byte(text))
がlines := split(text)
に変更され、[]byte()
変換が削除されました。p.writeItem(pos, p.escape(string(line)))
がp.writeItem(pos, p.escape(line))
に変更され、string()
変換が削除されました。
これらの変更により、string
と []byte
の間でデータがコピーされる回数が減少し、結果としてメモリ割り当てが削減され、ガベージコレクションの負担が軽減されます。これがベンチマーク結果に示されるパフォーマンス向上に繋がっています。
コアとなるコードの変更箇所
変更は src/pkg/go/printer/printer.go
ファイルに集中しています。
--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -362,25 +362,24 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as
}\n }\n \n-// TODO(gri): It should be possible to convert the code below from using\n-// []byte to string and in the process eliminate some conversions.\n-\n // Split comment text into lines\n-func split(text []byte) [][]byte {\n+// (using strings.Split(text, \"\\n\") is significantly slower for\n+// this specific purpose, as measured with: gotest -bench=Print)\n+func split(text string) []string {\n // count lines (comment text never ends in a newline)\n n := 1\n- for _, c := range text {\n- \tif c == \'\\n\' {\n+ for i := 0; i < len(text); i++ {\n+ \tif text[i] == \'\\n\' {\n \t\t\tn++\n \t\t}\n \t}\n \n // split\n- lines := make([][]byte, n)\n+ lines := make([]string, n)\n \tn = 0\n \ti := 0\n- for j, c := range text {\n- \tif c == \'\\n\' {\n+ for j := 0; j < len(text); j++ {\n+ \tif text[j] == \'\\n\' {\n \t\t\tlines[n] = text[i:j] // exclude newline\n \t\t\ti = j + 1 // discard newline\n \t\t\tn++\n@@ -391,16 +390,18 @@ func split(text []byte) [][]byte {\n \treturn lines\n }\n \n-func isBlank(s []byte) bool {\n- for _, b := range s {\n- \tif b > \' \' {\n+// Returns true if s contains only white space\n+// (only tabs and blanks can appear in the printer\'s context).\n+func isBlank(s string) bool {\n+ for i := 0; i < len(s); i++ {\n+ \tif s[i] > \' \' {\n \t\t\treturn false\n \t\t}\n \t}\n \treturn true\n }\n \n-func commonPrefix(a, b []byte) []byte {\n+func commonPrefix(a, b string) string {\n \ti := 0\n \tfor i < len(a) && i < len(b) && a[i] == b[i] && (a[i] <= \' \' || a[i] == \'*\') {\n \t\ti++\n@@ -408,7 +409,7 @@ func commonPrefix(a, b []byte) []byte {\n \treturn a[0:i]\n }\n \n-func stripCommonPrefix(lines [][]byte) {\n+func stripCommonPrefix(lines []string) {\n \tif len(lines) < 2 {\n \t\treturn // at most one line - nothing to do\n \t}\n@@ -432,19 +433,21 @@ func stripCommonPrefix(lines [][]byte) {\n \t// Note that the first and last line are never empty (they\n \t// contain the opening /* and closing */ respectively) and\n \t// thus they can be ignored by the blank line check.\n-\tvar prefix []byte\n+\tvar prefix string\n \tif len(lines) > 2 {\n+\t\tfirst := true\n \t\tfor i, line := range lines[1 : len(lines)-1] {\n \t\t\tswitch {\n \t\t\tcase isBlank(line):\n-\t\t\t\tlines[1+i] = nil // range starts at line 1\n-\t\t\tcase prefix == nil:\n+\t\t\t\tlines[1+i] = \"\" // range starts at line 1\n+\t\t\tcase first:\n \t\t\t\tprefix = commonPrefix(line, line)\n+\t\t\t\tfirst = false\n \t\t\tdefault:\n \t\t\t\tprefix = commonPrefix(prefix, line)\n \t\t\t}\n \t\t}\n-\t} else { // len(lines) == 2\n+\t} else { // len(lines) == 2, lines cannot be blank (contain /* and */)\n \t\tline := lines[1]\n \t\tprefix = commonPrefix(line, line)\n \t}\n@@ -453,7 +456,7 @@ func stripCommonPrefix(lines [][]byte) {\n \t * Check for vertical \"line of stars\" and correct prefix accordingly.\n \t */\n \tlineOfStars := false\n-\tif i := bytes.Index(prefix, []byte{\'*\'}); i >= 0 {\n+\tif i := strings.Index(prefix, \"*\"); i >= 0 {\n \t\t// Line of stars present.\n \t\tif i > 0 && prefix[i-1] == \' \' {\n \t\t\ti-- // remove trailing blank from prefix so stars remain aligned\n@@ -501,7 +504,7 @@ func stripCommonPrefix(lines [][]byte) {\n \t\t\t}\n \t\t\t// Shorten the computed common prefix by the length of\n \t\t\t// suffix, if it is found as suffix of the prefix.\n-\t\t\tif bytes.HasSuffix(prefix, suffix) {\n+\t\t\tif strings.HasSuffix(prefix, string(suffix)) {\n \t\t\t\tprefix = prefix[0 : len(prefix)-len(suffix)]\n \t\t\t}\n \t\t}\n@@ -511,19 +514,18 @@ func stripCommonPrefix(lines [][]byte) {\n \t// with the opening /*, otherwise align the text with the other\n \t// lines.\n \tlast := lines[len(lines)-1]\n-\tclosing := []byte(\"*/\")\n-\ti := bytes.Index(last, closing)\n+\tclosing := \"*/\"\n+\ti := strings.Index(last, closing) // i >= 0 (closing is always present)\n \tif isBlank(last[0:i]) {\n \t\t// last line only contains closing */\n-\t\tvar sep []byte\n \t\tif lineOfStars {\n-\t\t\t// insert an aligning blank\n-\t\t\tsep = []byte{\' \'}\n+\t\t\tclosing = \" */\" // add blank to align final star\n \t\t}\n-\t\tlines[len(lines)-1] = bytes.Join([][]byte{prefix, closing}, sep)\n+\t\tlines[len(lines)-1] = prefix + closing\n \t} else {\n \t\t// last line contains more comment text - assume\n-\t\t// it is aligned like the other lines\n+\t\t// it is aligned like the other lines and include\n+\t\t// in prefix computation\n \t\tprefix = commonPrefix(prefix, last)\n \t}\n \n@@ -549,9 +551,9 @@ func (p *printer) writeComment(comment *ast.Comment) {\n \t\t\t// update our own idea of the file and line number\n \t\t\t// accordingly, after printing the directive.\n \t\t\tfile := pos[:i]\n-\t\t\tline, _ := strconv.Atoi(string(pos[i+1:]))\n+\t\t\tline, _ := strconv.Atoi(pos[i+1:])\n \t\t\tdefer func() {\n-\t\t\t\tp.pos.Filename = string(file)\n+\t\t\t\tp.pos.Filename = file\n \t\t\t\tp.pos.Line = line\n \t\t\t\tp.pos.Column = 1\n \t\t\t}()\n@@ -566,7 +568,7 @@ func (p *printer) writeComment(comment *ast.Comment) {\n \n \t// for /*-style comments, print line by line and let the\n \t// write function take care of the proper indentation\n-\tlines := split([]byte(text))\n+\tlines := split(text)\n \tstripCommonPrefix(lines)\n \n \t// write comment lines, separated by formfeed,\n@@ -579,7 +581,7 @@ func (p *printer) writeComment(comment *ast.Comment) {\n \t\t\tpos = p.pos\n \t\t}\n \t\tif len(line) > 0 {\n-\t\t\tp.writeItem(pos, p.escape(string(line)))\n+\t\t\tp.writeItem(pos, p.escape(line))\n \t\t}\n \t}\n }\n```
## コアとなるコードの解説
上記の差分は、主に以下のパターンで変更が行われていることを示しています。
1. **関数シグネチャの変更**:
* `split`, `isBlank`, `commonPrefix`, `stripCommonPrefix` の各関数で、`[]byte` 型の引数や戻り値が `string` 型に変更されています。これにより、これらの関数が `string` データを直接処理できるようになり、呼び出し元や関数内部での `string()` や `[]byte()` による型変換が不要になります。
2. **内部ループとアクセス方法の変更**:
* `split` および `isBlank` 関数では、`for _, c := range text` のような `range` ループが `for i := 0; i < len(text); i++` のようなインデックスベースのループに変更されています。これは、`string` 型の要素アクセスが `text[i]` のようにバイト単位で行われるためです。`range` ループはUTF-8のコードポイントをイテレートするため、バイト単位の処理にはインデックスアクセスがより適切です。
3. **スライス作成と代入の変更**:
* `split` 関数内で `lines := make([][]byte, n)` が `lines := make([]string, n)` に変更され、`lines[n] = text[i:j]` のように、直接文字列スライスが代入されるようになりました。これにより、`string(text[i:j])` のような変換が不要になります。
4. **`nil` から空文字列への変更**:
* `stripCommonPrefix` 関数内で、空白行を表現するために `lines[1+i] = nil` としていた箇所が `lines[1+i] = ""` に変更されています。`string` スライスでは `nil` は有効な要素ではないため、空文字列 `""` を使用するのが適切です。
5. **`bytes` パッケージから `strings` パッケージへの移行**:
* `stripCommonPrefix` 関数内で、バイトスライス操作を行う `bytes.Index` や `bytes.HasSuffix` が、文字列操作を行う `strings.Index` や `strings.HasSuffix` に置き換えられています。これにより、文字列データを直接扱うことが可能になり、`[]byte{'*'}` や `[]byte("*/")` のようなバイトスライスリテラルを `"*"` や `"*/"` のような文字列リテラルに置き換えることができました。
* ただし、`strings.HasSuffix(prefix, string(suffix))` のように、`suffix` が `[]byte` 型のままで `string()` に変換されている箇所が一つあります。これは、`suffix` が `[]byte` リテラルとして定義されているため、`strings.HasSuffix` の引数型に合わせるために必要な変換であり、「不必要な」変換とは見なされなかったと考えられます。
6. **`writeComment` 関数における変換の削除**:
* `strconv.Atoi(string(pos[i+1:]))` や `p.pos.Filename = string(file)`、`lines := split([]byte(text))`、`p.writeItem(pos, p.escape(string(line)))` といった箇所で、明示的な `string()` や `[]byte()` への変換が削除されています。これは、関連する関数や変数の型が `string` に変更されたため、これらの変換がもはや不要になったことを意味します。
これらの変更は、Go言語の `string` と `[]byte` の特性を深く理解し、パフォーマンスに影響を与える可能性のある型変換を特定し、それを排除するという、Goのイディオムに沿った最適化の典型例と言えます。
## 関連リンク
* Go言語の `string` 型と `[]byte` 型に関する公式ドキュメントやブログ記事:
* [Go Slices: usage and internals - The Go Programming Language](https://go.dev/blog/slices) (スライス全般についてですが、`[]byte` の理解に役立ちます)
* [Strings, bytes, runes and characters in Go - The Go Programming Language](https://go.dev/blog/strings) (文字列とバイト、ルーンの関係について)
* `go/printer` パッケージのドキュメント:
* [go/printer package - go/printer - Go Packages](https://pkg.go.dev/go/printer)
* Go言語のベンチマークに関するドキュメント:
* [Testing Go programs - The Go Programming Language](https://go.dev/doc/tutorial/add-a-test#benchmarks)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のブログ記事
* Go言語のソースコード(特に `src/pkg/go/printer/printer.go`)
* Go言語のベンチマークに関する一般的な知識
* `string` と `[]byte` の変換コストに関するGoコミュニティの議論
* GitHubのコミット履歴と差分表示
* Gerritの変更リスト (https://golang.org/cl/5416049)
* `strings` パッケージと `bytes` パッケージのGoドキュメント
---
**注記**: この解説は、提供されたコミット情報と一般的なGo言語の知識に基づいて生成されています。Go言語の進化に伴い、`string` と `[]byte` の内部実装や最適化は変更される可能性があります。