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

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

このコミットは、Goコンパイラのインライン化処理におけるバグ修正に関するものです。具体的には、関数がアンダースコア _ を引数として持つ場合に、その引数に関連する副作用がインライン化中に誤って破棄されてしまう問題に対処しています。

変更されたファイルは以下の通りです。

  • src/cmd/gc/inl.c: Goコンパイラのインライン化処理を司るC言語のソースファイル。このファイルでバグ修正の主要なロジックが変更されています。
  • test/fixedbugs/bug441.go: このコミットで修正されたバグを再現し、修正が正しく行われたことを検証するための新しいテストファイルです。

コミット

commit ee5f59ab4feafd987972a096d5a5c315e753f358
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 01:54:07 2012 -0400

    cmd/gc: preserve side effects during inlining of function with _ argument
    
    Fixes #3593.
    
    R=ken2
    CC=golang-dev, lvd
    https://golang.org/cl/6305061

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

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

元コミット内容

cmd/gc: preserve side effects during inlining of function with _ argument

Fixes #3593.

R=ken2
CC=golang-dev, lvd
https://golang.org/cl/6305061

変更の背景

このコミットは、Go言語のコンパイラ(cmd/gc)における特定のバグ、Issue 3593を修正するために行われました。このバグは、インライン化された関数がアンダースコア _ を引数として受け取る場合に発生しました。

具体的には、Go言語では、関数呼び出しの引数に副作用(例えば、グローバル変数の変更やI/O操作など)が含まれる場合、その副作用は引数がアンダースコア _ であっても評価される必要があります。しかし、このバグが存在するGoコンパイラのバージョンでは、インライン化の過程で、アンダースコア _ に対応する引数の副作用が誤って破棄されてしまうという問題がありました。

例えば、foo(side()) のようなコードで、foo 関数が _ int のようにアンダースコア引数を持つ場合、side() 関数が持つ副作用(例えば、カウンタをインクリメントする)が、インライン化によって失われてしまう可能性がありました。これはプログラムの動作を予期せぬものにする重大なバグであり、修正が必要とされました。

前提知識の解説

Go言語のアンダースコア _ (ブランク識別子)

Go言語におけるアンダースコア _ は「ブランク識別子 (blank identifier)」と呼ばれ、特別な意味を持ちます。これは、変数を宣言したが使用しない場合や、関数の戻り値の一部を無視したい場合などに使用されます。例えば、_ = someValue と書くことで、someValue の評価は行われるものの、その結果は変数に代入されず破棄されます。

重要なのは、_ が単に値を無視するだけであり、その値が生成される過程で発生する副作用は評価されるという点です。例えば、fmt.Println(f())_ = f() の両方で f() が呼び出され、f() が何らかの副作用を持つ場合、その副作用はどちらの場合でも発生します。

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

関数インライン化は、コンパイラ最適化の一種です。これは、関数呼び出しのオーバーヘッドを削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込むプロセスです。これにより、関数呼び出しのスタックフレームの作成や破棄、引数の受け渡しといったコストが削減され、プログラムの実行速度が向上する可能性があります。

しかし、インライン化はコンパイラにとって複雑な処理であり、特に副作用を持つ式や特殊な引数(今回のケースでは _)の扱いを誤ると、プログラムのセマンティクス(意味)が変わってしまう可能性があります。

副作用 (Side Effects)

プログラミングにおいて、副作用とは、関数や式の評価が、その戻り値以外にプログラムの状態に何らかの変更をもたらすことを指します。これには以下のような例があります。

  • グローバル変数や外部変数の変更
  • I/O操作(ファイルの読み書き、ネットワーク通信、画面出力など)
  • 例外の発生
  • タイマーや乱数ジェネレータの状態変更

副作用はプログラムの動作に不可欠な要素ですが、コンパイラ最適化の際には、副作用が正しく評価されるように細心の注意を払う必要があります。副作用を持つ式が最適化によって削除されたり、評価順序が変更されたりすると、プログラムの動作が壊れてしまう可能性があります。

技術的詳細

このバグは、Goコンパイラのインライン化処理 (src/cmd/gc/inl.c) において、アンダースコア _ を引数として持つ関数のインライン化時に、その引数に渡される式の副作用が正しく保持されないことに起因していました。

Goコンパイラは、関数をインライン化する際に、呼び出し元の引数をインライン化される関数の仮引数にマッピングします。このマッピングの過程で、アンダースコア _ で宣言された仮引数に対しては、通常、一時変数を割り当てずにその値を破棄するような処理が行われます。しかし、この処理が不適切であったため、引数として渡される式が持つ副作用が評価されずにスキップされてしまう問題が発生していました。

修正前は、isblank(t->nname) (引数名が _ であるかどうかのチェック) が行われ、もし _ であれば、一時変数を割り当てずに処理を進めていました。この際、引数として渡される式自体が持つ副作用の評価が適切に行われないケースがあったと考えられます。

このコミットでは、tinlvar という新しいヘルパー関数を導入し、インライン化時に引数に対応するノードを生成するロジックを改善しています。tinlvar 関数は、引数 t (型情報) を受け取り、その引数名が _ でない場合は既存の inlvar (インライン化された変数を表すノード) を返し、_ である場合は nblank (ブランク識別子を表す特別なノード) を返すようにしています。

これにより、アンダースコア _ の引数に対しても、nblank というノードが割り当てられるようになり、そのノードが持つべき副作用の評価がコンパイラによって正しく認識され、実行されるようになりました。

具体的には、mkinlcall1 関数内の引数処理ロジックが変更され、tinlvar(t) を使用して引数ノードを生成するように統一されました。これにより、メソッド呼び出しのレシーバや通常の引数など、あらゆるケースでアンダースコア _ の引数に対する副作用が正しく評価されるようになりました。

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

src/cmd/gc/inl.c

このファイルでは、主に以下の変更が行われています。

  1. tinlvar 関数の追加:

    static Node*
    tinlvar(Type *t)
    {
        if(t->nname && !isblank(t->nname)) {
            if(!t->nname->inlvar)
                fatal("missing inlvar for %N\\n", t->nname);
            return t->nname->inlvar;
        }
        typecheck(&nblank, Erv | Easgn);
        return nblank;
    }
    

    この新しいヘルパー関数が、インライン化される関数の引数に対応するノードを生成する役割を担います。引数名が _ でない場合は既存の inlvar を返し、_ の場合は nblank を返します。

  2. mkinlcall1 関数内の引数処理の変更:

    • メソッド呼び出しのレシーバの処理部分:
      -			if(t->nname != N && !isblank(t->nname))
      -				as = nod(OAS, t->nname->inlvar, n->left->left);
      -			else
      -				as = nod(OAS, temp(t->type), n->left->left);
      +			as = nod(OAS, tinlvar(t), n->left->left);
      
      tinlvar(t) を使用するように変更され、_ 引数のレシーバも正しく処理されるようになりました。
    • 非メソッド呼び出しのメソッド引数の処理部分:
      -			if(t != T && t->nname != N && !isblank(t->nname))
      -				as = nod(OAS, t->nname->inlvar, n->list->n);
      +			if(t != T)
      +				as = nod(OAS, tinlvar(t), n->list->n);
      
      ここでも tinlvar(t) が導入され、_ 引数の処理が改善されました。
    • 通常の引数処理のループ内:
      -		for(t = getinargx(fn->type)->type; t; t=t->down) {
      -			if(t->nname && !isblank(t->nname)) {
      -				if(!t->nname->inlvar)
      -					fatal("missing inlvar for %N\\n", t->nname);
      -				as->list = list(as->list, t->nname->inlvar);
      -			} else {
      -				as->list = list(as->list, temp(t->type));
      -			}
      -		}		
      +		for(t = getinargx(fn->type)->type; t; t=t->down)
      +			as->list = list(as->list, tinlvar(t));		
      
      引数リストの生成も tinlvar(t) を使うように簡略化され、_ 引数に対する副作用の評価が保証されるようになりました。
    • 非メソッド呼び出しの引数処理のループ内:
      -		for(t = getinargx(fn->type)->type; t && ll; t=t->down) {
      -			if(t->nname && !isblank(t->nname)) {
      -				if(!t->nname->inlvar)
      -					fatal("missing inlvar for %N\\n`, t->nname);
      -				as->list = list(as->list, t->nname->inlvar);
      -				as->rlist = list(as->rlist, ll->n);
      -			}
      +		for(t = getinargx(fn->type)->type; t && ll; t=t->down) {
      +			as->list = list(as->list, tinlvar(t));
      +			as->rlist = list(as->rlist, ll->n);
      
      同様に、tinlvar(t) を使用するように変更されています。

test/fixedbugs/bug441.go

このファイルは、新しいテストケースとして追加されました。このテストは、アンダースコア _ を引数として持つ関数がインライン化された際に、引数に渡される式の副作用が正しく評価されることを検証します。

// run

// Copyright 2012 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.

// Was discarding function calls made for arguments named _
// in inlined functions.  Issue 3593.

package main

var did int

func main() {
	foo(side())
	foo2(side(), side())
	foo3(side(), side())
	T.m1(T(side()))
	T(1).m2(side())
	const want = 7
	if did != want {
		println("BUG: missing", want-did, "calls")
	}
}

func foo(_ int) {}
func foo2(_, _ int) {}
func foo3(int, int) {} // This is equivalent to foo2 in terms of blank identifiers
type T int
func (_ T) m1() {}
func (t T) m2(_ int) {}

func side() int {
	did++
	return 1
}

このテストでは、side() 関数が呼び出されるたびにグローバル変数 did をインクリメントするという副作用を持っています。foo, foo2, foo3, T.m1, T.m2 といった関数やメソッドは、アンダースコア _ を引数として受け取ります。

もしバグが修正されていなければ、これらの関数がインライン化された際に side() の呼び出しがスキップされ、did の値が期待される 7 にならないはずです。テストは最終的に did の値が want (7) と一致するかどうかをチェックし、一致しない場合は「BUG: missing X calls」というメッセージを出力します。これにより、副作用が正しく保持されていることを検証します。

コアとなるコードの解説

このコミットの核心は、src/cmd/gc/inl.c に追加された tinlvar 関数と、それを利用するように変更された mkinlcall1 関数内の引数処理ロジックです。

tinlvar 関数の役割

tinlvar 関数は、インライン化される関数の仮引数 t (Type構造体) を受け取り、その引数に対応する適切なノードを返します。

  • if(t->nname && !isblank(t->nname)):

    • t->nname は引数の名前を表すノードです。これが存在し、かつ isblank(t->nname) (引数名が _ であるかどうかのチェック) が false、つまり引数名が _ でない場合、通常の名前付き引数として扱われます。
    • この場合、t->nname->inlvar が返されます。inlvar は、インライン化された関数内でこの引数に対応する変数ノードを指します。もし inlvar が設定されていない場合は fatal エラーが発生します。
  • else (引数名が _ の場合):

    • 引数名が _ である場合、typecheck(&nblank, Erv | Easgn) が呼び出されます。nblank はGoコンパイラ内部でブランク識別子 _ を表す特別なノードです。Erv | Easgn は、このノードが右辺値として評価され、代入可能であることを示唆するフラグです。
    • 最終的に nblank が返されます。

この tinlvar 関数を導入することで、コンパイラは _ 引数に対しても nblank という具体的なノードを割り当てるようになりました。これにより、_ 引数に渡される式が持つ副作用が、nblank ノードの評価の一部として正しくコンパイラによって認識され、最適化の過程で誤って削除されることがなくなりました。

mkinlcall1 関数内の変更

mkinlcall1 関数は、実際のインライン化処理を行う主要な関数です。この関数内で、関数呼び出しの引数をインライン化される関数の仮引数にマッピングする際に、tinlvar 関数が利用されるようになりました。

以前のコードでは、引数名が _ であるかどうかを直接チェックし、その場合は一時変数を割り当てずに処理を進めていました。この処理が、副作用を持つ式が _ 引数に渡された場合に、その副作用の評価をスキップしてしまう原因となっていました。

新しいコードでは、tinlvar(t) を呼び出すことで、引数名が _ であっても nblank ノードが返され、そのノードが引数として扱われるようになりました。これにより、_ 引数に渡される式が持つ副作用が、nblank ノードの評価の一部として正しくコンパイラによって認識され、実行されるようになりました。

例えば、as = nod(OAS, tinlvar(t), n->left->left); のような行では、tinlvar(t) が返すノード(通常の変数ノードか nblank ノード)に対して、右辺の式 n->left->left の値が代入される(または評価される)という操作が表現されます。nblank ノードへの代入は、実際には値を破棄しますが、その前に右辺の式が評価されることを保証します。

この変更により、Goコンパイラは、アンダースコア _ を引数として持つ関数のインライン化においても、引数に渡される式の副作用を正しく保持し、プログラムのセマンティクスを維持できるようになりました。

関連リンク

参考にした情報源リンク