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

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

このコミットは、Goコンパイラ(cmd/gc)におけるライブネス解析のバグ修正に関するものです。具体的には、アドレスが取得された(addrtaken)関数結果(戻り値)が、関数のエントリ時に誤って「ライブ」(使用中)とマークされる問題を解決しています。

コミット

commit 824e918ca4f799c4105ef1b96d81894a137a1b29
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 13 21:11:50 2014 -0500

    cmd/gc: fix liveness for addressed results
    
    Was spuriously marking results live on entry to function.
    
    TBR=iant
    CC=golang-codereviews
    https://golang.org/cl/63640043

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

https://github.com/golang/go/commit/824e918ca4f799c4105ef1b96d81894a137a1b29

元コミット内容

--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -668,8 +668,15 @@ progeffects(Prog *prog, Array *vars, Bvec *uevar, Bvec *varkill, Bvec *avarinit)
 		    node = *(Node**)arrayget(vars, i);
 		    switch(node->class & ~PHEAP) {
 		    case PPARAM:
-		    case PPARAMOUT:
 		    	bvset(uevar, i);
+		    case PPARAMOUT:
+		    	// If the result had its address taken, it is being tracked
+		    	// by the avarinit code, which does not use uevar.
+		    	// If we added it to uevar too, we'd not see any kill
+		    	// and decide that the varible was live entry, which it is not.
+		    	// So only use uevar in the non-addrtaken case.
+		    	if(!node->addrtaken)
+		    		bvset(uevar, i);
 		    	break;
 		    }
 		}
--- a/test/live.go
+++ b/test/live.go
@@ -86,3 +86,12 @@ func f6() (_, y string) {\n 	y = "hello"\n 	return\n }\n+\n+// confusion about addressed results used to cause "live at entry to f7: x".\n+\n+func f7() (x string) {\n+	_ = &x\n+	x = "hello"\n+	return\n+}\n+\n```

## 変更の背景

Goコンパイラ(`cmd/gc`)のライブネス解析において、関数が名前付きの戻り値(結果変数)を持ち、かつその戻り値のアドレスが関数内で取得されている(例: `&x`)場合に、誤ったライブネス情報が生成されるバグが存在しました。

具体的には、このような戻り値が、関数のエントリ時点ですでに「ライブ」(使用される可能性がある)であると誤って判断されていました。これは、コンパイラが変数のライフタイムを正確に追跡する上で問題となり、最適化の妨げになったり、デバッグ情報の誤りにつながる可能性がありました。

このコミットは、この誤ったライブネス情報の生成を防ぎ、コンパイラがより正確なライブネス解析を行えるようにすることを目的としています。

## 前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

1.  **Goコンパイラ (`cmd/gc`)**: Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。コンパイル過程には、構文解析、型チェック、中間表現生成、最適化、コード生成など、様々なフェーズが含まれます。

2.  **ライブネス解析 (Liveness Analysis)**:
    *   コンパイラのデータフロー解析の一種で、特定のプログラムポイントにおいて、ある変数の値が将来的に使用される可能性があるかどうか(「ライブ」であるか)を決定する静的解析手法です。
    *   変数が「ライブ」であるとは、その変数の現在の値が、プログラムの実行パスのどこかで後続の命令によって読み取られる可能性があることを意味します。
    *   変数が「デッド」(ライブでない)であるとは、その変数の現在の値が、それ以降のプログラム実行で決して使用されないことを意味します。
    *   ライブネス解析は、レジスタ割り当て(デッドな変数が占めていたレジスタを解放する)、デッドコード削除(デッドな変数を計算する命令を削除する)、スタックフレームの最適化など、多くのコンパイラ最適化の基礎となります。

3.  **`PPARAM` と `PPARAMOUT`**:
    *   Goコンパイラの内部表現におけるノードのクラス(種類)です。
    *   `PPARAM`: 関数の入力パラメータ(引数)を表します。
    *   `PPARAMOUT`: 関数の出力パラメータ(戻り値)を表します。Goでは、`func f() (x int)` のように名前付きの戻り値を定義できます。この `x` が `PPARAMOUT` に該当します。

4.  **`addrtaken` フラグ**:
    *   コンパイラ内部で、ある変数のアドレスがプログラム内で取得されたかどうかを示すフラグです(例: `&myVar`)。
    *   アドレスが取得された変数は、ポインタを介してアクセスされる可能性があるため、コンパイラは通常、その変数をスタックではなくヒープに割り当てる(エスケープ解析)か、あるいはその変数のライフタイムをより慎重に管理する必要があります。

5.  **`uevar` (Upward Exposed Variables)**:
    *   「上向きに露出した変数」または「使用前に定義されていない変数」を意味します。
    *   データフロー解析の文脈では、ある基本ブロック(Basic Block)の入り口でライブである変数、つまりそのブロック内で使用されるが、そのブロック内では定義されていない変数を指します。これらの変数の値は、ブロックに入る前に外部から提供される必要があります。
    *   `bvset(uevar, i)` は、`uevar` ビットベクトル(ビットマップ)の `i` 番目のビットをセットする操作で、`i` 番目の変数が `uevar` であることを示します。

6.  **`avarinit` (Address-taken Variable Initialization)**:
    *   このコミットの文脈では、アドレスが取得された変数の初期化やライフタイムを追跡するためのコンパイラ内部のメカニズムを指していると考えられます。
    *   `uevar` とは異なる方法で変数のライブネスを管理していることが示唆されています。

## 技術的詳細

Goコンパイラの`cmd/gc/plive.c`ファイルは、プログラムのライブネス解析を担当する部分です。`progeffects`関数は、個々の命令(`Prog`)が変数のライブネスに与える影響を分析する際に呼び出されます。

問題は、`PPARAMOUT`(名前付き戻り値)の変数が処理される際に発生していました。
元のコードでは、`PPARAM`(入力引数)と同様に、`PPARAMOUT` の変数も無条件に `uevar` ビットベクトルにセットされていました。これは、これらの変数が関数のエントリ時点で「上向きに露出している」(つまり、外部から値が提供される必要がある)と見なされることを意味します。

しかし、名前付き戻り値は、関数が実行を開始した時点ではまだ値が設定されていません。これらは関数本体内で初期化され、`return` ステートメントによって最終的な値が設定されます。したがって、関数のエントリ時点で「ライブ」であるとマークされるのは誤りです。

特に問題となるのは、戻り値のアドレスが取得された(`node->addrtaken` が真である)場合です。コミットメッセージのコメントによると、アドレスが取得された変数は `avarinit` という別のメカニズムによって追跡されます。この `avarinit` は `uevar` とは独立して動作します。

もし `addrtaken` な `PPARAMOUT` 変数を `uevar` にもセットしてしまうと、以下の問題が発生します。
1.  `uevar` は、変数が「使用前に定義されていない」ことを示すため、関数のエントリでその変数がライブであると推論します。
2.  しかし、`avarinit` はこの変数の実際の初期化とライフタイムを管理しており、`uevar` の状態を「キル」(デッドにする)するような操作を行いません。
3.  結果として、コンパイラは `addrtaken` な `PPARAMOUT` 変数が関数のエントリから終了までずっとライブであると誤って判断してしまいます。これは、実際には関数内で初期化されるまでライブではないため、不正確です。

このバグは、`test/live.go` に追加された `f7` 関数で具体的に示されています。
```go
func f7() (x string) {
	_ = &x // ここで戻り値 'x' のアドレスが取得される
	x = "hello"
	return
}

この f7 関数では、戻り値 x のアドレスが _ = &x によって取得されています。この場合、修正前のコンパイラは「f7 のエントリで x がライブである」という誤った診断を下していました。

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

変更は src/cmd/gc/plive.c ファイルの progeffects 関数内にあります。

 // 変更前
 case PPARAM:
 case PPARAMOUT:
     bvset(uevar, i);

 // 変更後
 case PPARAM:
     bvset(uevar, i);
 case PPARAMOUT:
     // If the result had its address taken, it is being tracked
     // by the avarinit code, which does not use uevar.
     // If we added it to uevar too, we'd not see any kill
     // and decide that the varible was live entry, which it is not.
     // So only use uevar in the non-addrtaken case.
     if(!node->addrtaken)
         bvset(uevar, i);

また、この修正を検証するためのテストケースが test/live.go に追加されました。

 // confusion about addressed results used to cause "live at entry to f7: x".

 func f7() (x string) {
 	_ = &x
 	x = "hello"
 	return
 }

コアとなるコードの解説

修正の核心は、PPARAMOUT(関数結果)の変数を uevar ビットベクトルにセットする条件に !node->addrtaken を追加した点です。

  • PPARAM の場合: 入力引数は関数のエントリ時点で値が提供されているため、引き続き無条件に uevar にセットされます。これは正しい挙動です。
  • PPARAMOUT の場合:
    • node->addrtakentrue(アドレスが取得されている)の場合、bvset(uevar, i) は実行されません。これは、これらの変数のライブネスは avarinit メカニズムによって適切に管理されており、uevar に含めることで誤ったライブネス情報が生成されるのを防ぐためです。
    • node->addrtakenfalse(アドレスが取得されていない)の場合、bvset(uevar, i) が実行されます。これは、アドレスが取得されていない名前付き戻り値は、通常の変数と同様に uevar を介してライブネスが追跡される必要があるためです。

この変更により、コンパイラは addrtakenPPARAMOUT 変数に対して、関数のエントリ時に「ライブ」であるという誤った推論を行わなくなります。これにより、ライブネス解析の正確性が向上し、その後の最適化フェーズでの誤った判断を防ぐことができます。

test/live.go に追加された f7 関数は、この修正が正しく機能することを確認するためのものです。この関数は、名前付き戻り値 x のアドレスを取得し、その後に x を初期化しています。修正前は、このコードに対してコンパイラが誤ったライブネス警告を出していましたが、修正後は警告が出なくなり、バグが修正されたことを示します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特に src/cmd/gc/plive.c および test/live.go
  • コンパイラのデータフロー解析、特にライブネス解析に関する一般的な文献やオンラインリソース。
  • Goコンパイラの内部に関するドキュメントやブログ記事(もしあれば)。
  • GoのIssueトラッカーやChange List (CL) の関連情報(https://golang.org/cl/63640043)。