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

[インデックス 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のメモリ管理のルールを破っている)ことを意味します。

このコミットは、システムコールの引数が「エスケープしない」ことを明示的にコンパイラに伝えることで、以下の目的を達成しようとしています。

  1. ムービングGCへの対応準備: 将来ムービングGCが導入された際に、システムコールに渡されたヒープ上のオブジェクトが移動しても問題が発生しないように、コンパイラにその事実を認識させる。
  2. メモリリークの削減: エスケープ解析がより正確になることで、不要なヒープ割り当てや、システムコール引数として渡されたポインタがGCによって適切に回収されない「リーク」の可能性を減らす。コミットメッセージにある「Reduces number of leaked params from 125 to 36 on linux」という記述は、この最適化の効果を示しています。
  3. コードの健全性の保証: ユーザーがシステムコールに渡したオブジェクトが、システムコール完了後も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. uintptrunsafe.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が回収しないようにする必要があります。

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

このコミットで変更された主要なファイルは以下の通りです。

  1. src/cmd/gc/esc.c: Goコンパイラのエスケープ解析ロジックが含まれるファイル。

    • esctag関数内のロジックが変更されました。
    • 以前はfunc->noescapeが真の場合、haspointers(t->type)が真である引数のみがEscNoneとマークされていました。
    • 変更後、haspointers(t->type)のチェックが削除され、noescapeが指定された関数のすべての引数(ポインタ型でなくても)がEscNoneとマークされるようになりました。
  2. src/pkg/syscall/dll_windows.go: Windows固有のシステムコール定義ファイル。

    • Syscall, Syscall6, Syscall9, Syscall12, Syscall15関数の直前に//go:noescapeディレクティブが追加されました。
  3. src/pkg/syscall/syscall_linux_386.go: Linux/386アーキテクチャ固有のシステムコール定義ファイル。

    • socketcall, rawsocketcall関数の直前に//go:noescapeディレクティブが追加されました。
  4. src/pkg/syscall/syscall_plan9.go: Plan 9固有のシステムコール定義ファイル。

    • Syscall, Syscall6, RawSyscall, RawSyscall6関数の直前に//go:noescapeディレクティブが追加されました。
  5. 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パッケージのファイルにおいて、SyscallRawSyscallなどのシステムコールを直接呼び出す関数定義の直前に//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.csrc/pkg/syscallディレクトリ)
  • Go言語のコミット履歴と関連するコードレビュー(Go CL 45930043)
  • Go言語のエスケープ解析やGCに関する一般的な技術記事や解説
  • Go言語の//go:noescapeディレクティブに関する情報