[インデックス 18179] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における[]byte("string")
形式のバイトスライス初期化のメモリ使用量を大幅に削減することを目的としています。具体的には、コンパイラがこの形式の初期化を処理する際に、中間表現の最適化によって発生していたメモリフットプリントの増大を抑制し、約100分の1に削減します。
コミット
commit d227d680ece216603c31e36ee995b814259325dc
Author: Russ Cox <rsc@golang.org>
Date: Mon Jan 6 20:43:44 2014 -0500
cmd/gc: use 100x less memory for []byte("string")
[]byte("string") was simplifying to
[]byte{0: 0x73, 1: 0x74, 2: 0x72, 3: 0x69, 4: 0x6e, 5: 0x67},
but that latter form takes up much more memory in the compiler.
Preserve the string form and recognize it to turn global variables
initialized this way into linker-initialized data.
Reduces the compiler memory footprint for a large []byte initialized
this way from approximately 10 kB/B to under 100 B/B.
See also issue 6643.
R=golang-codereviews, r, iant, oleku.konko, dave, gobot, bradfitz
CC=golang-codereviews
https://golang.org/cl/15930045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d227d680ece216603c31e36ee995b814259325dc
元コミット内容
cmd/gc: use 100x less memory for []byte("string")
[]byte("string")
は、以前は []byte{0: 0x73, 1: 0x74, 2: 0x72, 3: 0x69, 4: 0x6e, 5: 0x67}
のように簡略化されていました。しかし、この後者の形式はコンパイラ内でより多くのメモリを消費していました。このコミットでは、文字列形式を保持し、この方法で初期化されたグローバル変数をリンカ初期化データに変換するために認識するように変更します。
これにより、この方法で初期化された大きな []byte
のコンパイラのメモリフットプリントが、約 10 kB/B から 100 B/B 未満に削減されます。
関連するIssue 6643も参照してください。
変更の背景
Goコンパイラ(cmd/gc
)は、ソースコードを機械語に変換する過程で、プログラムの様々な要素を内部的なデータ構造として表現します。特に、[]byte("string literal")
のようなバイトスライスの初期化は、コンパイラにとってメモリ消費の大きな要因となっていました。
以前のGoコンパイラでは、[]byte("string")
のような構文が検出されると、これを []byte{0: 's', 1: 't', ...}
のような個々のバイト要素を持つ配列リテラルに変換していました。この変換は、コンパイラの内部表現において、文字列リテラルを直接参照するのではなく、各バイトを個別のノードとして表現することを意味します。
この「個々のバイト要素を持つ配列リテラル」形式は、特に長い文字列の場合に、コンパイラのメモリ使用量を著しく増加させる問題がありました。例えば、1KBの文字列を []byte
で初期化しようとすると、コンパイラ内部ではその1KBのデータだけでなく、各バイトに対応するノードやそれらを管理するためのオーバーヘッドが加わり、最終的に10KB以上のメモリを消費するような状況が発生していました。これは、コンパイル時のメモリフットプリントの増大、ひいてはコンパイル時間の増加や、大規模なプロジェクトでのコンパイルの困難さにつながる可能性がありました。
この問題は、GoのIssue 6643として報告されており、コンパイラのメモリ効率の改善が求められていました。このコミットは、その問題に対する直接的な解決策として提案されました。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラおよびGo言語の基本的な概念を理解しておく必要があります。
-
Goコンパイラ (
cmd/gc
):- Go言語の公式コンパイラであり、Goソースコードをバイナリ実行ファイルに変換します。
- コンパイルプロセスには、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成などの段階があります。
- コンパイラは、ソースコードの要素(変数、関数、リテラルなど)を内部的なデータ構造(ASTノードなど)として表現し、これらを操作しながら処理を進めます。
-
AST (Abstract Syntax Tree):
- ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、これを使って型チェックや最適化を行います。
- 各ノードは、変数、演算子、リテラルなどのプログラム要素に対応します。
-
[]byte("string")
:- Go言語において、文字列リテラルをバイトスライスに変換する構文です。
- 例:
[]byte("hello")
は、'h', 'e', 'l', 'l', 'o'
というバイト列を含むバイトスライスを生成します。 - Goの文字列はUTF-8エンコードされたバイト列であり、
[]byte
への変換は、そのバイト列を直接コピーして新しいバイトスライスを作成します。
-
グローバル変数と初期化:
- Goのグローバル変数は、プログラムの実行開始時に初期化されます。
- コンパイル時に値が確定しているグローバル変数は、実行ファイルのデータセクションに直接埋め込まれる(リンカ初期化データとなる)ことで、実行時の初期化オーバーヘッドを削減できます。
-
メモリフットプリント:
- プログラムやプロセスが実行中に使用するメモリの総量。コンパイラの場合、コンパイル中に内部データ構造を保持するために必要なメモリ量を指します。
- メモリフットプリントが大きいと、コンパイル時間が長くなったり、メモリ不足エラーが発生したりする可能性があります。
-
Issue 6643:
- GoプロジェクトのIssueトラッカーで報告されたバグや改善提案の識別子です。このコミットは、Issue 6643で議論された問題(
[]byte("string")
のコンパイル時メモリ消費問題)を解決するために作成されました。
- GoプロジェクトのIssueトラッカーで報告されたバグや改善提案の識別子です。このコミットは、Issue 6643で議論された問題(
技術的詳細
このコミットの核心は、Goコンパイラが []byte("string")
形式の初期化を処理する方法を変更し、コンパイル時のメモリ効率を向上させる点にあります。
以前のコンパイラでは、[]byte("string")
という表現は、内部的に OARRAYLIT
(配列リテラル) のASTノードに変換され、その要素は個々のバイトリテラルとして表現されていました。例えば、[]byte("abc")
は、概念的には []byte{0:'a', 1:'b', 2:'c'}
のようなAST構造に展開されていました。この展開は、文字列の長さが長くなるにつれて、コンパイラが保持しなければならないASTノードの数が線形に増加することを意味します。各バイトリテラルがそれぞれASTノードとして表現されるため、ノード自体のメモリ消費と、それらを連結するためのポインタなどのオーバーヘッドが加わり、結果として元の文字列データサイズに対して非常に大きなメモリフットプリントが発生していました(コミットメッセージにある「10 kB/B」という表現は、1バイトの文字列データに対してコンパイラが10KBのメモリを消費していたことを示唆しています)。
このコミットでは、この挙動を変更し、[]byte("string")
のような初期化を、コンパイラがより効率的に扱える新しい内部表現 OSTRARRAYBYTE
として導入します。
主な変更点は以下の通りです。
-
OSTRARRAYBYTE
ノードの導入:src/cmd/gc/typecheck.c
において、OSTRARRAYBYTE
という新しいASTノードタイプが導入されました。これは、文字列リテラルからバイトスライスを生成する特定のケースを表現します。- 重要なのは、この
OSTRARRAYBYTE
ノードが、以前のように個々のバイトリテラルに展開されるstringtoarraylit
関数を使用しないように変更されたことです。これにより、コンパイラは文字列リテラルそのものを参照する形でこの初期化を保持できるようになり、中間表現のメモリ消費を大幅に削減します。
-
slicebytes
関数の追加:src/cmd/gc/obj.c
にslicebytes
という新しい関数が追加されました。この関数は、OSTRARRAYBYTE
ノードによって表現されるバイトスライス初期化を、最終的なオブジェクトファイルに効率的に書き出す役割を担います。slicebytes
は、与えられた文字列データ (char *s
,int len
) を、新しいシンボル(.gobytes.N
のような名前)としてデータセクションに書き込みます。これは、リンカによって初期化される静的なデータとして扱われます。- その後、バイトスライスが初期化されるグローバル変数(
nam
ノード)に対して、この新しく作成されたデータシンボルへのポインタと、スライスの長さ(len
)と容量(len
)を書き込みます。これにより、実行時に動的にバイトスライスを構築するのではなく、コンパイル時にデータが配置され、リンカによってそのアドレスが解決されるようになります。
-
staticassign
の変更:src/cmd/gc/sinit.c
のstaticassign
関数は、グローバル変数の静的初期化を処理する部分です。- この関数に
OSTRARRAYBYTE
ケースが追加され、もし初期化される変数がグローバル変数(PEXTERN
)であり、初期化値が文字列リテラル(OLITERAL
)である場合、新しく追加されたslicebytes
関数を呼び出すように変更されました。 - これにより、
[]byte("string")
で初期化されるグローバル変数は、コンパイル時にデータセクションに直接文字列データが埋め込まれ、そのポインタと長さが変数に設定されるようになります。これは、実行時の初期化コストを削減し、コンパイラが中間表現として個々のバイトノードを保持する必要がなくなるため、メモリ効率が向上します。
この一連の変更により、コンパイラは []byte("string")
のような初期化を、よりコンパクトな形式で内部的に表現し、最終的なバイナリではリンカによって初期化される静的データとして扱うことができるようになりました。結果として、コンパイル時のメモリフットプリントが劇的に削減され、特に大きなバイトスライスを初期化する際のコンパイラのパフォーマンスが向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下の4つのファイルにわたります。
-
src/cmd/gc/go.h
:slicebytes
関数のプロトタイプ宣言が追加されました。
--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -1263,6 +1263,7 @@ int duintptr(Sym *s, int off, uint64 v); int dsname(Sym *s, int off, char *dat, int ndat); void dumpobj(void); Sym* stringsym(char*, int); +void slicebytes(Node*, char*, int); LSym* linksym(Sym*); /*
-
src/cmd/gc/obj.c
:slicebytes
関数の実装が追加されました。この関数は、文字列データをオブジェクトファイルのデータセクションに書き込み、そのシンボル情報を設定します。
--- a/src/cmd/gc/obj.c +++ b/src/cmd/gc/obj.c @@ -253,3 +253,31 @@ stringsym(char *s, int len) return sym; } + +void +slicebytes(Node *nam, char *s, int len) +{ + int off, n, m; + static int gen; + Sym *sym; + + snprint(namebuf, sizeof(namebuf), ".gobytes.%d", ++gen); + sym = pkglookup(namebuf, localpkg); + sym->def = newname(sym); + + off = 0; + for(n=0; n<len; n+=m) { + m = 8; + if(m > len-n) + m = len-n; + off = dsname(sym, off, s+n, m); + } + ggloblsym(sym, off, 0, 0); + + if(nam->op != ONAME) + fatal("slicebytes %N", nam); + off = nam->xoffset; + off = dsymptr(nam->sym, off, sym, 0); + off = duintxx(nam->sym, off, len, widthint); + duintxx(nam->sym, off, len, widthint); +}
-
src/cmd/gc/sinit.c
:staticassign
関数にOSTRARRAYBYTE
のケースが追加され、グローバル変数の[]byte("string")
初期化をslicebytes
関数で処理するように変更されました。
--- a/src/cmd/gc/sinit.c +++ b/src/cmd/gc/sinit.c @@ -378,6 +378,7 @@ staticassign(Node *l, Node *r, NodeList **out) InitPlan *p; InitEntry *e; int i; + Strlit *sval; switch(r->op) { default: @@ -426,6 +427,14 @@ staticassign(Node *l, Node *r, NodeList **out) } break; + case OSTRARRAYBYTE: + if(l->class == PEXTERN && r->left->op == OLITERAL) { + sval = r->left->val.u.sval; + slicebytes(l, sval->s, sval->len); + return 1; + } + break; + case OARRAYLIT: initplan(r); if(isslice(r->type)) {
-
src/cmd/gc/typecheck.c
:OSTRARRAYBYTE
ノードがstringtoarraylit
関数を使用しないように変更されました。これにより、コンパイラは文字列リテラルを個々のバイトリテラルに展開する代わりに、よりコンパクトな形式で保持します。
--- a/src/cmd/gc/typecheck.c +++ b/src/cmd/gc/typecheck.c @@ -1406,6 +1406,9 @@ reswitch: } break; case OSTRARRAYBYTE: + // do not use stringtoarraylit. + // generated code and compiler memory footprint is better without it. + break; case OSTRARRAYRUNE: if(n->left->op == OLITERAL) stringtoarraylit(&n);
コアとなるコードの解説
src/cmd/gc/go.h
の変更
slicebytes
関数の前方宣言が追加されています。これは、obj.c
で定義されるこの関数が、他のコンパイラコンポーネント(特にsinit.c
)から呼び出されることを可能にします。
src/cmd/gc/obj.c
の slicebytes
関数
この関数は、[]byte("string")
の初期化において、文字列リテラルを最終的なオブジェクトファイルのデータセクションに効率的に埋め込むための中心的なロジックを含んでいます。
-
シンボルの生成:
snprint(namebuf, sizeof(namebuf), ".gobytes.%d", ++gen);
で、.gobytes.N
のような一意のシンボル名を生成します。これは、この文字列データがリンカによって参照されるための名前です。sym = pkglookup(namebuf, localpkg);
で、このシンボルを現在のパッケージのシンボルテーブルに登録します。sym->def = newname(sym);
で、シンボルに対応する名前ノードを作成します。
-
データセクションへの書き込み:
for(n=0; n<len; n+=m)
ループで、文字列データs
を8バイトずつ(m=8
)に分割し、dsname(sym, off, s+n, m)
を使って、生成したシンボルsym
に関連付けられたデータセクションに書き込んでいきます。dsname
は、指定されたシンボルにデータを追加し、現在のオフセットを返します。ggloblsym(sym, off, 0, 0);
は、このシンボルsym
がグローバルシンボルであり、そのサイズがoff
であることをコンパイラに伝えます。これにより、リンカはこのシンボルを外部から参照できるようになります。
-
バイトスライス構造の初期化:
nam
は、初期化されるバイトスライス変数(グローバル変数)のASTノードです。off = nam->xoffset;
で、グローバル変数nam
のデータセクション内でのオフセットを取得します。off = dsymptr(nam->sym, off, sym, 0);
は、nam
のシンボルに、先ほどデータセクションに書き込んだ文字列データsym
へのポインタを書き込みます。Goのバイトスライスは内部的にポインタ、長さ、容量の3つのフィールドを持つ構造体として表現されるため、最初のフィールドにデータへのポインタが設定されます。off = duintxx(nam->sym, off, len, widthint);
とduintxx(nam->sym, off, len, widthint);
は、それぞれバイトスライスの長さ(len)と容量(len)を、nam
のシンボルに書き込みます。これにより、グローバル変数がコンパイル時に完全に初期化されたバイトスライスとして定義されます。
src/cmd/gc/sinit.c
の staticassign
関数
この関数は、静的に初期化される変数(特にグローバル変数)の処理を担当します。
OSTRARRAYBYTE
ケースの追加:case OSTRARRAYBYTE:
ブロックが追加されました。これは、ASTノードの操作がOSTRARRAYBYTE
タイプである場合に実行されます。if(l->class == PEXTERN && r->left->op == OLITERAL)
: この条件は、初期化される左辺l
がグローバル変数(PEXTERN
)であり、右辺r
の左の子ノードが文字列リテラル(OLITERAL
)であることを確認します。これはまさにglobalVar := []byte("string")
のようなケースに該当します。sval = r->left->val.u.sval;
: 文字列リテラルの値を取得します。slicebytes(l, sval->s, sval->len);
: ここで、新しく追加されたslicebytes
関数が呼び出されます。これにより、文字列データがオブジェクトファイルのデータセクションに直接埋め込まれ、グローバル変数がそのデータへのポインタと長さで初期化されます。return 1;
: 処理が完了したことを示します。
src/cmd/gc/typecheck.c
の変更
このファイルは、Goの型チェックとASTノードの変換を担当します。
OSTRARRAYBYTE
のstringtoarraylit
使用停止:case OSTRARRAYBYTE:
ブロックに// do not use stringtoarraylit.
というコメントとbreak;
が追加されました。- 以前は、
OSTRARRAYBYTE
はstringtoarraylit
関数を呼び出して、文字列リテラルを個々のバイト要素を持つOARRAYLIT
ノードに変換していました。この変換がコンパイラのメモリを大量に消費する原因でした。 - この変更により、
OSTRARRAYBYTE
ノードはstringtoarraylit
による展開を行わず、文字列リテラルそのものを参照する形で保持されるようになります。これにより、ASTのノード数が削減され、コンパイラのメモリフットプリントが大幅に改善されます。
これらの変更が連携することで、[]byte("string")
のような初期化が、コンパイル時に効率的に処理され、最終的なバイナリではリンカによって初期化される静的データとして表現されるようになり、コンパイラのメモリ使用量が劇的に削減されました。
関連リンク
- Go Issue 6643:
cmd/gc: []byte("string") uses too much memory
- https://github.com/golang/go/issues/6643 - Go CL 15930045:
cmd/gc: use 100x less memory for []byte("string")
- https://golang.org/cl/15930045
参考にした情報源リンク
- Go Issue 6643の議論
- Go CL 15930045のコードレビューとコメント
- Goコンパイラのソースコード (
src/cmd/gc
ディレクトリ内の関連ファイル) - Go言語のバイトスライスと文字列に関する公式ドキュメント
- コンパイラの内部構造に関する一般的な知識