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

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

コミット

commit cae604f734ac4e444a36bc3dc18afa42c6f4c737
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 5 13:51:44 2012 -0500

    cmd/gc: must not inline panic, recover
    
    R=lvd, gri
    CC=golang-dev
    https://golang.org/cl/5731061

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

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

元コミット内容

cmd/gc: must not inline panic, recover

R=lvd, gri
CC=golang-dev
https://golang.org/cl/5731061

変更の背景

このコミットは、Goコンパイラ(cmd/gc)において、panicおよびrecover関数がインライン化されないようにするための変更です。

Go言語のコンパイラは、プログラムの実行性能を向上させるために、関数のインライン化(Inlining)という最適化を行います。インライン化とは、呼び出し元の関数に呼び出される関数のコードを直接埋め込むことで、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減する技術です。

しかし、panicrecoverはGoの例外処理メカニズムの根幹をなす特殊な関数であり、通常の関数とは異なる振る舞いをします。panicは現在のゴルーチンを停止させ、遅延関数(defer)を実行しながらスタックを巻き戻し(unwind)ます。recoverは、panicによって発生したパニックを捕捉し、プログラムの実行を継続させるためにdefer関数内で使用されます。

これらの特殊な制御フローを持つ関数が安易にインライン化されると、コンパイラが生成するコードの予測可能性が損なわれたり、panic/recoverのセマンティクスが正しく機能しなくなる可能性がありました。特に、スタックの巻き戻しやdeferの実行順序など、panic/recoverの動作はスタックフレームの構造に密接に関連しているため、インライン化によってスタックフレームの構造が変化すると、予期せぬバグや動作不良を引き起こすリスクがあったと考えられます。

このコミットは、このような潜在的な問題を回避し、panicrecoverの堅牢な動作を保証するために、これらの関数がインライン化の対象から除外されるようにコンパイラの挙動を修正することを目的としています。

前提知識の解説

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

Go言語の公式コンパイラは、gc(Go Compiler)と呼ばれ、Goのソースコードを機械語に変換する役割を担っています。gcは、最適化フェーズにおいて、プログラムの実行効率を高めるための様々な変換を行います。

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

インライン化は、コンパイラ最適化の一種です。関数呼び出しの代わりに、呼び出される関数の本体コードを呼び出し元の位置に直接挿入します。 利点:

  • 関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、引数のコピー、レジスタの保存・復元など)を排除し、実行速度を向上させます。
  • インライン化されたコードに対して、さらに他の最適化(定数伝播、デッドコード削除など)を適用しやすくなります。 欠点:
  • コードサイズが増加する可能性があります。
  • コンパイル時間が増加する可能性があります。
  • デバッグが難しくなる場合があります(元の関数呼び出しのスタックトレースが見えなくなるため)。

Goコンパイラは、関数の複雑さやサイズに基づいて、インライン化の対象とするかどうかを決定します。

3. panicrecover

Go言語には、例外処理のメカニズムとしてpanicrecoverがあります。

  • panic: プログラムの異常終了を示すために使用されます。panicが呼び出されると、通常の実行フローは停止し、現在のゴルーチン内で遅延関数(defer)が実行されながら、スタックが巻き戻されていきます。panicがゴルーチンの最上位まで到達すると、プログラムはクラッシュします。
  • recover: panicによって発生したパニックを捕捉し、プログラムの実行を継続させるために使用されます。recoverdefer関数内でのみ有効です。defer関数内でrecoverが呼び出されると、パニックの値が返され、パニックの連鎖が停止し、通常の実行フローが再開されます。

panicrecoverは、通常の関数呼び出しとは異なり、非ローカルな制御フロー(non-local control flow)を伴います。これは、関数呼び出しスタックを遡って実行をジャンプさせるため、コンパイラが通常の最適化を適用する際に特別な考慮が必要となります。

4. 抽象構文木 (AST) と中間表現 (IR)

コンパイラは、ソースコードを直接機械語に変換するのではなく、いくつかの段階を踏みます。

  • 抽象構文木 (AST): ソースコードの構文構造を木構造で表現したものです。Goコンパイラでは、ソースコードをパースしてASTを構築します。
  • 中間表現 (IR): ASTからさらに変換された、コンパイラ内部で最適化やコード生成のために使われる形式です。Goコンパイラでは、ASTノードが内部的に様々な操作を表す型(ONODE)にマッピングされます。OPANICORECOVERは、Goコンパイラが内部的にpanicrecoverを表すために使用するASTノードの種類、または中間表現における操作の種類を指します。

5. エスケープ解析 (Escape Analysis)

エスケープ解析は、コンパイラ最適化の一種で、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。変数が関数のスコープ外で参照される可能性がある場合(例えば、ポインタが返される場合)、その変数はヒープに「エスケープ」すると判断され、ヒープに割り当てられます。このコミットのtest/escape4.goファイルは、エスケープ解析のテストケースを含んでおり、インライン化がエスケープ解析の結果に影響を与える可能性を示唆しています。

技術的詳細

このコミットの技術的詳細の中心は、Goコンパイラのインライン化ロジックがpanicrecoverの呼び出しを「インライン化すべきではない」と判断するように変更された点です。

Goコンパイラのsrc/cmd/gc/inl.cファイルは、インライン化の判断ロジックを実装しています。特に、ishairy関数は、特定のノード(ASTの要素)がインライン化に適しているかどうかを判断する役割を担っています。ishairyは、インライン化を妨げるような「複雑な」または「特殊な」操作を検出するために使用されます。

変更前は、ishairy関数はOCALLFUNC(通常の関数呼び出し)、OCALLINTER(インターフェースメソッド呼び出し)、OCALLMETH(構造体メソッド呼び出し)といったノードタイプをチェックしていましたが、OPANICORECOVERといった特殊な操作は明示的にインライン化を妨げるものとして扱われていませんでした。

このコミットでは、ishairy関数にOPANICORECOVERのケースが追加されました。これにより、コンパイラはpanicまたはrecoverの呼び出しを含む関数をインライン化しようとする際に、ishairy関数が1(インライン化すべきではない、または複雑である)を返すようになります。

具体的には、debug['l'] < 4という条件があります。これは、コンパイラのデバッグレベルが特定の閾値(この場合は4)未満の場合に、これらの操作を「インライン化すべきではない」と判断することを意味します。デバッグレベルが高い場合(debug['l'] >= 4)は、より積極的なインライン化が行われる可能性がありますが、通常運用ではこの条件が満たされ、panic/recoverのインライン化は抑制されます。

test/escape4.goの変更は、このインライン化抑制の動作を検証するためのものです。以前のテストコードでは、alloc関数のインライン化に関するコメントがありましたが、panicrecoverを含む関数についてはインライン化されないことを明示的に示す新しいテストケースが追加されました。// No inline for panic, recover.というコメントと、f3f4という新しい関数が追加され、それぞれpanic(1)recover()を呼び出しています。これらの関数には// ERROR "can inline f2"のようなインライン化に関するエラーコメントが意図的に付けられていません。これは、コンパイラがこれらの関数をインライン化しないことを期待しているためです。

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

src/cmd/gc/inl.c

diff --git a/src/cmd/gc/inl.c b/src/cmd/gc/inl.c
index 96080cbfaf..efce56057d 100644
--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -182,6 +182,8 @@ ishairy(Node *n, int *budget)\n 	case OCALLFUNC:\n 	case OCALLINTER:\n 	case OCALLMETH:\n+\tcase OPANIC:\n+\tcase ORECOVER:\n 		if(debug['l'] < 4)\n 			return 1;\n 		break;

test/escape4.go

diff --git a/test/escape4.go b/test/escape4.go
index ab3aee2244..8875708963 100644
--- a/test/escape4.go
+++ b/test/escape4.go
@@ -11,8 +11,8 @@ package foo
 
 var p *int
 
-func alloc(x int) *int {  // ERROR "can inline alloc" "moved to heap: x"\n-\treturn &x  // ERROR "&x escapes to heap"\n+func alloc(x int) *int { // ERROR "can inline alloc" "moved to heap: x"\n+\treturn &x // ERROR "&x escapes to heap"\n }\n \n var f func()\n@@ -22,12 +22,18 @@ func f1() {\n \n 	// Escape analysis used to miss inlined code in closures.\n \n-\tfunc() {  // ERROR "func literal does not escape"\n-\t\tp = alloc(3)  // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"\n+\tfunc() { // ERROR "func literal does not escape"\n+\t\tp = alloc(3) // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"\n \t}()\n-\t\n-\tf = func() {  // ERROR \"func literal escapes to heap\"\n-\t\tp = alloc(3)  // ERROR \"inlining call to alloc\" \"&x escapes to heap\" \"moved to heap: x\"\n+\n+\tf = func() { // ERROR "func literal escapes to heap"\n+\t\tp = alloc(3) // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"\n \t}\n \tf()\n }\n+\n+func f2() {} // ERROR "can inline f2"\n+\n+// No inline for panic, recover.\n+func f3() { panic(1) }\n+func f4() { recover() }\n```

## コアとなるコードの解説

### `src/cmd/gc/inl.c` の変更

`src/cmd/gc/inl.c`はGoコンパイラのインライン化に関するロジックが含まれるファイルです。
変更の中心は`ishairy`関数です。この関数は、与えられたASTノード(`Node *n`)がインライン化に適しているかどうかを判断し、インライン化を妨げるような複雑な構造や特殊な操作を持つ場合に`1`を返します。

追加されたコードは以下の通りです。

```c
	case OPANIC:
	case ORECOVER:
		if(debug['l'] < 4)
			return 1;
		break;
  • case OPANIC:: これは、Goコンパイラが内部的にpanic呼び出しを表すために使用するASTノードのタイプです。
  • case ORECOVER:: これは、Goコンパイラが内部的にrecover呼び出しを表すために使用するASTノードのタイプです。

これらのケースが追加されたことで、ishairy関数はpanicまたはrecoverの呼び出しを検出すると、debug['l'] < 4という条件が真である限り(つまり、デバッグレベルが4未満の場合)、直ちに1を返します。1を返すことは、そのノードが「インライン化すべきではない」と判断されたことを意味します。これにより、panicrecoverを含む関数は、コンパイラのインライン化最適化の対象から除外されるようになります。

test/escape4.go の変更

test/escape4.goは、Goコンパイラのエスケープ解析とインライン化の挙動をテストするためのファイルです。

変更点:

  1. 既存のalloc関数のコメントの修正:
    -func alloc(x int) *int {  // ERROR "can inline alloc" "moved to heap: x"\n-\treturn &x  // ERROR "&x escapes to heap"\n    +func alloc(x int) *int { // ERROR "can inline alloc" "moved to heap: x"\n    +\treturn &x // ERROR "&x escapes to heap"\n    ```
    これは主に空白文字の調整であり、機能的な変更ではありません。
    
    
  2. 新しい関数の追加:
    func f2() {} // ERROR "can inline f2"
    
    // No inline for panic, recover.
    func f3() { panic(1) }
    func f4() { recover() }
    
    • func f2() {} // ERROR "can inline f2": これは、空の関数f2がインライン化可能であることをテストするための既存のパターンです。
    • // No inline for panic, recover.というコメントは、続くf3f4がインライン化されないことを意図していることを示しています。
    • func f3() { panic(1) }: panicを呼び出す関数です。
    • func f4() { recover() }: recoverを呼び出す関数です。

これらの新しいテストケースは、src/cmd/gc/inl.cの変更が正しく機能し、panicrecoverを含む関数が実際にインライン化の対象から外れることを検証するために追加されました。もしこれらの関数がインライン化されるべきだとコンパイラが判断した場合、テストは失敗するか、異なるエラーメッセージを生成するでしょう。しかし、この変更によって、コンパイラはこれらの関数をインライン化しないため、テストは期待通りにパスします。

関連リンク

参考にした情報源リンク