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

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

このコミットは、Goコンパイラのcmd/gcにおけるエスケープ解析のバグを修正するものです。具体的には、配列のスライス(slice of array)に対するエスケープ解析が正しく行われず、本来ヒープに割り当てられるべき変数がスタックに割り当てられてしまう問題を解決します。これにより、Goプログラムのメモリ安全性とパフォーマンスが向上します。

コミット

commit f078711b412e9949d242e2bb54fc26d759232f5f
Author: Russ Cox <rsc@golang.org>
Date:   Mon May 12 14:45:05 2014 -0400

    cmd/gc: fix escape analysis for slice of array
    
    Fixes #7931.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/100390044

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

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

元コミット内容

cmd/gc: fix escape analysis for slice of array

Fixes #7931.

LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/100390044

変更の背景

このコミットは、Goコンパイラのエスケープ解析における特定のバグ、Issue 7931を修正するために導入されました。エスケープ解析は、Goプログラムの変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定する重要な最適化プロセスです。スタック割り当ては高速であり、ガベージコレクションのオーバーヘッドがないため、可能な限りスタックを使用することが望ましいとされています。

しかし、このバグが存在するGoコンパイラでは、配列から作成されたスライス(slice of array)が、その寿命が関数スコープを超えてしまう場合(例えば、そのスライスの要素へのポインタが関数外に渡される場合)でも、誤ってスタックに割り当てられてしまう可能性がありました。これは、エスケープ解析がOSLICEARR(配列のスライスを表す内部ノードタイプ)を正しく処理できていなかったためです。

具体的には、test/escape2.goに追加されたテストケースbar151bar151bbar151cbar151dがこの問題を示しています。これらのテストケースでは、ローカルに宣言された配列からスライスを作成し、そのスライスの一部へのポインタを外部にエスケープさせています。本来であれば、この配列はヒープに移動されるべきですが、バグのあるコンパイラではスタックに残り、結果としてダングリングポインタや未定義の動作を引き起こす可能性がありました。

この修正は、このような誤ったスタック割り当てを防ぎ、Goプログラムのメモリ安全性と信頼性を確保することを目的としています。

前提知識の解説

1. Goのエスケープ解析 (Escape Analysis)

Goのエスケープ解析は、コンパイラが行う最適化の一つで、変数がメモリのどこに割り当てられるべきかを決定します。主な目的は、可能な限り多くの変数をスタックに割り当てることで、ガベージコレクションの負担を軽減し、プログラムの実行速度を向上させることです。

  • スタック割り当て (Stack Allocation): 関数内で宣言され、その関数の実行が終了すると不要になる変数は、通常スタックに割り当てられます。スタックはLIFO(後入れ先出し)構造で、割り当てと解放が非常に高速です。
  • ヒープ割り当て (Heap Allocation): 変数の寿命がそれを宣言した関数のスコープを超える場合(例えば、関数からポインタが返される場合や、グローバル変数に代入される場合)、その変数はヒープに割り当てられます。ヒープはより柔軟なメモリ管理を提供しますが、ガベージコレクタによる管理が必要となり、オーバーヘッドが発生します。

エスケープ解析は、変数が「エスケープする」(つまり、その変数の寿命が現在のスコープを超える)かどうかを判断します。エスケープすると判断された変数はヒープに割り当てられます。

2. Goのスライス (Slice) と配列 (Array)

  • 配列 (Array): Goの配列は、同じ型の要素を固定長で連続して格納するデータ構造です。配列の長さは型の一部であり、コンパイル時に決定されます。例: var a [10]int
  • スライス (Slice): スライスは、配列の一部を参照する動的なビューです。スライス自体は、基底配列へのポインタ、長さ(len)、容量(cap)の3つのフィールドを持つ小さなデータ構造(ヘッダ)です。スライスは配列とは異なり、長さを実行時に変更できます(ただし、容量の範囲内)。
    • スライスヘッダは通常、スタックに割り当てられます。
    • スライスが参照する基底配列は、その寿命やサイズによってスタックまたはヒープに割り当てられます。
      • 基底配列が関数内で宣言され、そのスライスや要素へのポインタが関数外にエスケープしない場合、基底配列もスタックに割り当てられる可能性があります。
      • スライスが関数から返される、グローバル変数に代入される、または要素へのポインタがエスケープする場合、基底配列はヒープに割り当てられます。
      • 非常に大きな配列は、スタックサイズ制限を超える可能性があるため、ヒープに割り当てられる傾向があります。

3. Goコンパイラの内部表現 (AST Nodes)

Goコンパイラは、ソースコードを解析する際に、抽象構文木(AST: Abstract Syntax Tree)と呼ばれる内部表現を構築します。ASTの各ノードは、プログラムの異なる要素(変数、演算子、関数呼び出しなど)を表します。エスケープ解析は、これらのASTノードを走査して、変数のエスケープを判断します。

このコミットに関連する主なASTノードは以下の通りです。

  • ODOT: 構造体のフィールドアクセスや、ポインタを介したフィールドアクセスを表します。
  • OSLICE: スライス操作(例: a[:])を表します。
  • OSLICEARR: 配列からスライスを作成する操作(例: a[low:high])を表します。
  • OSLICE3: 3つのインデックスを持つスライス操作(例: a[low:high:max])を表します。
  • OSLICE3ARR: 配列から3つのインデックスを持つスライスを作成する操作(例: a[low:high:max])を表します。

エスケープ解析は、これらのノードを適切に処理し、参照される値がエスケープするかどうかを正確に追跡する必要があります。

技術的詳細

このコミットの技術的詳細は、Goコンパイラのsrc/cmd/gc/esc.cファイルにおけるエスケープ解析のロジックの修正にあります。このファイルは、GoプログラムのASTを走査し、各変数のエスケープ挙動を決定する役割を担っています。

問題は、escassign関数とescwalk関数におけるOSLICEARRノードの処理にありました。

escassign関数

escassign関数は、代入操作におけるエスケープ解析を処理します。以前のバージョンでは、OSLICEARRノードがOSLICEOSLICE3などと同じcaseブロックにまとめられていました。このブロックのコメントには「Conversions, field access, slice all preserve the input value.」とあり、入力値のエスケープ挙動をそのまま引き継ぐことを意図していました。

しかし、OSLICEARRは配列からスライスを作成する操作であり、その結果として得られるスライスは、元の配列への参照を持ちます。もしこのスライスがエスケープする場合、その基底となる配列もエスケープする必要があります。以前のコードでは、OSLICEARROSLICEと同じように扱われていたため、配列から作成されたスライスがエスケープしても、その基底配列が正しくヒープに移動されないケースがありました。

修正では、OSLICEARROSLICEOSLICE3などと同じ行に移動され、OSLICEOSLICE3OSLICE3ARRの後に配置されました。これは、コードの論理的なグループ化を改善し、OSLICEARRが他のスライス関連のノードと同様に、入力値のエスケープ挙動を考慮するように意図された変更です。

escwalk関数

escwalk関数は、ASTを再帰的に走査し、各ノードのエスケープ挙動を分析します。以前のバージョンでは、ODOTノードのcaseブロックの後にOSLICEノードがfall through(フォールスルー)として処理されていました。これは、ODOTOSLICEが同様のエスケープ挙動を持つと見なされていたためです。

しかし、OSLICEOSLICEARROSLICE3OSLICE3ARRといったスライス関連のノードは、そのsrc->left(スライスの基底となる式)のエスケープ挙動に依存します。特に、配列からスライスを作成するOSLICEARRの場合、元の配列がエスケープするかどうかの判断が重要です。

修正では、ODOTcaseブロックにOSLICEOSLICEARROSLICE3OSLICE3ARRが追加されました。これにより、これらのスライス関連ノードがODOTと同様に、その左の子(基底となる式)のエスケープ挙動を再帰的にescwalkで分析するようになりました。これにより、配列から作成されたスライスがエスケープする場合に、その基底配列も正しくエスケープされるように、エスケープ解析のロジックが強化されました。

以前のコードでは、OSLICEODOTPTROINDEXMAPOINDと同じfall throughのグループに属していましたが、これはスライスがポインタやマップのインデックス操作とは異なるエスケープ挙動を持つため、誤解を招く可能性がありました。修正により、OSLICEはこのグループから削除され、より適切なODOTグループに移動されました。

これらの変更により、Goコンパイラは配列のスライスに対するエスケープ解析をより正確に行えるようになり、Issue 7931で報告されたような、本来ヒープに割り当てられるべき変数が誤ってスタックに割り当てられる問題を解決しました。

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

diff --git a/src/cmd/gc/esc.c b/src/cmd/gc/esc.c
index 028163abbf..4091682485 100644
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -767,8 +767,8 @@ escassign(EscState *e, Node *dst, Node *src)\n 	case ODOTTYPE:\n 	case ODOTTYPE2:\n 	case OSLICE:\n-\tcase OSLICEARR:\n \tcase OSLICE3:\n+\tcase OSLICEARR:\n \tcase OSLICE3ARR:\n \t\t// Conversions, field access, slice all preserve the input value.\n \t\tescassign(e, dst, src->left);\n@@ -1155,6 +1155,10 @@ escwalk(EscState *e, int level, Node *dst, Node *src)\n \t\tbreak;\n \n \tcase ODOT:\n+\tcase OSLICE:\n+\tcase OSLICEARR:\n+\tcase OSLICE3:\n+\tcase OSLICE3ARR:\n \t\tescwalk(e, level, dst, src->left);\n \t\tbreak;\n \n@@ -1164,7 +1168,6 @@ escwalk(EscState *e, int level, Node *dst, Node *src)\n \t\t\tbreak;\n \t\t}\n \t\t// fall through\n-\tcase OSLICE:\n \tcase ODOTPTR:\n \tcase OINDEXMAP:\n \tcase OIND:\n```

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

### `src/cmd/gc/esc.c` の変更点

#### `escassign` 関数 (行 767-770)

```diff
-	case OSLICEARR:
 	case OSLICE3:
+	case OSLICEARR:
 	case OSLICE3ARR:
  • 変更前: OSLICEARROSLICEの直後に配置されていました。
  • 変更後: OSLICEARROSLICE3の後に移動され、OSLICE3ARRの前に配置されました。
  • 解説: この変更は、主にコードの論理的なグループ化と可読性を改善するためのものです。OSLICEARRは配列からスライスを作成する操作であり、OSLICEOSLICE3OSLICE3ARRといった他のスライス関連のノードと同様に、その入力値(src->left)のエスケープ挙動を継承する必要があります。この移動により、OSLICEARRが他のスライス関連のノードとより一貫した位置に配置され、エスケープ解析がこれらのノードに対して同様の処理を行うことを明確にしています。

escwalk 関数 (行 1155-1160)

 	case ODOT:
+	case OSLICE:
+	case OSLICEARR:
+	case OSLICE3:
+	case OSLICE3ARR:
 	\t\tescwalk(e, level, dst, src->left);
 	\t\tbreak;
  • 変更前: ODOTのみがこのcaseブロックにありました。
  • 変更後: ODOTに加えて、OSLICEOSLICEARROSLICE3OSLICE3ARRが追加されました。
  • 解説: この変更は、エスケープ解析の正確性を大幅に向上させます。これらのノード(ODOTOSLICEOSLICEARROSLICE3OSLICE3ARR)は、その左の子(src->left)が参照する値のエスケープ挙動に依存します。例えば、ODOTは構造体のフィールドアクセスを表し、フィールドがエスケープすれば構造体全体もエスケープする可能性があります。同様に、スライス関連のノードは、その基底となる配列がエスケープするかどうかを正確に判断する必要があります。この修正により、これらのノードがescwalk関数内で再帰的にsrc->leftのエスケープ解析を行うようになり、配列から作成されたスライスがエスケープする場合に、その基底配列も正しくヒープに移動されるようになりました。これがIssue 7931の根本的な解決策です。

escwalk 関数 (行 1164-1168)

 	\t\t// fall through
-	\tcase OSLICE:
 	\tcase ODOTPTR:
 	\tcase OINDEXMAP:
 	\tcase OIND:
  • 変更前: OSLICEODOTPTROINDEXMAPOINDと同じfall throughのグループに属していました。
  • 変更後: OSLICEがこのグループから削除されました。
  • 解説: 以前のコードでは、OSLICEがポインタの逆参照(ODOTPTR)、マップのインデックス操作(OINDEXMAP)、間接参照(OIND)と同じように処理されていました。しかし、スライスはこれらの操作とは異なるエスケープ挙動を持つため、このfall throughは誤解を招く可能性がありました。OSLICEが上記の変更でODOTのグループに移動されたため、この行からの削除は論理的な整合性を保つためのものです。これにより、エスケープ解析が各ノードタイプに対してより正確かつ適切な処理を行うことが保証されます。

これらの変更は、Goコンパイラのエスケープ解析が、特に配列のスライスのような複雑なケースにおいて、より堅牢で正確になるように設計されています。

関連リンク

参考にした情報源リンク