[インデックス 18307] ファイルの概要
このコミットは、Goコンパイラのエスケープ解析(Escape Analysis: EA)における uintptr
型の扱いを修正するものです。具体的には、関数引数として渡される uintptr
型の値が、実際にはポインタを含まないにもかかわらず、エスケープ解析によってポインタとして追跡される問題を解決します。これにより、エスケープ解析の挙動がガベージコレクタ(GC)の挙動と整合し、特に syscall.Syscall
の引数などが誤ってヒープにエスケープすると判断されることがなくなります。
コミット
commit eb592d828924725ea63563052788cebe415c2781
Author: Russ Cox <rsc@golang.org>
Date: Tue Jan 21 13:31:34 2014 -0500
cmd/gc: do not follow uintptr passed as function argument
The escape analysis works by tracing assignment paths from
variables that start with pointer type, or addresses of variables
(addresses are always pointers). It does allow non-pointers
in the path, so that in this code it sees x's value escape into y:
var x *[10]int
y := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(x))+32))
It must allow uintptr in order to see through this kind of
"pointer arithmetic".
It also traces such values if they end up as uintptrs passed to
functions. This used to be important because packages like
encoding/gob passed around uintptrs holding real pointers.
The introduction of precise collection of stacks has forced
code to be more honest about which declared stack variables
hold pointers and which do not. In particular, the garbage
collector no longer sees pointers stored in uintptr variables.
Because of this, packages like encoding/gob have been fixed.
There is not much point in the escape analysis accepting
uintptrs as holding pointers at call boundaries if the garbage
collector does not.
Excluding uintptr-valued arguments brings the escape
analysis in line with the garbage collector and has the
useful side effect of making arguments to syscall.Syscall
not appear to escape.
That is, this CL should yield the same benefits as
CL 45930043 (rolled back in CL 53870043), but it does
so by making uintptrs less special, not more.
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/53940043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eb592d828924725ea63563052788cebe415c2781
元コミット内容
Goコンパイラ (cmd/gc
) のエスケープ解析において、関数引数として渡される uintptr
型の値をポインタとして追跡しないように変更します。
エスケープ解析は、ポインタ型で始まる変数や変数のアドレスからの代入パスを追跡することで機能します。このパスには非ポインタ型も含まれるため、例えば unsafe.Pointer(uintptr(unsafe.Pointer(x))+32)
のようなポインタ演算を通じて x
の値が y
にエスケープするのを検出できます。このような「ポインタ演算」を透過的に見るためには、uintptr
をパスに含める必要があります。
しかし、エスケープ解析は、関数に uintptr
として渡される値も追跡していました。これはかつて、encoding/gob
のようなパッケージが実際のポインタを uintptr
として渡していたため重要でした。
スタックの「正確なコレクション(precise collection of stacks)」が導入されたことで、宣言されたスタック変数がポインタを保持しているか否かについて、コードがより正直になることが求められるようになりました。特に、ガベージコレクタは uintptr
変数に格納されたポインタを認識しなくなりました。このため、encoding/gob
のようなパッケージも修正されました。
ガベージコレクタが uintptr
をポインタとして認識しないのであれば、エスケープ解析が関数呼び出し境界で uintptr
をポインタとして受け入れる意味はほとんどありません。
uintptr
型の引数をエスケープ解析の対象から除外することで、エスケープ解析はガベージコレクタと整合するようになり、syscall.Syscall
の引数がエスケープしないように見えるという有用な副次効果も得られます。
つまり、この変更は、CL 45930043(CL 53870043 でロールバックされたもの)と同じ利点をもたらしますが、uintptr
をより特殊にするのではなく、より特殊でなくすることで実現しています。
変更の背景
Go言語のコンパイラには、プログラムのメモリ割り当てを最適化するための「エスケープ解析(Escape Analysis: EA)」という重要な機能があります。EAは、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかをコンパイル時に決定します。スタック割り当ては高速ですが、関数呼び出しが終了するとメモリが解放されます。一方、ヒープ割り当ては低速ですが、メモリはGCによって管理され、関数呼び出し後も存続できます。
このコミットの背景には、EAとGoのガベージコレクタ(GC)の間での uintptr
型の扱いの不整合がありました。
-
uintptr
の特殊な役割:uintptr
は、ポインタを整数として表現する型です。unsafe.Pointer
と組み合わせることで、Goの型システムを迂回してポインタ演算を行うことが可能になります。EAは、このようなポインタ演算のパスを追跡するために、uintptr
をポインタとして扱う必要がありました。例えば、x
がポインタで、y := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(x))+32))
のようなコードでは、x
の値がy
にエスケープするのを検出するためにuintptr
を経由した追跡が必要でした。 -
過去の
uintptr
の使用: 以前は、encoding/gob
のような一部のパッケージが、実際のポインタをuintptr
として関数に渡すことがありました。このため、EAは関数呼び出しの境界でuintptr
をポインタとして追跡する必要がありました。 -
「precise collection of stacks」の導入とGCの変更: Goのランタイムに「precise collection of stacks」(スタックの正確なコレクション)が導入されたことで、GCの挙動が変化しました。この変更により、GCは
uintptr
型の変数に格納された値をポインタとして認識しなくなりました。これは、uintptr
が純粋な整数型であり、GCが追跡すべきポインタ情報を含まないという原則に沿ったものです。この変更に伴い、encoding/gob
のようなパッケージも、uintptr
を介してポインタを渡すのではなく、より明示的なポインタ型を使用するように修正されました。 -
EAとGCの不整合: GCが
uintptr
をポインタとして認識しなくなったにもかかわらず、EAは依然として関数引数として渡されるuintptr
をポインタとして追跡し続けていました。この不整合により、実際にはヒープにエスケープしないはずの変数が、EAによって誤ってエスケープすると判断される問題が発生しました。特に、syscall.Syscall
の引数は通常uintptr
で渡されますが、これらが誤ってエスケープすると判断されることで、不必要なヒープ割り当てが発生したり、最適化が妨げられたりする可能性がありました。
このコミットは、EAの挙動をGCの挙動と整合させることで、この不整合を解消し、より正確なエスケープ解析と効率的なメモリ管理を実現することを目的としています。
前提知識の解説
Goのエスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラが変数の寿命を分析し、その変数がスタックに割り当てられるべきか、ヒープに割り当てられるべきかを決定する最適化手法です。
- スタック割り当て: 関数内で宣言され、その関数が終了すると同時に寿命が尽きる変数は、通常スタックに割り当てられます。スタックは高速で、メモリの割り当てと解放が非常に効率的です。
- ヒープ割り当て: 変数がその宣言されたスコープを超えて参照される可能性がある場合(例えば、関数の戻り値として返される場合や、グローバル変数に代入される場合など)、その変数はヒープに割り当てられます。ヒープはGCによって管理され、寿命が長いため、割り当てと解放のオーバーヘッドが大きくなります。
EAの目的は、可能な限り多くの変数をスタックに割り当てることで、GCの負荷を減らし、プログラムのパフォーマンスを向上させることです。
Goのガベージコレクタ (Garbage Collector)
GoのGCは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなったものを自動的に解放するシステムです。GoのGCは並行かつ低遅延で動作するように設計されており、プログラムの実行を長時間停止させることなくメモリを回収します。GCが正しく機能するためには、どのメモリ領域がポインタを含んでいるかを正確に把握している必要があります。
uintptr
型と unsafe.Pointer
uintptr
: Goの組み込み型の一つで、ポインタを保持するのに十分な大きさの符号なし整数型です。これは純粋な整数であり、GCはuintptr
の中身をポインタとして認識しません。unsafe.Pointer
: Goのunsafe
パッケージで提供される特殊なポインタ型です。任意の型のポインタとuintptr
の間で相互に変換することができます。これにより、Goの型安全性を一時的にバイパスして、低レベルのメモリ操作(C言語のポインタ演算に似たもの)を行うことが可能になります。*T
(任意の型のポインタ) からunsafe.Pointer
への変換unsafe.Pointer
から*T
への変換uintptr
からunsafe.Pointer
への変換unsafe.Pointer
からuintptr
への変換uintptr(unsafe.Pointer(x))
のようにunsafe.Pointer
を介してuintptr
に変換されたポインタは、GCによって追跡されなくなります。
スタックとヒープ
- スタック (Stack): プログラムの実行中に一時的なデータを格納するために使用されるメモリ領域です。関数呼び出し、ローカル変数、関数の引数などがスタックに割り当てられます。LIFO(Last-In, First-Out)の原則で動作し、高速なアクセスが可能です。関数が終了すると、その関数に割り当てられたスタックフレームは自動的に解放されます。
- ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てるために使用されるメモリ領域です。スタックとは異なり、ヒープに割り当てられたメモリは、その割り当てを行った関数が終了しても存続できます。ヒープメモリはGCによって管理されます。
syscall.Syscall
syscall
パッケージは、Goプログラムからオペレーティングシステムのシステムコールを直接呼び出すための機能を提供します。システムコールは通常、低レベルの操作(ファイルI/O、ネットワーク通信、プロセス管理など)を実行するために使用されます。syscall.Syscall
関数は、引数として uintptr
型の値を複数受け取ることが一般的です。これは、システムコールが様々な型の引数を取るため、汎用的な整数型で表現する必要があるためです。
cmd/gc/esc.c
cmd/gc
はGoコンパイラのバックエンドの一部であり、esc.c
はその中でエスケープ解析のロジックを実装しているC言語のソースファイルです。GoコンパイラはGo言語で書かれていますが、一部の低レベルな部分やパフォーマンスが重要な部分はC言語で書かれています。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのエスケープ解析ロジックが記述されている src/cmd/gc/esc.c
ファイル内の esccall
関数に対する変更です。
esccall
関数は、Goプログラム内の関数呼び出しのエスケープ解析を担当します。この関数は、呼び出しのレシーバ(メソッド呼び出しの場合)と引数を分析し、それらの値がヒープにエスケープするかどうかを判断します。
変更前は、esccall
関数内でレシーバと各引数に対して無条件に escassignfromtag
関数が呼び出されていました。escassignfromtag
は、エスケープ解析の主要なヘルパー関数であり、ポインタの割り当てパスを追跡し、変数のエスケープ状態を更新します。
コミットによって導入された変更は、この escassignfromtag
の呼び出しに条件を追加することです。具体的には、haspointers(t->type)
というチェックが追加されました。
haspointers(t->type)
: この関数は、与えられた型t->type
が実際にポインタを含む型であるかどうかを判定します。例えば、*int
や[]byte
(スライスは内部的にポインタを持つ)のような型はtrue
を返しますが、int
やuintptr
のような純粋な値型はfalse
を返します。
変更後、escassignfromtag
は、レシーバまたは引数の型が haspointers
関数によってポインタを含むと判断された場合にのみ呼び出されるようになりました。
この変更がもたらす影響:
uintptr
型の引数の無視:uintptr
型はポインタを含まないため、haspointers(uintptr)
はfalse
を返します。これにより、関数引数として渡されるuintptr
型の値は、エスケープ解析のポインタ追跡の対象から完全に除外されます。- GCとの整合性: ガベージコレクタが
uintptr
変数内のポインタを追跡しないという事実と、エスケープ解析の挙動が一致するようになります。これにより、EAとGCの間の不整合が解消されます。 syscall.Syscall
の最適化:syscall.Syscall
の引数は通常uintptr
で渡されます。この変更により、これらの引数が誤ってヒープにエスケープすると判断されることがなくなり、不必要なヒープ割り当てが回避され、より効率的なコードが生成されるようになります。uintptr
の「特殊性」の低減: コミットメッセージにあるように、この変更はuintptr
を「より特殊にする」のではなく、「より特殊でなくする」方向への一歩です。つまり、uintptr
はその本来の目的(ポインタを整数として扱う)に忠実になり、エスケープ解析がその内部にポインタが隠されていると仮定して追跡することはなくなります。
この変更は、Goのコンパイラとランタイムの間のより深い理解と整合性を示すものであり、メモリ管理の正確性と効率性を向上させるための重要なステップです。
コアとなるコードの変更箇所
変更は src/cmd/gc/esc.c
ファイルの esccall
関数内で行われました。
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -916,8 +916,12 @@ esccall(EscState *e, Node *n)
// print("esc analyzed fn: %#N (%+T) returning (%+H)\n", fn, fntype, n->escretval);
// Receiver.
- if(n->op != OCALLFUNC)
- escassignfromtag(e, getthisx(fntype)->type->note, n->escretval, n->left->left);
+ if(n->op != OCALLFUNC) {
+ t = getthisx(fntype)->type;
+ src = n->left->left;
+ if(haspointers(t->type))
+ escassignfromtag(e, t->note, n->escretval, src);
+ }
for(t=getinargx(fntype)->type; ll; ll=ll->next) {
src = ll->n;
@@ -930,7 +934,8 @@ esccall(EscState *e, Node *n)
// e->noesc = list(e->noesc, src);
// n->right = src;
//}
- escassignfromtag(e, t->note, n->escretval, src);
+ if(haspointers(t->type))
+ escassignfromtag(e, t->note, n->escretval, src);
if(src != ll->n)
break;
t = t->down;
コアとなるコードの解説
上記のdiffには、esccall
関数内の2つの主要な変更点が含まれています。
-
レシーバの処理 (
if(n->op != OCALLFUNC)
ブロック内):- 変更前:
ここでは、メソッド呼び出しのレシーバ(if(n->op != OCALLFUNC) escassignfromtag(e, getthisx(fntype)->type->note, n->escretval, n->left->left);
n->left->left
)に対して、その型に関わらず無条件にescassignfromtag
が呼び出されていました。getthisx(fntype)->type->note
はレシーバの型情報に関連するエスケープ解析のタグ(注釈)を取得します。 - 変更後:
新しいコードでは、まずレシーバの型 (if(n->op != OCALLFUNC) { t = getthisx(fntype)->type; // レシーバの型を取得 src = n->left->left; // レシーバのノードを取得 if(haspointers(t->type)) // レシーバの型がポインタを含む場合のみ escassignfromtag(e, t->note, n->escretval, src); }
t
) とソースノード (src
) を取得します。そして、if(haspointers(t->type))
という条件が追加されました。これにより、レシーバの型が実際にポインタを含む場合にのみescassignfromtag
が呼び出されるようになります。もしレシーバがuintptr
のようなポインタを含まない型であれば、この呼び出しはスキップされます。
- 変更前:
-
引数の処理 (
for
ループ内):- 変更前:
関数呼び出しの各引数 (escassignfromtag(e, t->note, n->escretval, src);
src
) に対して、その型 (t
) に関わらず無条件にescassignfromtag
が呼び出されていました。 - 変更後:
同様に、引数の処理においてもif(haspointers(t->type)) // 引数の型がポインタを含む場合のみ escassignfromtag(e, t->note, n->escretval, src);
if(haspointers(t->type))
という条件が追加されました。これにより、引数の型がポインタを含む場合にのみescassignfromtag
が呼び出されます。uintptr
型の引数はhaspointers
がfalse
を返すため、エスケープ解析のポインタ追跡の対象から外れることになります。
- 変更前:
これらの変更により、エスケープ解析は、ポインタを含まない型(特に uintptr
)のレシーバや引数に対しては、不必要にポインタの追跡を行わなくなります。これは、ガベージコレクタが uintptr
の中身をポインタとして認識しないという事実と整合し、エスケープ解析の精度と効率を向上させます。結果として、syscall.Syscall
の引数のようなケースで誤ったエスケープ判定が減り、より最適なコード生成に貢献します。
関連リンク
- このコミットのGitHubページ: https://github.com/golang/go/commit/eb592d828924725ea63563052788cebe415c2781
- このコミットのGerrit CL: https://golang.org/cl/53940043
- 関連するロールバックされたCL: https://golang.org/cl/45930043
- 関連するロールバックのCL: https://golang.org/cl/53870043
参考にした情報源リンク
- Go Escape Analysis: https://go.dev/doc/articles/go_mem (Go公式ドキュメントのメモリ管理に関する記事)
- The Go Programming Language Specification - Numeric types: https://go.dev/ref/spec#Numeric_types (
uintptr
の定義) - The Go Programming Language Specification - Unsafe Pointer: https://go.dev/ref/spec#UnsafePointer (
unsafe.Pointer
の定義) - Go's Garbage Collector: https://go.dev/doc/gc-guide (Go公式ドキュメントのGCに関するガイド)
- syscall package documentation: https://pkg.go.dev/syscall (syscallパッケージのドキュメント)
- Go source code (src/cmd/gc/esc.c): https://github.com/golang/go/blob/master/src/cmd/gc/esc.c (Goコンパイラのエスケープ解析ソースコード)
- Go's precise stack collection (関連する概念の議論): https://go.dev/blog/go1.4-gc (Go 1.4のGCに関するブログ記事、"precise stack collection"の背景にある概念に触れている可能性)
- Go issue tracker (関連する議論やバグ報告): https://github.com/golang/go/issues (具体的なissue番号は不明だが、関連する議論が見つかる可能性)
- Go mailing lists (golang-dev, golang-nuts): https://groups.google.com/g/golang-dev (過去の議論を検索する際に有用)
- Go's escape analysis in depth: https://medium.com/a-journey-with-go/go-escape-analysis-in-depth-a048b239a06b (エスケープ解析に関する詳細な解説記事)