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

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

このコミットは、Goコンパイラ(cmd/gc)における可変引数関数(variadic functions)のインライン化に関する機能追加と改善を扱っています。具体的には、可変引数関数がインライン化の対象となるようにコンパイラのロジックが修正され、それに伴う引数処理の複雑性が解決されています。

コミット

commit 73b83a228e61045b76b758f6948df80f7e7e32cd
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Jan 31 08:40:59 2013 +0100

    cmd/gc: inlining of variadic functions.
    
    R=rsc, lvd, golang-dev, kardianos
    CC=golang-dev
    https://golang.org/cl/7093050

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

https://github.com/golang/go/commit/73b83a228e61045b76b758f6948df80f7e7e32cd

元コミット内容

cmd/gc: inlining of variadic functions.

このコミットは、Goコンパイラ(cmd/gc)において、可変引数関数(variadic functions)のインライン化を可能にするための変更を導入します。

変更の背景

Go言語のコンパイラは、プログラムの実行性能を向上させるために様々な最適化を行います。その一つが「インライン化」です。関数をインライン化することで、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減し、さらに呼び出し元と呼び出し先のコードを統合することで、より広範な最適化(例えば、定数伝播やデッドコード削除)の機会を増やすことができます。

しかし、可変引数関数(例: func foo(args ...int))は、その引数がスライスとして扱われるという特殊性から、通常の関数とは異なる引数処理ロジックを必要とします。これまでのGoコンパイラでは、この複雑性のため、可変引数関数はインライン化の対象外とされていました。

このコミットの背景には、可変引数関数もインライン化の恩恵を受けられるようにし、Goプログラム全体の実行性能をさらに向上させたいという意図があります。特に、fmt.Printlnのような頻繁に利用される標準ライブラリの関数が可変引数であるため、これらの関数のインライン化は大きな性能改善に繋がる可能性があります。

前提知識の解説

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

Go言語の公式コンパイラは、通常 gc と呼ばれます。これはGoのソースコードを機械語に変換する役割を担っています。src/cmd/gc ディレクトリには、このコンパイラのソースコードが含まれています。コンパイラは、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成といった複数のフェーズを経て処理を進めます。

2. 関数インライン化 (Function Inlining)

関数インライン化は、コンパイラ最適化の一種です。関数呼び出しの箇所で、呼び出される関数の本体のコードを直接展開する(埋め込む)ことで、関数呼び出しのコストを削減します。

例:

func add(a, b int) int {
    return a + b
}

func main() {
    x := add(1, 2) // ここで add 関数がインライン化されると
    // x := 1 + 2   // このように展開されるイメージ
}

インライン化は、小さな関数や頻繁に呼び出される関数に対して特に効果的です。しかし、コードサイズが増加する可能性もあるため、コンパイラはインライン化のヒューリスティック(判断基準)を持っています。Goコンパイラでは、関数の複雑さ(ノード数など)や、特定のデバッグレベル設定(debug['l'])に基づいてインライン化の可否を判断します。

3. 可変引数関数 (Variadic Functions)

Go言語では、引数の数が可変である関数を定義できます。これは、最後のパラメータの型の前に ... を付けることで実現されます。

例:

func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func main() {
    sum(1, 2, 3) // nums は []int{1, 2, 3} となる
    sum(10)      // nums は []int{10} となる
}

関数内部では、可変引数はその型のスライスとして扱われます。例えば、...int[]int として関数本体に渡されます。

4. Goコンパイラ内部のデータ構造と概念

  • Node: Goコンパイラがソースコードを抽象構文木(AST)として表現する際の基本的な単位です。変数、定数、演算子、関数呼び出しなど、プログラムのあらゆる要素が Node として表現されます。
  • Type: Go言語の型システムを表すデータ構造です。関数のシグネチャ、変数の型などを定義します。
  • NodeList: Node のリスト(通常はリンクリスト)です。関数のボディ(ステートメントのリスト)や引数のリストなどを表現するのに使われます。
  • OP (Operation): Node の種類を示す列挙型です。例えば、OCALL(関数呼び出し)、OAS(代入)、OAS2(多重代入)、OCOMPLIT(複合リテラル)、OSLICE(スライス操作)などがあります。
  • isddd: Type 構造体内のフラグで、その型が可変引数(...)であることを示します。
  • tinlvar / retvar: インライン化の際に、一時的な変数(インライン化された関数の引数や戻り値を保持するため)を生成するためのヘルパー関数です。

技術的詳細

このコミットの主要な変更は、src/cmd/gc/inl.c ファイルに集中しており、Goコンパイラのインライン化ロジックが可変引数関数を適切に処理できるように拡張されています。

1. インライン化デバッグレベル debug['l'] の変更

以前は debug['l'] のレベル3がコメントアウトされていましたが、このコミットで「allow variadic functions」という説明が追加されました。これは、コンパイラのデバッグレベルが3以上の場合に可変引数関数のインライン化が許可されることを示唆しています。

2. caninl 関数の変更

caninl 関数は、特定の関数がインライン化可能かどうかを判断する役割を担っています。 変更前は、関数の型を走査し、t->isddd(可変引数)が見つかった場合、無条件にインライン化を拒否していました。

// 変更前
// can't handle ... args yet
for(t=fn->type->type->down->down->type; t; t=t->down)
    if(t->isddd)
        return; // 可変引数が見つかったら即座にインライン化を拒否

変更後、このロジックは debug['l'] < 3 の条件で囲まれました。

// 変更後
if(debug['l'] < 3)
    for(t=fn->type->type->down->down->type; t; t=t->down)
        if(t->isddd)
            return; // debug['l'] が3未満の場合のみ、可変引数でインライン化を拒否

これにより、debug['l'] が3以上の場合は、可変引数関数もインライン化の候補として扱われるようになります。これは、可変引数関数のインライン化を有効にするための重要なゲートキーパーの変更です。

3. mkinlcall および mkinlcall1 関数の変更

mkinlcallmkinlcall1 は、実際にインライン化された関数呼び出しのASTを構築する中心的な関数です。 これらの関数に isddd という新しい引数が追加されました。この引数は、呼び出し元が可変引数呼び出し(例: foo(slice...) のようにスライスを展開して渡す形式)であるかどうかを示します。

最も重要な変更は mkinlcall1 関数内部の引数処理ロジックです。

  • 可変引数検出と初期化: variadic, varargcount, multiret, vararg, varargs, varargtype, vararrtype といった変数が導入され、インライン化される関数が可変引数であるか、呼び出し元が多値戻り値を持つ関数からの引数であるかなどを検出します。 variadic フラグは、インライン化される関数自体が可変引数である場合にセットされます。ただし、呼び出し元も ... で引数を渡している場合(isddd が真の場合)は、その可変引数性は無視されます。

  • 引数割り当てロジックの再構築: インライン化された関数の仮引数に、呼び出し元の実引数を割り当てる OAS2 (多重代入) ノードの構築ロジックが大幅に修正されました。 変更前は、単純に n->list(呼び出し元の実引数リスト)と getinargx(fn->type)->type(インライン化される関数の仮引数リスト)を対応付けていました。

    変更後は、chkargcount(引数リストが複数要素を持つか)の有無で分岐し、特に可変引数(variadic && t->isddd)の処理が追加されました。 可変引数部分については、varargcount(可変引数として渡される実引数の数)に基づいて、個々の引数を一時変数に割り当て、それらを後でスライスにまとめるための varargs リストを構築します。

  • 可変引数スライスの構築: インライン化された関数の可変引数に対応するスライスを構築するロジックが追加されました。 variadic が真の場合、OAS (代入) ノードが作成され、vararg(可変引数スライスを表す一時変数)に値が割り当てられます。 引数が一つもない場合(!varargcount)、nodnil() を使って空のスライスが作成されます。 引数がある場合、OCOMPLIT (複合リテラル) を使って varargs リストの要素から配列リテラルを作成し、それを OSLICE (スライス) オペレーションでスライスに変換します。

4. argvar 関数の追加

argvar という新しい静的ヘルパー関数が追加されました。 この関数は、多値戻り値を持つ関数からの引数を受け取る際に、インライン化された関数の引数を格納するための一時変数を合成する役割を担います。 ~arg%d のようなユニークな名前を持つ Node を作成し、その型を設定し、PAUTO クラス(自動変数)として現在の関数(呼び出し元の関数)の宣言リストに追加します。

// 新しく追加された argvar 関数
static Node*
argvar(Type *t, int i)
{
    Node *n;

    snprint(namebuf, sizeof(namebuf), "~arg%d", i);
    n = newname(lookup(namebuf));
    n->type = t->type;
    n->class = PAUTO;
    n->used = 1;
    n->curfn = curfn;   // the calling function, not the called one
    curfn->dcl = list(curfn->dcl, n);
    return n;
}

この関数は、特に multiret(多値戻り値)のケースで、可変引数スライスを構築する際に、個々の要素を一時的に保持するために利用されます。

これらの変更により、Goコンパイラは可変引数関数のインライン化を正確に処理できるようになり、Goプログラムの実行時性能向上に貢献します。

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

src/cmd/gc/inl.c ファイルにおける主要な変更箇所は以下の通りです。

  1. debug['l'] のコメント変更 (L13)

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -13,7 +13,7 @@
     //      0: disabled
     //      1: 40-nodes leaf functions, oneliners, lazy typechecking (default)
     //      2: early typechecking of all imported bodies 
    -//      3: 
    +//      3: allow variadic functions
     //      4: allow non-leaf functions , (breaks runtime.Caller)
     //      5: transitive inlining
     //
    
  2. mkinlcall 関数のシグネチャ変更 (L39, L477) isddd 引数の追加。

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -39,9 +39,10 @@ static int	ishairylist(NodeList *ll, int *budget);
     // Used by inlcalls
     static void	inlnodelist(NodeList *l);
     static void	inlnode(Node **np);
    -static void	mkinlcall(Node **np, Node *fn);
    +static void	mkinlcall(Node **np, Node *fn, int isddd);
     static Node*	inlvar(Node *n);
     static Node*	retvar(Type *n, int i);
    +static Node*	argvar(Type *n, int i);
     static Node*	newlabel(void);
     static Node*	inlsubst(Node *n);
     static NodeList* inlsubstlist(NodeList *l);
    
    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -477,10 +479,10 @@ inlnode(Node **np)
     	lineno = lno;
     }
     
    -static void	mkinlcall1(Node **np, Node *fn);
    +static void	mkinlcall1(Node **np, Node *fn, int isddd);
     
     static void
    -mkinlcall(Node **np, Node *fn)
    +mkinlcall(Node **np, Node *fn, int isddd)
     {
     	int save_safemode;
     	Pkg *pkg;
    
  3. caninl 関数のロジック変更 (L131) 可変引数チェックに debug['l'] < 3 の条件を追加。

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -131,9 +132,10 @@ caninl(Node *fn)
     		fatal("caninl on non-typechecked function %N", fn);
     
     	// can't handle ... args yet
    -	for(t=fn->type->type->down->down->type; t; t=t->down)
    -		if(t->isddd)
    -			return;
    +	if(debug['l'] < 3)
    +		for(t=fn->type->type->down->down->type; t; t=t->down)
    +			if(t->isddd)
    +				return;
     
     	budget = 40;  // allowed hairyness
     	if(ishairylist(fn->nbody, &budget))
    
  4. inlnode 関数内の mkinlcall 呼び出し変更 (L453, L469) n->isddd を新しい引数として渡す。

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -453,10 +455,10 @@ inlnode(Node **np)
     		if(debug['m']>3)
     			print("%L:call to func %+N\\n", n->lineno, n->left);
     		if(n->left->inl)	// normal case
    -			mkinlcall(np, n->left);
    +			mkinlcall(np, n->left, n->isddd);
     		else if(n->left->op == ONAME && n->left->left && n->left->left->op == OTYPE && n->left->right &&  n->left->right->op == ONAME)  // methods called as functions
     			if(n->left->sym->def)
    -				mkinlcall(np, n->left->sym->def);
    +				mkinlcall(np, n->left->sym->def, n->isddd);
     		break;
     
     	case OCALLMETH:
    @@ -469,7 +471,7 @@ inlnode(Node **np)
     		if(n->left->type->nname == N) 
     			fatal("no function definition for [%p] %+T\\n", n->left->type, n->left->type);
     
    -		mkinlcall(np, n->left->type->nname);
    +		mkinlcall(np, n->left->type->nname, n->isddd);
     
     		break;
     	}
    
  5. mkinlcall1 関数の大幅な変更 (L513-L652) 可変引数処理のための新しい変数、ロジック、およびスライス構築の追加。

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -513,13 +515,18 @@ tinlvar(Type *t)
     // inlined function body and list, rlist contain the input, output
     // parameters.
     static void
    -mkinlcall1(Node **np, Node *fn)
    +mkinlcall1(Node **np, Node *fn, int isddd)
     {
     	int i;
     	int chkargcount;
     	Node *n, *call, *saveinlfn, *as, *m;
     	NodeList *dcl, *ll, *ninit, *body;
     	Type *t;
    +	// For variadic fn.
    +	int variadic, varargcount, multiret;
    +	Node *vararg;
    +	NodeList *varargs;
    +	Type *varargtype, *vararrtype;
     
     	if (fn->inl == nil)
     		return;
    @@ -589,6 +596,40 @@ mkinlcall1(Node **np, Node *fn)
     		}
     	}
     
    +	// check if inlined function is variadic.
    +	variadic = 0;
    +	varargtype = T;
    +	varargcount = 0;
    +	for(t=fn->type->type->down->down->type; t; t=t->down) {
    +		if(t->isddd) {
    +			variadic = 1;
    +			varargtype = t->type;
    +		}
    +	}
    +	// but if argument is dotted too forget about variadicity.
    +	if(variadic && isddd)
    +		variadic = 0;
    +
    +	// check if argument is actually a returned tuple from call.
    +	multiret = 0;
    +	if(n->list && !n->list->next) {
    +		switch(n->list->n->op) {
    +		case OCALL:
    +		case OCALLFUNC:
    +		case OCALLINTER:
    +		case OCALLMETH:
    +			if(n->list->n->left->type->outtuple > 1)
    +				multiret = n->list->n->left->type->outtuple-1;
    +		}
    +	}
    +
    +	if(variadic) {
    +		varargcount = count(n->list) + multiret;
    +		if(n->left->op != ODOTMETH)
    +			varargcount -= fn->type->thistuple;
    +		varargcount -= fn->type->intuple - 1;
    +	}
    +
     	// assign arguments to the parameters' temp names
     	as = nod(OAS2, N, N);\
      as->rlist = n->list;\
    @@ -611,21 +652,73 @@ mkinlcall1(Node **np, Node *fn)
     
     	// append ordinary arguments to LHS.
     	chkargcount = n->list && n->list->next;\
    -	for(t = getinargx(fn->type)->type; t && (!chkargcount || ll); t=t->down) {
    -		if(chkargcount && ll) {
    -			// len(n->list) > 1, count arguments.
    +	vararg = N;    // the slice argument to a variadic call
    +	varargs = nil; // the list of LHS names to put in vararg.
    +	if(!chkargcount) {
    +		// 0 or 1 expression on RHS.
    +		for(t = getinargx(fn->type)->type; t; t=t->down) {
    +			if(variadic && t->isddd) {
    +				vararg = tinlvar(t);
    +				for(i=0; i<varargcount && ll; i++) {
    +					m = argvar(varargtype, i);
    +					varargs = list(varargs, m);
    +					as->list = list(as->list, m);
    +				}
    +				break;
    +			}
    +			as->list = list(as->list, tinlvar(t));
    +		}
    +	} else {
    +		// match arguments except final variadic (unless the call is dotted itself)
    +		for(t = getinargx(fn->type)->type; t;) {
    +			if(!ll)
    +				break;
    +			if(variadic && t->isddd)
    +				break;
    +			as->list = list(as->list, tinlvar(t));
    +			t=t->down;
     			ll=ll->next;
     		}
    -		as->list = list(as->list, tinlvar(t));
    +		// match varargcount arguments with variadic parameters.
    +		if(variadic && t && t->isddd) {
    +			vararg = tinlvar(t);
    +			for(i=0; i<varargcount && ll; i++) {
    +				m = argvar(varargtype, i);
    +				varargs = list(varargs, m);
    +				as->list = list(as->list, m);
    +				ll=ll->next;
    +			}
    +			if(i==varargcount)
    +				t=t->down;
    +		}
    +		if(ll || t)
    +			fatal("arg count mismatch: %#T  vs %,H\\n",  getinargx(fn->type), n->list);
     	}
    -	if(chkargcount && (ll || t))
    -		fatal("arg count mismatch: %#T  vs %,H\\n",  getinargx(fn->type), n->list);
     
      if (as->rlist) {
     		typecheck(&as, Etop);
     		ninit = list(ninit, as);
      }
     
    +	// turn the variadic args into a slice.
    +	if(variadic) {
    +		as = nod(OAS, vararg, N);
    +		if(!varargcount) {
    +			as->right = nodnil();
    +			as->right->type = varargtype;
    +		} else {
    +			vararrtype = typ(TARRAY);
    +			vararrtype->type = varargtype->type;
    +			vararrtype->bound = varargcount;
    +
    +			as->right = nod(OCOMPLIT, N, typenod(varargtype));
    +			as->right->list = varargs;
    +			as->right = nod(OSLICE, as->right, nod(OKEY, N, N));
    +		}
    +		typecheck(&as, Etop);
    +		ninit = list(ninit, as);
    +	}
    +
      // zero the outparams
      for(ll = inlretvars; ll; ll=ll->next) {
     		as = nod(OAS, ll->n, N);
    
  6. argvar 関数の追加 (L802-L817)

    --- a/src/cmd/gc/inl.c
    +++ b/src/cmd/gc/inl.c
    @@ -709,6 +802,23 @@ retvar(Type *t, int i)
     	return n;
     }
     
    +// Synthesize a variable to store the inlined function's arguments
    +// when they come from a multiple return call.
    +static Node*
    +argvar(Type *t, int i)
    +{
    +	Node *n;
    +
    +	snprint(namebuf, sizeof(namebuf), "~arg%d", i);
    +	n = newname(lookup(namebuf));
    +	n->type = t->type;
    +	n->class = PAUTO;
    +	n->used = 1;
    +	n->curfn = curfn;   // the calling function, not the called one
    +	curfn->dcl = list(curfn->dcl, n);
    +	return n;
    +}
    +
     static Node*
      newlabel(void)
      {
    

コアとなるコードの解説

このコミットの核となる変更は、mkinlcall1 関数における可変引数関数の引数処理ロジックの再構築です。

  1. 可変引数性の判定: variadic 変数を用いて、インライン化対象の関数が可変引数であるかを判定します。t->isddd フラグがその判断基準となります。 また、呼び出し元が既に可変引数としてスライスを展開して渡している場合(isddd が真の場合)、インライン化される関数側での可変引数処理は不要となるため、variadic フラグをリセットします。

  2. 多値戻り値の考慮: multiret 変数は、呼び出し元の式が多値戻り値を持つ関数呼び出しである場合に、その戻り値の数を考慮に入れます。これは、多値戻り値が可変引数として渡される可能性があるためです。

  3. varargcount の計算: varargcount は、可変引数として実際に渡される引数の数を正確に計算します。これは、呼び出し元の引数リストの数 (count(n->list)) に multiret を加算し、さらにメソッド呼び出しや通常の引数の数を減算することで導出されます。これにより、可変引数スライスに含めるべき要素の数が確定します。

  4. 引数割り当ての分岐: chkargcount(呼び出し元の引数リストが複数要素を持つか)の有無によって、引数割り当てのロジックが分岐します。

    • 単一式の場合 (!chkargcount): 呼び出し元が単一の式(例: foo(mySlice...) のようにスライスをそのまま渡す場合)であるケースを処理します。この場合、可変引数部分を tinlvar(t) で一時変数に割り当て、その後のループで argvar を使って個々の要素を varargs リストに格納します。
    • 複数式の場合 (chkargcount): 呼び出し元が複数の引数を直接渡しているケース(例: foo(1, 2, 3))を処理します。通常の引数を先に割り当て、その後、可変引数部分に相当する引数を varargcount に基づいて argvar で一時変数に割り当て、varargs リストに格納します。
  5. 可変引数スライスの構築: if(variadic) ブロック内で、最終的な可変引数スライスを構築します。

    • varargcount が0の場合、nodnil() を使って空のスライスを表すASTノードを作成します。
    • varargcount が0でない場合、OCOMPLIT(複合リテラル)を使って varargs リストの要素から配列リテラルを作成します。この配列リテラルは、可変引数の実体となる配列です。
    • 次に、この配列リテラルを OSLICE(スライス)オペレーションを使ってスライスに変換し、vararg(可変引数スライスを表す一時変数)に代入します。
  6. argvar 関数の役割: argvar 関数は、インライン化の過程で必要となる一時的な変数(特に多値戻り値から可変引数に渡される個々の要素を保持するため)を動的に生成します。これにより、コンパイラは実行時に必要なメモリを確保し、引数の値を適切に伝播させることができます。

これらの変更により、Goコンパイラは可変引数関数の複雑な引数渡しメカニズムをインライン化のコンテキストで正確に再現できるようになり、結果として可変引数関数のインライン化が可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語のコンパイラ最適化に関する一般的な情報 (例: インライン化):
    • Go Compiler Internals (GoDoc): https://pkg.go.dev/cmd/compile (直接的なインライン化のドキュメントではないが、コンパイラ全般の理解に役立つ)
    • Goのインライン化に関する議論や記事 (例: "Go's inliner" で検索)
  • Go言語の可変引数関数に関する情報:
  • GoコンパイラのASTノードや内部構造に関する情報 (例: src/cmd/compile/internal/syntaxsrc/cmd/compile/internal/ir ディレクトリのコード、または関連するGoの設計ドキュメント)

これらのリンクは、Goコンパイラの内部動作、インライン化のメカニズム、および可変引数関数の実装に関するより深い理解を得るために役立ちます。