[インデックス 15580] ファイルの概要
コミット
commit 13075ed416bda45d5fcdf4722baec5c940a2ec0c
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Tue Mar 5 04:23:37 2013 +0800
test/bench/go1: use raw string instead of string addition
to reduce compile time memory/stack usage.
Update #4970
$ go test -c ../test/bench/go1
before:
0.36user 0.07system 0:00.44elapsed 100%CPU
(0avgtext+0avgdata 540720maxresident)k
0inputs+19840outputs (0major+56451minor)pagefaults 0swaps
after:
0.33user 0.05system 0:00.39elapsed 100%CPU
(0avgtext+0avgdata 289936maxresident)k
0inputs+19864outputs (0major+29615minor)pagefaults 0swaps
And stack usage is reduced to below 1MiB.
R=golang-dev, r, dave
CC=golang-dev
https://golang.org/cl/7436050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13075ed416bda45d5fcdf4722baec5c940a2ec0c
元コミット内容
test/bench/go1: use raw string instead of string addition to reduce compile time memory/stack usage. Update #4970
このコミットは、test/bench/go1
ディレクトリ内のベンチマークテストにおいて、文字列の結合ではなくGoの「raw string literal」(バッククォートで囲まれた文字列)を使用することで、コンパイル時のメモリ使用量とスタック使用量を削減することを目的としています。
コミットメッセージには、変更前後のgo test -c ../test/bench/go1
コマンドの実行結果が示されており、メモリ使用量(maxresident
)が540720KBから289936KBへと大幅に削減され、スタック使用量も1MiB未満に減少したことが報告されています。
変更の背景
この変更の背景には、Goコンパイラが大きな文字列リテラルを処理する際の効率性の問題があります。特に、複数の小さな文字列を+
演算子で結合して大きな文字列を構築する場合、コンパイラは中間的な文字列オブジェクトを多数生成し、それらをメモリ上に保持する必要があります。これにより、コンパイル時のメモリ消費量が増大し、特にリソースが限られた環境や大規模なプロジェクトでは、コンパイル時間の増加やコンパイラのクラッシュといった問題を引き起こす可能性があります。
コミットメッセージに記載されているUpdate #4970
は、GoのIssueトラッカーにおける特定の課題(Issue 4970)に関連していることを示唆しています。このIssueは、おそらくGoコンパイラのメモリ使用量やパフォーマンスに関するもので、本コミットはその解決策の一部として提案されたと考えられます。
test/bench/go1/jsondata_test.go
ファイルは、Goのベンチマークテストスイートの一部であり、Goコンパイラやランタイムのパフォーマンスを評価するために使用されます。このファイルには、非常に大きなJSONデータが文字列リテラルとして埋め込まれており、これがコンパイル時のメモリ使用量の問題を引き起こしていた可能性が高いです。
前提知識の解説
Go言語の文字列リテラル
Go言語には、主に2種類の文字列リテラルがあります。
-
解釈済み文字列リテラル (Interpreted String Literals):
- ダブルクォート
"
で囲まれた文字列です。 - バックスラッシュ
\
によるエスケープシーケンス(例:\n
、\t
、\"
)が解釈されます。 - 複数行にわたる文字列を記述する場合、各行の終わりに
\n
を含める必要があります。
- ダブルクォート
-
Raw文字列リテラル (Raw String Literals):
- バッククォート
`
で囲まれた文字列です。 - エスケープシーケンスは一切解釈されず、バッククォート内の文字がそのまま文字列の内容となります。
- 複数行にわたる文字列を、改行を含めてそのまま記述できます。これは、JSON、XML、HTMLなどの構造化されたテキストデータをコード内に埋め込む際に非常に便利です。
- バッククォート
コンパイル時のメモリ使用量とスタック使用量
プログラムがコンパイルされる際、コンパイラはソースコードを解析し、中間表現を生成し、最終的な実行可能ファイルを生成します。このプロセス中、コンパイラ自身がメモリを消費します。
- メモリ使用量 (Memory Usage): コンパイラがソースコードの抽象構文木(AST)やシンボルテーブル、型情報、最適化のための中間データなどをメモリにロードして処理するために必要なRAMの量です。大きな文字列リテラルを文字列結合で表現すると、コンパイラは各部分文字列とそれらの結合結果を一時的にメモリに保持する必要があり、これがメモリ使用量を増大させます。
- スタック使用量 (Stack Usage): コンパイラが関数呼び出しやローカル変数を管理するために使用するスタックメモリの量です。特に、非常に長い文字列リテラルを文字列結合で構築するような場合、コンパイラがその処理のために深い再帰的な呼び出しや多数の一時変数をスタックに割り当てる必要が生じ、スタック使用量が増加することがあります。スタックオーバーフローは、コンパイラのクラッシュにつながる可能性があります。
go test -c
コマンド
go test -c
コマンドは、指定されたテストパッケージのテストバイナリをコンパイルしますが、実行はしません。このコマンドは、テストのコンパイル時間やコンパイル時のリソース使用量(メモリ、CPUなど)を測定するのに役立ちます。本コミットでは、このコマンドを用いて変更前後のコンパイル時リソース使用量を比較しています。
技術的詳細
このコミットの主要な技術的変更は、test/bench/go1/jsondata_test.go
ファイル内の巨大なjsonbz2_base64
変数の定義方法です。
変更前:
var jsonbz2_base64 = "QlpoOTFBWSZTWZ0H0LkG0bxfgFH8UAf/8D////q////6YSvJveAAAAAH3ddt7gAN" +
"\t\"FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA" +
// ... 非常に多くの文字列結合 ...
"\t\"FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA"
このように、非常に長いBase64エンコードされたBzip2圧縮JSONデータが、多数の短い文字列リテラルを+
演算子で結合して定義されていました。Goコンパイラは、この+
演算子による文字列結合をコンパイル時に解決しようとします。この際、中間的な文字列が多数生成され、それらがメモリに一時的に保持されるため、コンパイル時のメモリ使用量とスタック使用量が膨大になっていました。
変更後:
var jsonbz2_base64 = []byte(`
+QlpoOTFBWSZTWZ0H0LkG0bxfgFH8UAf/8D////q////6YSvJveAAAAAH3ddt7gAN
+FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA
+// ... 巨大なraw string literal ...
+FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA
+`)
変更後では、jsonbz2_base64
変数は[]byte
型として定義され、その値は単一の巨大なraw文字列リテラル(バッククォートで囲まれた文字列)で初期化されています。raw文字列リテラルは、その内容が文字通りに解釈されるため、コンパイラは文字列結合のような複雑な処理を行う必要がありません。これにより、コンパイル時のメモリ割り当てが大幅に削減され、コンパイル速度も向上します。
また、json_test.go
ファイルでは、strings.NewReader
とstrings.Replace
(これは元のコードには明示的にありませんが、strings
パッケージがインポートされていたことから、文字列操作が行われていたと推測されます)の使用が削除され、代わりにbytes.NewReader
とbytes.Replace
が導入されています。これは、jsonbz2_base64
が[]byte
型になったことに伴う変更です。bytes.Replace(jsonbz2_base64, []byte{'\n'}, nil, -1)
は、raw文字列リテラルに含まれる改行文字を削除するために使用されています。Base64データは通常、改行を含まない単一の長い文字列として扱われるため、この処理が必要です。
この最適化は、特に大きなデータセットをコードに埋め込む場合に、Goコンパイラの効率を向上させる上で非常に重要です。
コアとなるコードの変更箇所
test/bench/go1/json_test.go
--- a/test/bench/go1/json_test.go
+++ b/test/bench/go1/json_test.go
@@ -7,12 +7,12 @@
package go1
import (
+ "bytes"
"compress/bzip2"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
- "strings"
"testing"
)
@@ -23,7 +23,7 @@ var (
func makeJsonBytes() []byte {
var r io.Reader
- r = strings.NewReader(jsonbz2_base64)
+ r = bytes.NewReader(bytes.Replace(jsonbz2_base64, []byte{'\n'}, nil, -1))
r = base64.NewDecoder(base64.StdEncoding, r)
r = bzip2.NewReader(r)
b, err := ioutil.ReadAll(r)
test/bench/go1/jsondata_test.go
--- a/test/bench/go1/jsondata_test.go
+++ b/test/bench/go1/jsondata_test.go
@@ -13,1806 +13,1807 @@
package go1
-var jsonbz2_base64 = "QlpoOTFBWSZTWZ0H0LkG0bxfgFH8UAf/8D////q////6YSvJveAAAAAH3ddt7gAN" +
-\t"FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA" +
-// ... (中略:非常に長い文字列結合) ...
-\t"FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA"
+var jsonbz2_base64 = []byte(`
+QlpoOTFBWSZTWZ0H0LkG0bxfgFH8UAf/8D////q////6YSvJveAAAAAH3ddt7gAN
+FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA
+// ... (中略:非常に長いraw文字列リテラル) ...
+FrKppN9gw0gA++fGB9xKkUpX0YWTENCgqzUW1tlsyMB2w9nnvNSigNyS+3cui5zA
+`)
コアとなるコードの解説
test/bench/go1/jsondata_test.go
の変更
var jsonbz2_base64 = "..." + "..."
からvar jsonbz2_base64 = []byte(
...)
へ:- 最も重要な変更は、
jsonbz2_base64
変数の定義方法です。 - 変更前は、Base64エンコードされた巨大な文字列が、複数のダブルクォート文字列リテラルを
+
演算子で連結して構成されていました。Goコンパイラは、このような文字列結合をコンパイル時に評価し、最終的な文字列を生成します。このプロセスは、特に文字列が非常に長い場合、多くのメモリとCPU時間を消費します。コンパイラは、中間的な文字列オブジェクトを多数生成し、それらをヒープまたはスタックに一時的に割り当てる必要があります。これが、コミットメッセージに示されているような高いメモリ使用量(540720maxresident)k
)とスタック使用量の原因でした。 - 変更後では、
jsonbz2_base64
は[]byte
型(バイトスライス)として宣言され、その値は単一のraw文字列リテラル(バッククォート`
で囲まれた文字列)で直接初期化されています。raw文字列リテラルは、その内部の文字が文字通りに解釈されるため、エスケープシーケンスの処理や文字列結合の評価が不要です。これにより、コンパイラは文字列の内容を直接バイトスライスとして効率的に処理でき、コンパイル時のメモリ割り当てと処理負荷が大幅に削減されます。コミットメッセージの289936maxresident)k
という結果は、この最適化の効果を明確に示しています。また、[]byte
型にすることで、文字列からバイトスライスへの変換コストも削減されます。
- 最も重要な変更は、
test/bench/go1/json_test.go
の変更
import "strings"
の削除とimport "bytes"
の追加:jsondata_test.go
でjsonbz2_base64
が[]byte
型に変更されたため、json_test.go
でこの変数を使用する際に、文字列(string
)を扱うstrings
パッケージではなく、バイトスライス([]byte
)を扱うbytes
パッケージが必要になりました。
r = strings.NewReader(jsonbz2_base64)
からr = bytes.NewReader(bytes.Replace(jsonbz2_base64, []byte{'\n'}, nil, -1))
へ:makeJsonBytes
関数は、jsonbz2_base64
からio.Reader
を作成し、Base64デコードとBzip2解凍を行う部分です。- 変更前は、
strings.NewReader
を使用して文字列からio.Reader
を作成していました。 - 変更後では、
jsonbz2_base64
が[]byte
型になったため、bytes.NewReader
を使用するように変更されました。 - さらに重要なのは、
bytes.Replace(jsonbz2_base64, []byte{'\n'}, nil, -1)
が追加された点です。raw文字列リテラルは、ソースコード中の改行をそのまま含みます。しかし、Base64エンコードされたデータは通常、改行を含まない単一の連続した文字列として扱われるべきです。このbytes.Replace
の呼び出しは、jsonbz2_base64
内のすべての改行文字(\n
)を削除し、クリーンなBase64データを提供するために行われています。これにより、Base64デコードが正しく機能するようになります。
これらの変更は、Goコンパイラの内部的な文字列処理の効率を改善し、特に大きな埋め込みデータを扱う際のコンパイルパフォーマンスを向上させるための、実践的かつ効果的な最適化を示しています。
関連リンク
- Go Issue 4970 (コミットメッセージに記載されているIssue番号ですが、直接のリンクは提供されていません。GoのIssueトラッカーで検索することで見つかる可能性があります。)
- Go言語の仕様: String literals
- Go言語の
bytes
パッケージ - Go言語の
strings
パッケージ
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のIssueトラッカー (Issue 4970に関する詳細情報)
- Go言語のコンパイラ最適化に関する一般的な情報源