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

[インデックス 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種類の文字列リテラルがあります。

  1. 解釈済み文字列リテラル (Interpreted String Literals):

    • ダブルクォート"で囲まれた文字列です。
    • バックスラッシュ\によるエスケープシーケンス(例: \n\t\")が解釈されます。
    • 複数行にわたる文字列を記述する場合、各行の終わりに\nを含める必要があります。
  2. 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.NewReaderstrings.Replace(これは元のコードには明示的にありませんが、stringsパッケージがインポートされていたことから、文字列操作が行われていたと推測されます)の使用が削除され、代わりにbytes.NewReaderbytes.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.gojsonbz2_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言語の公式ドキュメント
  • Go言語のIssueトラッカー (Issue 4970に関する詳細情報)
  • Go言語のコンパイラ最適化に関する一般的な情報源