[インデックス 18280] ファイルの概要
このコミットは、Go言語のランタイムとsyscall
パッケージにおける重要な変更を導入しています。具体的には、システムコール(Syscall)の引数にnoescape
ディレクティブを適用することで、エスケープ解析の挙動を最適化し、将来的なムービングGC(Garbage Collector)への対応を見据えた改善を行っています。これにより、ヒープオブジェクトがシステムコール実行後にOSによってアクセスされることを防ぎ、メモリリークの可能性を低減しています。
コミット
commit fc37eba149e134eba6ccd0debf283f9d7af635e1
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Jan 17 20:18:37 2014 +0400
syscall: mark arguments to Syscall as noescape
Heap arguments to "async" syscalls will break when/if we have moving GC anyway.
With this change is must not break until moving GC, because a user must
reference the object in Go to preserve liveness. Otherwise the code is broken already.
Reduces number of leaked params from 125 to 36 on linux.
R=golang-codereviews, mikioh.mikioh, bradfitz
CC=cshapiro, golang-codereviews, khr, rsc
https://golang.org/cl/45930043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fc37eba149e134eba6ccd0debf283f9d7af635e1
元コミット内容
このコミットの元の内容は、syscall
パッケージ内のシステムコール関数(Syscall
, Syscall6
など)の引数に//go:noescape
ディレクティブを追加すること、およびGoコンパイラのesc.c
(エスケープ解析のコード)を修正し、noescape
が指定された関数の引数すべて(ポインタだけでなく)をエスケープしないものとして扱うようにすることです。これにより、システムコールに渡されるヒープ上のデータが、システムコール完了後にOSによってアクセスされることを防ぎ、将来のムービングGCとの互換性を確保します。
変更の背景
この変更の背景には、Go言語のメモリ管理とガベージコレクション(GC)の進化があります。当時のGoのGCは、オブジェクトを移動させない(non-moving)GCでしたが、将来的にムービングGCを導入する計画がありました。ムービングGCでは、ヒープ上のオブジェクトがメモリ内で移動する可能性があります。
システムコールは、GoのランタイムからOSのカーネルに処理を委譲するメカニズムです。Goのコードからシステムコールを呼び出す際、Goのヒープ上に確保されたデータへのポインタをOSに渡すことがあります。もし、システムコールが完了した後もOSがそのポインタを保持し続け、GoのGCがそのオブジェクトを移動させてしまった場合、OSが不正なメモリ領域にアクセスしようとしてクラッシュする可能性があります。
この問題を防ぐため、Goのコンパイラは「エスケープ解析」という最適化を行います。エスケープ解析は、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。通常、関数から戻った後も参照され続ける変数はヒープにエスケープします。しかし、システムコールの引数として渡されるポインタは、システムコールが完了すればOSはそれ以上参照しないはずです。もし参照し続けるのであれば、それはGoのコードが既に壊れている(OSがGoのメモリ管理のルールを破っている)ことを意味します。
このコミットは、システムコールの引数が「エスケープしない」ことを明示的にコンパイラに伝えることで、以下の目的を達成しようとしています。
- ムービングGCへの対応準備: 将来ムービングGCが導入された際に、システムコールに渡されたヒープ上のオブジェクトが移動しても問題が発生しないように、コンパイラにその事実を認識させる。
- メモリリークの削減: エスケープ解析がより正確になることで、不要なヒープ割り当てや、システムコール引数として渡されたポインタがGCによって適切に回収されない「リーク」の可能性を減らす。コミットメッセージにある「Reduces number of leaked params from 125 to 36 on linux」という記述は、この最適化の効果を示しています。
- コードの健全性の保証: ユーザーがシステムコールに渡したオブジェクトが、システムコール完了後もGoのコード内で参照され続けることで、そのオブジェクトの生存期間が保証されるべきであるという前提を強化します。もしGoのコードが参照を保持しないのにOSが参照し続ける場合、それはGoのコードのバグであると見なされます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部動作に関する知識が必要です。
1. エスケープ解析 (Escape Analysis)
Goコンパイラの重要な最適化の一つです。変数がメモリのどこに割り当てられるべきかを決定します。
- スタック割り当て: 関数内で宣言され、その関数が終了すると不要になる変数は、通常スタックに割り当てられます。スタックは高速で、GCの対象外です。
- ヒープ割り当て: 関数から戻った後も参照され続ける可能性がある変数(例: ポインタが関数の外に返される場合、グローバル変数に代入される場合など)は、ヒープに割り当てられます。ヒープはGCの対象であり、割り当てと解放にはオーバーヘッドが伴います。
エスケープ解析は、プログラムのパフォーマンスに大きな影響を与えます。不要なヒープ割り当てを減らすことで、GCの負荷を軽減し、実行速度を向上させます。
2. ガベージコレクション (Garbage Collection, GC)
Goは自動メモリ管理を採用しており、不要になったメモリ領域を自動的に解放するGCを備えています。
- Non-moving GC: Goの初期のGCは、ヒープ上のオブジェクトをメモリ内で移動させないタイプでした。オブジェクトが一度割り当てられると、そのアドレスは固定されます。
- Moving GC: 将来的に導入が検討されていたGCのタイプで、ヒープ上のオブジェクトをメモリ内で移動させることがあります。これにより、メモリの断片化を解消し、より効率的なメモリ利用が可能になります。しかし、オブジェクトが移動すると、そのオブジェクトを指すポインタも更新される必要があります。OSがGoのヒープ上のポインタを保持している場合、GoのGCがそのポインタを更新できないため問題が生じます。
3. システムコール (Syscall)
GoプログラムがOSの機能(ファイルI/O、ネットワーク通信、プロセス管理など)を利用するためのインターフェースです。Goのsyscall
パッケージを通じて提供されます。システムコールを呼び出す際、Goのメモリ上のデータ(バッファなど)へのポインタをOSに渡すことがあります。
4. //go:noescape
ディレクティブ
Goコンパイラに対する指示(プラグマ)の一つです。関数定義の直前に記述することで、その関数の引数がヒープにエスケープしないことをコンパイラに明示的に伝えます。これは、コンパイラがエスケープ解析を行う際のヒントとして機能します。通常、このディレクティブは、引数が関数内でローカルにのみ使用され、関数が戻った後もその引数が参照され続けることがない場合に適用されます。システムコールの場合、OSが引数を一時的に使用するだけで、Goのランタイムがその引数の生存期間を管理するという意図をコンパイラに伝えます。
5. uintptr
と unsafe.Pointer
uintptr
: 整数型であり、ポインタの値を保持できますが、ポインタ演算はできません。GCの対象外です。unsafe.Pointer
: 任意の型のポインタを保持できる特殊なポインタ型です。uintptr
との間で相互変換が可能であり、ポインタ演算も可能です。unsafe
パッケージを使用するため、Goの型安全性をバイパスします。システムコールでは、Goの型システムから独立してOSにメモリを渡すために、unsafe.Pointer
を介してuintptr
に変換されることがよくあります。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのエスケープ解析とsyscall
パッケージの連携にあります。
Goのシステムコール関数(例: syscall.Syscall
)は、通常、アセンブリ言語で実装されており、GoのランタイムとOSカーネルの間のブリッジとして機能します。これらの関数は、Goのヒープ上のデータをOSに渡すために、uintptr
型の引数を受け取ります。Goのコードでは、unsafe.Pointer
を介してGoのポインタをuintptr
に変換してシステムコールに渡すことが一般的です。
//go:noescape
ディレクティブをシステムコール関数に適用することで、コンパイラはこれらの関数の引数が、関数呼び出しの期間中のみ有効であり、関数が戻った後もヒープにエスケープして参照され続けることはない、と判断します。これは、将来のムービングGCがヒープ上のオブジェクトを移動させても、システムコールに渡されたポインタがOSによって不正にアクセスされることを防ぐための重要なステップです。
src/cmd/gc/esc.c
の変更は、このnoescape
ディレクティブの解釈を強化しています。以前は、noescape
が指定された関数の引数のうち、ポインタ型を持つものだけがEscNone
(エスケープしない)とマークされていました。しかし、syscall
パッケージでは、ポインタがuintptr
に変換されて渡されるため、Goの型システム上はポインタ型ではありません。この変更により、noescape
が指定された関数のすべての引数(ポインタ型であろうとなかろうと)がEscNone
とマークされるようになります。これにより、uintptr
に変換された元のポインタも、エスケープ解析によって適切に「エスケープしない」と判断されるようになります。
この結果、コンパイラはシステムコールに渡されるヒープ上のオブジェクトが、システムコール完了後もGoのランタイムによって管理されるべきであり、OSがその生存期間を保証する責任はないと理解します。もしユーザーがシステムコールに渡したオブジェクトをシステムコール完了後もGoのコード内で参照したい場合、ユーザー自身がそのオブジェクトへの参照を保持し、GCが回収しないようにする必要があります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の通りです。
-
src/cmd/gc/esc.c
: Goコンパイラのエスケープ解析ロジックが含まれるファイル。esctag
関数内のロジックが変更されました。- 以前は
func->noescape
が真の場合、haspointers(t->type)
が真である引数のみがEscNone
とマークされていました。 - 変更後、
haspointers(t->type)
のチェックが削除され、noescape
が指定された関数のすべての引数(ポインタ型でなくても)がEscNone
とマークされるようになりました。
-
src/pkg/syscall/dll_windows.go
: Windows固有のシステムコール定義ファイル。Syscall
,Syscall6
,Syscall9
,Syscall12
,Syscall15
関数の直前に//go:noescape
ディレクティブが追加されました。
-
src/pkg/syscall/syscall_linux_386.go
: Linux/386アーキテクチャ固有のシステムコール定義ファイル。socketcall
,rawsocketcall
関数の直前に//go:noescape
ディレクティブが追加されました。
-
src/pkg/syscall/syscall_plan9.go
: Plan 9固有のシステムコール定義ファイル。Syscall
,Syscall6
,RawSyscall
,RawSyscall6
関数の直前に//go:noescape
ディレクティブが追加されました。
-
src/pkg/syscall/syscall_unix.go
: Unix系OS(Linux, macOSなど)共通のシステムコール定義ファイル。Syscall
,Syscall6
,RawSyscall
,RawSyscall6
関数の直前に//go:noescape
ディレクティブが追加されました。
コアとなるコードの解説
src/cmd/gc/esc.c
の変更点
// 変更前
- if(haspointers(t->type))
- t->note = mktag(EscNone);
// 変更後
+ // Mark all arguments, not only pointers,
+ // to support the following use case.
+ // Syscall package converts all pointers to uintptr
+ // when calls asm-implemented Syscall function:
+ //
+ // Syscall(SYS_FOO, uintptr(unsafe.Pointer(p)), 0, 0)
+ t->note = mktag(EscNone);
この変更は、esctag
関数内でnoescape
ディレクティブが指定された関数(func->noescape
が真)の引数に対するエスケープ解析の挙動を修正しています。
変更前は、引数t
がポインタ型(haspointers(t->type)
が真)である場合にのみ、その引数がエスケープしない(EscNone
)とマークされていました。
しかし、syscall
パッケージでは、Goのポインタがuintptr(unsafe.Pointer(p))
のようにuintptr
に変換されてシステムコールに渡されます。このuintptr
はGoの型システム上はポインタ型ではないため、変更前のロジックではEscNone
とマークされませんでした。
変更後、haspointers(t->type)
のチェックが削除され、noescape
が指定された関数のすべての引数が無条件にEscNone
とマークされるようになりました。これにより、uintptr
に変換された引数も正しくエスケープしないと判断され、エスケープ解析の精度が向上します。
src/pkg/syscall/*.go
ファイル群の変更点
// 追加されたコメントとディレクティブの例 (syscall_windows.goより抜粋)
// Pointers passed to syscalls must not escape (be accessed by OS after the syscall returns).
// For heap objects this will break when/if we have moving GC.
// And for other objects (global, C allocated) go:noescape has no effect.
//go:noescape
func Syscall(trap, nargs, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
各OS固有のsyscall
パッケージのファイルにおいて、Syscall
やRawSyscall
などのシステムコールを直接呼び出す関数定義の直前に//go:noescape
ディレクティブが追加されました。
このディレクティブは、Goコンパイラに対して、これらの関数の引数(trap
, nargs
, a1
, a2
, a3
など)が、関数呼び出しの期間中のみ有効であり、関数が戻った後もヒープにエスケープして参照され続けることはないことを明示的に伝えます。
追加されたコメントは、このディレクティブの目的を明確に説明しています。
- システムコールに渡されるポインタは、システムコールが戻った後もOSによってアクセスされるべきではない。
- ヒープ上のオブジェクトの場合、これは将来ムービングGCが導入された際に問題を引き起こす可能性がある。
- グローバル変数やC言語で割り当てられたオブジェクトなど、他の種類のオブジェクトに対しては
noescape
は効果がない(それらはGoのGCの管理外であるため)。
この変更により、Goコンパイラはシステムコールに渡される引数についてより正確なエスケープ解析を行うことができ、将来のGCの変更に対する互換性を確保し、不要なヒープ割り当てやメモリリークの可能性を低減します。
関連リンク
- Go言語のエスケープ解析に関する公式ドキュメントやブログ記事(当時の情報源を探すのは難しいかもしれませんが、一般的な概念は共通です)
- Go言語のガベージコレクションに関するドキュメント
- Go言語の
syscall
パッケージのドキュメント
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc/esc.c
とsrc/pkg/syscall
ディレクトリ) - Go言語のコミット履歴と関連するコードレビュー(Go CL 45930043)
- Go言語のエスケープ解析やGCに関する一般的な技術記事や解説
- Go言語の
//go:noescape
ディレクティブに関する情報