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

[インデックス 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言語の基本的な概念を理解しておく必要があります。

  1. Goコンパイラ (cmd/gc):

    • Go言語の公式コンパイラであり、Goソースコードをバイナリ実行ファイルに変換します。
    • コンパイルプロセスには、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成などの段階があります。
    • コンパイラは、ソースコードの要素(変数、関数、リテラルなど)を内部的なデータ構造(ASTノードなど)として表現し、これらを操作しながら処理を進めます。
  2. AST (Abstract Syntax Tree):

    • ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、これを使って型チェックや最適化を行います。
    • 各ノードは、変数、演算子、リテラルなどのプログラム要素に対応します。
  3. []byte("string"):

    • Go言語において、文字列リテラルをバイトスライスに変換する構文です。
    • 例: []byte("hello") は、'h', 'e', 'l', 'l', 'o' というバイト列を含むバイトスライスを生成します。
    • Goの文字列はUTF-8エンコードされたバイト列であり、[]byte への変換は、そのバイト列を直接コピーして新しいバイトスライスを作成します。
  4. グローバル変数と初期化:

    • Goのグローバル変数は、プログラムの実行開始時に初期化されます。
    • コンパイル時に値が確定しているグローバル変数は、実行ファイルのデータセクションに直接埋め込まれる(リンカ初期化データとなる)ことで、実行時の初期化オーバーヘッドを削減できます。
  5. メモリフットプリント:

    • プログラムやプロセスが実行中に使用するメモリの総量。コンパイラの場合、コンパイル中に内部データ構造を保持するために必要なメモリ量を指します。
    • メモリフットプリントが大きいと、コンパイル時間が長くなったり、メモリ不足エラーが発生したりする可能性があります。
  6. Issue 6643:

    • GoプロジェクトのIssueトラッカーで報告されたバグや改善提案の識別子です。このコミットは、Issue 6643で議論された問題([]byte("string") のコンパイル時メモリ消費問題)を解決するために作成されました。

技術的詳細

このコミットの核心は、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 として導入します。

主な変更点は以下の通りです。

  1. OSTRARRAYBYTE ノードの導入:

    • src/cmd/gc/typecheck.c において、OSTRARRAYBYTE という新しいASTノードタイプが導入されました。これは、文字列リテラルからバイトスライスを生成する特定のケースを表現します。
    • 重要なのは、この OSTRARRAYBYTE ノードが、以前のように個々のバイトリテラルに展開される stringtoarraylit 関数を使用しないように変更されたことです。これにより、コンパイラは文字列リテラルそのものを参照する形でこの初期化を保持できるようになり、中間表現のメモリ消費を大幅に削減します。
  2. slicebytes 関数の追加:

    • src/cmd/gc/obj.cslicebytes という新しい関数が追加されました。この関数は、OSTRARRAYBYTE ノードによって表現されるバイトスライス初期化を、最終的なオブジェクトファイルに効率的に書き出す役割を担います。
    • slicebytes は、与えられた文字列データ (char *s, int len) を、新しいシンボル(.gobytes.N のような名前)としてデータセクションに書き込みます。これは、リンカによって初期化される静的なデータとして扱われます。
    • その後、バイトスライスが初期化されるグローバル変数(nam ノード)に対して、この新しく作成されたデータシンボルへのポインタと、スライスの長さ(len)と容量(len)を書き込みます。これにより、実行時に動的にバイトスライスを構築するのではなく、コンパイル時にデータが配置され、リンカによってそのアドレスが解決されるようになります。
  3. staticassign の変更:

    • src/cmd/gc/sinit.cstaticassign 関数は、グローバル変数の静的初期化を処理する部分です。
    • この関数に OSTRARRAYBYTE ケースが追加され、もし初期化される変数がグローバル変数(PEXTERN)であり、初期化値が文字列リテラル(OLITERAL)である場合、新しく追加された slicebytes 関数を呼び出すように変更されました。
    • これにより、[]byte("string") で初期化されるグローバル変数は、コンパイル時にデータセクションに直接文字列データが埋め込まれ、そのポインタと長さが変数に設定されるようになります。これは、実行時の初期化コストを削減し、コンパイラが中間表現として個々のバイトノードを保持する必要がなくなるため、メモリ効率が向上します。

この一連の変更により、コンパイラは []byte("string") のような初期化を、よりコンパクトな形式で内部的に表現し、最終的なバイナリではリンカによって初期化される静的データとして扱うことができるようになりました。結果として、コンパイル時のメモリフットプリントが劇的に削減され、特に大きなバイトスライスを初期化する際のコンパイラのパフォーマンスが向上しました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下の4つのファイルにわたります。

  1. 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*);
     
     /*
    
  2. 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);
    +}
    
  3. 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)) {
    
  4. 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.cslicebytes 関数

この関数は、[]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.cstaticassign 関数

この関数は、静的に初期化される変数(特にグローバル変数)の処理を担当します。

  • 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ノードの変換を担当します。

  • OSTRARRAYBYTEstringtoarraylit 使用停止:
    • case OSTRARRAYBYTE: ブロックに // do not use stringtoarraylit. というコメントと break; が追加されました。
    • 以前は、OSTRARRAYBYTEstringtoarraylit 関数を呼び出して、文字列リテラルを個々のバイト要素を持つ OARRAYLIT ノードに変換していました。この変換がコンパイラのメモリを大量に消費する原因でした。
    • この変更により、OSTRARRAYBYTE ノードは stringtoarraylit による展開を行わず、文字列リテラルそのものを参照する形で保持されるようになります。これにより、ASTのノード数が削減され、コンパイラのメモリフットプリントが大幅に改善されます。

これらの変更が連携することで、[]byte("string") のような初期化が、コンパイル時に効率的に処理され、最終的なバイナリではリンカによって初期化される静的データとして表現されるようになり、コンパイラのメモリ使用量が劇的に削減されました。

関連リンク

参考にした情報源リンク

  • Go Issue 6643の議論
  • Go CL 15930045のコードレビューとコメント
  • Goコンパイラのソースコード (src/cmd/gc ディレクトリ内の関連ファイル)
  • Go言語のバイトスライスと文字列に関する公式ドキュメント
  • コンパイラの内部構造に関する一般的な知識