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

[インデックス 15101] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるインライン化処理中に発生する、変数の重複したヒープアロケーション(メモリ割り当て)を回避するための修正です。具体的には、関数がインライン化される際に、本来一度で済むはずの変数のメモリ確保が二重に行われてしまうバグ(Issue #4667)を解決します。これにより、生成されるバイナリの効率が向上し、不要なメモリ割り当てが削減されます。

コミット

commit a72f9f46a2aacb522eb5da6bea9ea9a02a1aaea8
Author: Russ Cox <rsc@golang.org>
Date:   Sat Feb 2 23:17:25 2013 -0500

    cmd/gc: avoid duplicate allocation during inlining
    
    Fixes #4667.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7275046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a72f9f46a2aacb522eb5da6bea9ea9a02a1aaea8

元コミット内容

cmd/gc: avoid duplicate allocation during inlining Fixes #4667.

このコミットは、Goコンパイラのcmd/gc部分において、関数のインライン化中に発生する重複したメモリ割り当てを回避することを目的としています。これはIssue #4667を修正するものです。

変更の背景

Goコンパイラは、プログラムの実行効率を向上させるために様々な最適化を行います。その一つが「インライン化」です。インライン化とは、関数呼び出しをその関数の本体のコードで直接置き換えることで、関数呼び出しのオーバーヘッドを削減する最適化手法です。

しかし、このインライン化の過程で、特定の条件下で変数のメモリ割り当てが重複して行われるというバグが存在しました。特に、関数内で宣言されたローカル変数が「エスケープ」する場合に問題が発生していました。エスケープとは、ローカル変数のアドレスが関数のスコープ外に渡される(例えば、グローバル変数に代入される、戻り値として返されるなど)ことで、その変数がスタックではなくヒープに割り当てられる必要があるとコンパイラが判断する現象です。

Issue #4667は、このようなエスケープする変数がインライン化される際に、コンパイラがその変数をヒープに二重に割り当ててしまうという問題点を指摘していました。これにより、不要なメモリ割り当てが発生し、プログラムのメモリ使用量が増加したり、ガベージコレクションの頻度が増えたりする可能性がありました。このコミットは、この非効率な動作を修正し、インライン化されたコードがより効率的にメモリを使用するようにすることを目的としています。

前提知識の解説

このコミットの理解には、以下のGoコンパイラおよびプログラミング言語の基本的な概念が役立ちます。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。cmd/gcは、Goのツールチェインの一部として提供されています。
  • インライン化 (Inlining): コンパイラ最適化の一種。関数呼び出しを、その関数の本体のコードで直接置き換えることで、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数のプッシュ、戻りアドレスの保存など)を削減し、実行速度を向上させます。ただし、コードサイズが増加する可能性があります。
  • ヒープアロケーション (Heap Allocation) とスタックアロケーション (Stack Allocation):
    • スタック (Stack): 関数呼び出しごとに自動的に割り当てられるメモリ領域。ローカル変数や関数引数など、関数の実行中にのみ存在するデータが格納されます。高速で、自動的に管理されます。
    • ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てるための領域。関数のスコープを超えて存続する必要があるデータ(例: グローバル変数、ポインタによって参照されるデータ、newmakeで作成されるデータ)が格納されます。スタックに比べて割り当てが遅く、ガベージコレクションによる管理が必要です。
  • エスケープ解析 (Escape Analysis): Goコンパイラが行う最適化の一つ。変数がスタックに割り当てられるべきか、それともヒープに「エスケープ」して割り当てられるべきかを決定します。変数のアドレスが関数の外に渡される場合、その変数はヒープにエスケープすると判断されます。
  • 抽象構文木 (AST: Abstract Syntax Tree): コンパイラがソースコードを解析して生成する、プログラムの構造を木構造で表現したデータ構造。コンパイラはASTを操作して最適化やコード生成を行います。
  • Node: Goコンパイラの内部でASTの各要素を表すデータ構造。
  • ODCL (Operation Declaration): ASTノードの一種で、変数の宣言を表します。
  • ninit: Node構造体の一部で、そのノードに関連する初期化ステートメントのリストを保持します。例えば、変数の宣言ノードには、その変数の初期化式がninitに含まれることがあります。
  • inlvar: インライン化に関連する変数を示すフィールド。
  • PAUTO: Goコンパイラの内部で変数の「クラス」を示すフラグの一つで、自動変数(スタックに割り当てられるローカル変数)を意味します。
  • PHEAP: 変数のクラスを示すフラグの一つで、ヒープに割り当てられる変数を意味します。
  • testing.AllocsPerRun: Goの標準ライブラリtestingパッケージに含まれる関数で、指定された関数が実行される際に発生するヒープアロケーションの平均回数を測定するために使用されます。これにより、メモリ効率のボトルネックを特定できます。

技術的詳細

このコミットの核心は、src/cmd/gc/inl.cファイル内のmkinlcall1関数における変更です。この関数は、インライン化された関数呼び出しのASTノードを構築する際に重要な役割を果たします。

以前のコードでは、インライン化される関数の引数や戻り値、あるいはエスケープするローカル変数など、インライン化によって生成される可能性のある変数(ll->n->inlvar)に対して、無条件にODCL(宣言)ノードをninitリストに追加していました。ninitに追加されたODCLノードは、後続のコード生成フェーズで実際のメモリ割り当て(スタックまたはヒープ)をトリガーします。

問題は、エスケープ解析によって既にヒープに割り当てられることが決定されている変数や、その他の理由で既に適切に処理されている変数に対しても、このODCLノードが追加されてしまうことでした。これにより、同じ変数に対して二重のメモリ割り当て命令が生成され、重複したヒープアロケーションが発生していました。

このコミットでは、ODCLノードをninitに追加する前に、以下の条件を追加しました。

if ((ll->n->class&~PHEAP) != PAUTO)

この条件は、ll->n->class(変数のクラスを示すフラグ)からPHEAPフラグを除外した結果がPAUTO(スタック変数)ではない場合にのみ、ODCLノードを追加するようにします。

  • ll->n->class & ~PHEAP: これは、変数のクラスからPHEAPビットをマスクアウト(取り除く)する操作です。つまり、変数がヒープに割り当てられるかどうかに関わらず、その変数の基本的な種類(例: 自動変数、引数など)を調べます。
  • != PAUTO: マスクアウトした結果がPAUTOではない、つまりスタック変数ではない場合に真となります。

この条件の意図は以下の通りです。

  1. PAUTO変数(スタック変数)の場合: (ll->n->class & ~PHEAP)の結果がPAUTOになるため、条件は偽となり、ODCLノードは追加されません。スタック変数は通常、関数のプロローグでまとめて割り当てられるため、個別のODCLは不要です。
  2. PHEAP変数(ヒープ変数)の場合: ll->n->classPHEAPが含まれていても、&~PHEAPによってそのビットが取り除かれます。その結果がPAUTOではない場合(例えば、PHEAP以外の他のクラスフラグが立っている場合)、条件は真となり、ODCLノードが追加されます。これは、ヒープ変数の宣言が必要なケースに対応します。
  3. その他の変数クラスの場合: PAUTOでもPHEAPでもない、他の特殊な変数クラスの場合も、この条件によって適切にODCLノードの追加が制御されます。

この変更により、既にヒープに割り当てられることが決定している変数や、スタックに割り当てられるべき変数に対して、インライン化の過程で不要なODCLノードが生成されることがなくなり、結果として重複したメモリ割り当てが回避されます。

新しいテストケースtest/fixedbugs/issue4667.goは、この問題を具体的に再現し、修正が正しく機能することを確認します。F()関数内のローカル変数xは、そのアドレスがグローバル変数globlに代入されるため、エスケープ解析によってヒープに割り当てられると判断されます。testing.AllocsPerRunを使用して、F()およびG()F()を呼び出す関数)が実行される際のヒープアロケーション数を測定し、期待される1回のアロケーションに収まっていることを検証します。

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

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

  1. src/cmd/gc/gen.c:

    --- a/src/cmd/gc/gen.c
    +++ b/src/cmd/gc/gen.c
    @@ -266,6 +266,8 @@ gen(Node *n)
     	Label *lab;
     	int32 wasregalloc;
     
    +//dump("gen", n);
    +
     	lno = setlineno(n);
     	wasregalloc = anyregalloc();
    

    この変更は、デバッグ用のコメントアウトされた行を追加したもので、機能的な変更ではありません。

  2. src/cmd/gc/inl.c:

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -553,6 +553,8 @@ mkinlcall1(Node **np, Node *fn, int isddd)
     
     	ninit = n->ninit;
     
    +//dumplist("ninit pre", ninit);
    +
     	if (fn->defn) // local function
     		dcl = fn->defn->dcl;
     	else // imported function
    @@ -566,7 +568,8 @@ mkinlcall1(Node **np, Node *fn, int isddd)
     		// Typecheck because inlvar is not necessarily a function parameter.
     		typecheck(&ll->n->inlvar, Erv);
    -		ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));  // otherwise gen won't emit the allocations for heapallocs
    +		if ((ll->n->class&~PHEAP) != PAUTO)
    +			ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));  // otherwise gen won't emit the allocations for heapallocs
     		if (ll->n->class == PPARAMOUT)  // we rely on the order being correct here
     			inlretvars = list(inlretvars, ll->n->inlvar);
     	}
    @@ -733,6 +736,7 @@ mkinlcall1(Node **np, Node *fn, int isddd)
     	body = list(body, nod(OLABEL, inlretlabel, N));
     
     	typechecklist(body, Etop);
    +//dumplist("ninit post", ninit);
     
     	call = nod(OINLCALL, N, N);
     	call->ninit = ninit;
    @@ -742,6 +746,7 @@ mkinlcall1(Node **np, Node *fn, int isddd)
     	call->typecheck = 1;
     
     	setlno(call, n->lineno);
    +//dumplist("call body", body);
     
     	*np = call;
    

    このファイルが主要な修正箇所です。mkinlcall1関数内で、ninitリストにODCLノードを追加する条件が変更されています。

  3. test/fixedbugs/issue4667.go:

    --- /dev/null
    +++ b/test/fixedbugs/issue4667.go
    @@ -0,0 +1,37 @@
    +// run
    +
    +// Copyright 2013 The Go Authors.  All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +package main
    +
    +import (
    +	"fmt"
    +	"os"
    +	"testing"
    +)
    +
    +var globl *int
    +
    +func G() {
    +	F()
    +}
    +
    +func F() {
    +	var x int
    +	globl = &x
    +}
    +
    +func main() {
    +	nf := testing.AllocsPerRun(100, F)
    +	ng := testing.AllocsPerRun(100, G)
    +	if int(nf) != 1 {
    +		fmt.Printf("AllocsPerRun(100, F) = %v, want 1\n", nf)
    +		os.Exit(1)
    +	}
    +	if int(ng) != 1 {
    +		fmt.Printf("AllocsPerRun(100, G) = %v, want 1\n", ng)
    +		os.Exit(1)
    +	}
    +}
    

    このファイルは、バグを再現し、修正が正しく機能することを確認するための新しいテストケースです。

コアとなるコードの解説

src/cmd/gc/inl.cの変更がこのコミットの核心です。

-			ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));  // otherwise gen won't emit the allocations for heapallocs
+			if ((ll->n->class&~PHEAP) != PAUTO)
+				ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));  // otherwise gen won't emit the allocations for heapallocs
  • 変更前: ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));

    • この行は、インライン化された変数(ll->n->inlvar)の宣言ノード(ODCL)を、現在のノードの初期化リスト(ninit)に無条件に追加していました。
    • コメントにあるように、これはgenフェーズでヒープアロケーションのための命令が生成されるようにするためのものでした。しかし、これが重複アロケーションの原因となっていました。
  • 変更後: if ((ll->n->class&~PHEAP) != PAUTO) という条件が追加されました。

    • この条件は、ll->n->inlvarがスタック変数(PAUTO)ではない場合にのみ、ODCLノードをninitに追加するようにします。
    • ll->n->classは、変数のストレージクラス(例: スタック、ヒープ、グローバルなど)を示すビットフラグです。
    • ~PHEAPは、PHEAPフラグのビットを反転させたものです。&~PHEAPとすることで、ll->n->classからPHEAPフラグが立っているビットをクリアします。これにより、変数がヒープに割り当てられるかどうかに関わらず、その変数の基本的な種類(例: 自動変数、引数など)を調べることができます。
    • != PAUTOは、PHEAPフラグを取り除いた後の変数のクラスがPAUTO(スタック変数)ではないことを意味します。
    • この条件が真となるケース:
      • 変数がヒープに割り当てられる場合(PHEAPフラグが立っている場合)。この場合、&~PHEAPによってPHEAPがクリアされても、他のクラスフラグが立っている可能性があり、結果としてPAUTOではないと判断されることがあります。
      • 変数がPAUTO以外の特殊なクラスである場合。
    • この条件が偽となるケース:
      • 変数が純粋なスタック変数(PAUTO)である場合。この場合、ODCLノードは追加されません。スタック変数は通常、関数のプロローグでまとめて割り当てられるため、個別のODCLは不要です。

この修正により、コンパイラは、既にヒープに割り当てられることが決定している変数や、スタックに割り当てられるべき変数に対して、インライン化の過程で不要なODCLノードを生成しなくなります。結果として、重複したヒープアロケーションが回避され、生成されるコードのメモリ効率が向上します。

test/fixedbugs/issue4667.goは、この修正の有効性を検証するための重要なテストケースです。F()関数内のxはローカル変数ですが、globl = &xによってそのアドレスがグローバル変数にエスケープするため、xはヒープに割り当てられる必要があります。testing.AllocsPerRunは、このF()関数が実行される際に発生するヒープアロケーションの数を測定します。修正前はこれが複数回発生していた可能性がありますが、修正後は期待通り1回のアロケーションに収まることを確認しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Goコンパイラの内部構造に関する一般的な知識
  • Goのtestingパッケージに関するドキュメント