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

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

このコミットは、以前のコミット (CL 45930043 / c22889382a17) を元に戻すものです。具体的には、Goコンパイラのsrc/cmd/gc/esc.cにおけるエスケープ解析の変更と、src/pkg/syscallパッケージ内の//go:noescapeディレクティブの追加を削除しています。元の変更は、システムコール引数のエスケープを抑制し、メモリリークを減らすことを目的としていましたが、このコミットではそのアプローチが「醜いハック」であると判断され、より良い方法が模索されることになりました。

コミット

commit 5c9585953f5bfb9b783d9b47eb17cea890664b76
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 17 16:58:14 2014 -0500

    undo CL 45930043 / c22889382a17
    
    The compiler change is an ugly hack.
    We can do better.
    
    ««« original CL description
    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
    »»»
    
    R=golang-codereviews, r
    CC=bradfitz, dvyukov, golang-codereviews
    https://golang.org/cl/53870043

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

https://github.com/golang/go/commit/5c9585953f5bfb9b783d9b47eb17cea890664b76

元コミット内容

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.

変更の背景

このコミットの背景には、Go言語のガベージコレクション(GC)とシステムコール(syscall)の引数管理に関する課題がありました。元のコミット(CL 45930043)は、システムコールに渡される引数がヒープにエスケープするのを防ぐために、//go:noescapeディレクティブを使用し、コンパイラのエスケープ解析ロジックを変更しました。これにより、Linux環境での「リークするパラメータ」の数を125から36に削減できるとされていました。

しかし、このコミットの作者であるRuss Coxは、この変更を「醜いハック(ugly hack)」と評価し、より洗練された解決策が必要であると考えました。その主な理由は、//go:noescapeがコンパイラに対する強力なヒントであり、誤用するとメモリ破損を引き起こす可能性があること、そして将来的にGoが「移動型GC(moving GC)」を導入した場合に、ヒープ上のオブジェクトが移動することでシステムコール引数の扱いが複雑になるという懸念があったためです。

元のコミットは、ユーザーがGoのオブジェクトを参照し続けることで、そのオブジェクトの生存期間を保証する必要があるという前提に立っていました。しかし、これはプログラマに負担をかける可能性があり、また、コンパイラレベルでの「ハック」は、長期的なGoの設計思想に合致しないと判断されたのでしょう。このコミットは、一時的な解決策ではなく、より根本的で堅牢なアプローチを模索するための撤回措置として行われました。

前提知識の解説

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

Goコンパイラは、プログラムのコンパイル時に「エスケープ解析」と呼ばれる最適化手法を実行します。これは、変数がその宣言された関数スコープを超えて「エスケープ」するかどうかを判断するプロセスです。

  • スタック割り当て: 変数が関数スコープ内で完結し、外部に参照されない場合(エスケープしない場合)、その変数はスタックに割り当てられます。スタック割り当ては非常に高速で、ガベージコレクションの対象とならないため、パフォーマンス上の利点があります。
  • ヒープ割り当て: 変数が関数スコープを超えて参照される可能性がある場合(例えば、関数の戻り値として返される、グローバル変数に格納される、チャネルを通じて他のゴルーチンに渡されるなど)、その変数はヒープに割り当てられます。ヒープに割り当てられたオブジェクトは、ガベージコレクションの対象となり、GCのオーバーヘッドが発生します。

エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ヒープ割り当てを減らし、GCの負荷を軽減することです。

2. //go:noescape ディレクティブ

//go:noescapeは、Goコンパイラに対する特殊なディレクティブ(指示子)です。これは、主にアセンブリ言語で実装された関数や、Goランタイムの低レベルな部分で使用されます。このディレクティブが関数宣言の直前に記述されると、コンパイラに対して「この関数のポインタ引数は、関数が戻った後にヒープにエスケープしない」というヒントを与えます。

つまり、//go:noescapeが付与された関数の引数として渡されたポインタは、その関数内でのみ有効であり、関数が終了した後は参照されないことをコンパイラに約束します。これにより、コンパイラはこれらの引数をヒープに割り当てる必要がないと判断し、スタックに割り当てることができます。

しかし、このディレクティブはコンパイラへの「約束」であるため、もし実際には引数がエスケープしてしまうようなコードであった場合、コンパイラはそれを検出できず、メモリ破損や未定義の動作を引き起こす可能性があります。そのため、非常に慎重な使用が求められます。

3. 移動型GC (Moving GC) と非移動型GC (Non-Moving GC)

ガベージコレクションには、大きく分けて「移動型」と「非移動型」の2種類があります。

  • 非移動型GC (Non-Moving GC): Go言語の現在のGCは、基本的に非移動型マーク&スイープGCです。これは、メモリ上のオブジェクトを移動させずに、到達可能なオブジェクトをマークし、到達不能なオブジェクトが占めていたメモリを解放します。この方式では、オブジェクトのアドレスはGCの実行中も変化しません。
  • 移動型GC (Moving GC): 一部のGCシステムでは、メモリの断片化を解消したり、キャッシュ効率を向上させたりするために、GCの実行中にヒープ上のオブジェクトを移動させることがあります。オブジェクトが移動すると、そのアドレスが変更されるため、そのオブジェクトを指すすべてのポインタを更新する必要があります。

元のコミットの背景には、「将来的にGoが移動型GCを導入した場合に、システムコールに渡されたヒープ上の引数が問題になる」という懸念がありました。非移動型GCであれば、システムコール中にオブジェクトが移動することはないため問題になりにくいですが、移動型GCでは、システムコールが実行されている最中にGCがオブジェクトを移動させてしまうと、システムコールが不正なメモリアドレスを参照してしまう可能性があります。//go:noescapeは、この問題を回避するために、引数をヒープに置かない(スタックに置く)ことで、移動型GCの影響を受けないようにしようとした試みの一つでした。

4. システムコール (Syscall)

システムコールは、ユーザー空間のプログラムがオペレーティングシステム(OS)のカーネル空間の機能を利用するためのインターフェースです。ファイルI/O、ネットワーク通信、プロセス管理など、OSが提供する多くの機能はシステムコールを通じてアクセスされます。Go言語のsyscallパッケージは、これらのシステムコールをGoプログラムから呼び出すための機能を提供します。システムコールに渡される引数は、OSカーネルが直接アクセスするため、メモリの配置や生存期間について特別な注意が必要です。

技術的詳細

このコミットは、Goコンパイラのエスケープ解析とsyscallパッケージにおける//go:noescapeディレクティブの利用に関する設計上の問題を浮き彫りにしています。

元のコミット(CL 45930043)では、以下の2つの主要な変更が行われていました。

  1. src/cmd/gc/esc.cにおけるエスケープ解析の変更: esctag関数内で、func->noescapeが設定されている場合(つまり、//go:noescapeディレクティブが付与された関数である場合)、その関数のすべての入力引数(ポインタ型であるかどうかにかかわらず)をEscNone(エスケープしない)としてマークしていました。これは、システムコールがuintptr(unsafe.Pointer(p))のようにポインタをuintptrに変換して渡す場合でも、元のGoのオブジェクトがGCによって回収されないようにするための措置でした。元のコメントには「Syscall package converts all pointers to uintptr when calls asm-implemented Syscall function」とあり、この変換によってGoの型システムからポインタの追跡が失われるため、コンパイラが明示的にエスケープしないと判断する必要があったことを示唆しています。

  2. src/pkg/syscallパッケージにおける//go:noescapeディレクティブの追加: Syscall, Syscall6, RawSyscallなどのシステムコール関数に//go:noescapeディレクティブが追加されていました。これにより、これらの関数の引数がヒープにエスケープしないことをコンパイラに保証し、スタックに割り当てられるように意図されていました。これは、特に「async」なシステムコール(非同期的に実行され、Goの関数が戻った後もOSが引数にアクセスし続ける可能性があるもの)において、将来の移動型GCとの互換性を確保するための試みでした。

しかし、Russ Coxがこの変更を「醜いハック」と表現した理由は、以下の点が考えられます。

  • //go:noescapeの誤用リスク: //go:noescapeは、コンパイラに「この関数は引数をエスケープさせない」と強制するものであり、もし実際の動作が異なれば、メモリ破損を引き起こします。システムコールはOSカーネルと直接やり取りするため、Goランタイムが完全に制御できない外部の動作に依存する部分があり、このような強力なディレクティブを安易に使うことは危険です。
  • コンパイラロジックの複雑化: esc.cにおけるエスケープ解析の変更は、ポインタ型でない引数もEscNoneとマークするという、一般的なエスケープ解析の原則から逸脱した特殊なロジックでした。このような特殊なケースをコンパイラに組み込むことは、コンパイラの保守性を低下させ、将来的な最適化の妨げになる可能性があります。
  • 移動型GCへの対応の不十分さ: 元のコミットは移動型GCへの対応を謳っていましたが、//go:noescapeに頼るだけでは根本的な解決にはなりません。移動型GCが導入された場合、システムコール中にGoのオブジェクトが移動する可能性があり、その際にOSが古いアドレスを参照しないようにするための、より洗練されたメカニズム(例えば、GCがオブジェクトを移動する際にポインタを更新する、またはシステムコール中にGCを一時停止するなど)が必要になります。//go:noescapeは、あくまでGoコンパイラのエスケープ解析を最適化するものであり、OSとのインタラクションにおけるメモリ管理の複雑さを完全に解決するものではありません。
  • 「リークするパラメータ」の根本原因: 「リークするパラメータ」の問題は、GoのGCがシステムコールに渡されたGoのオブジェクトの生存期間を正確に追跡できないことに起因します。//go:noescapeは、この問題を回避するために、オブジェクトをヒープに置かないというアプローチを取りましたが、これは問題の根本的な解決ではなく、症状を抑えるための対症療法に過ぎませんでした。より良い解決策は、GCがシステムコール引数の生存期間を適切に管理できるような、ランタイムレベルでのサポートを強化することであると考えられます。

このコミットは、Goのランタイムとコンパイラの設計において、短期的なパフォーマンス最適化と長期的な堅牢性・保守性のバランスをどのように取るかという、重要な議論の一端を示しています。

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

このコミットでは、以下の5つのファイルが変更されています。

  1. src/cmd/gc/esc.c
  2. src/pkg/syscall/dll_windows.go
  3. src/pkg/syscall/syscall_linux_386.go
  4. src/pkg/syscall/syscall_plan9.go
  5. src/pkg/syscall/syscall_unix.go

変更の概要は以下の通りです。

  • src/cmd/gc/esc.c: エスケープ解析ロジックから、//go:noescapeが付与された関数のすべての引数をEscNoneとマークする特殊な処理を削除し、ポインタを持つ引数のみを対象とする元のロジックに戻しています。
  • src/pkg/syscall/dll_windows.go, src/pkg/syscall/syscall_linux_386.go, src/pkg/syscall/syscall_plan9.go, src/pkg/syscall/syscall_unix.go: これらのファイルから、SyscallSyscall6RawSyscallなどのシステムコール関数に付与されていた//go:noescapeディレクティブと、それに関連するコメントを削除しています。

コアとなるコードの解説

src/cmd/gc/esc.c

--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -1135,13 +1135,8 @@ esctag(EscState *e, Node *func)
 		if(func->nbody == nil) {
 			if(func->noescape) {
 				for(t=getinargx(func->type)->type; t; t=t->down)
-					// 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);
+					if(haspointers(t->type))
+						t->note = mktag(EscNone);
 			}
 			return;
 		}

この変更は、Goコンパイラのエスケープ解析ロジックの一部であるesctag関数にあります。 元のコードでは、func->noescape(つまり、//go:noescapeディレクティブが付与された関数)の場合、getinargx(func->type)->typeで取得される関数のすべての入力引数tに対して、t->note = mktag(EscNone);を実行していました。これは、引数がポインタ型であるかどうかにかかわらず、すべてを「エスケープしない」とマークするものでした。コメントには、syscallパッケージがポインタをuintptrに変換してシステムコールを呼び出すケースをサポートするためと説明されていました。

このコミットでは、この行がif(haspointers(t->type)) t->note = mktag(EscNone);に変更されています。 これにより、//go:noescapeが付与された関数であっても、エスケープしないとマークされるのはポインタを持つ型(haspointers(t->type)が真となる型)の引数のみに戻されました。これは、エスケープ解析の一般的な原則に沿ったものであり、ポインタを持たない値型が不必要にEscNoneとマークされるのを防ぎます。元の「醜いハック」とされた部分が、このコンパイラ側の特殊な処理であったことが示唆されます。

src/pkg/syscall/dll_windows.go, src/pkg/syscall/syscall_linux_386.go, src/pkg/syscall/syscall_plan9.go, src/pkg/syscall/syscall_unix.go

これらのファイルでは、SyscallSyscall6Syscall9Syscall12Syscall15socketcallrawsocketcallRawSyscallRawSyscall6といったシステムコール関数の宣言から、//go:noescapeディレクティブと、それに関連する説明コメントが削除されています。

例として、src/pkg/syscall/dll_windows.goの変更を示します。

--- a/src/pkg/syscall/dll_windows.go
+++ b/src/pkg/syscall/dll_windows.go
@@ -20,31 +20,11 @@ type DLLError struct {
 func (e *DLLError) Error() string { return e.Msg }
 
 // Implemented in ../runtime/syscall_windows.goc.
-//
-// 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)
-//
-//go:noescape
-//
 func Syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
-//
-//go:noescape
-//
 func Syscall9(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err Errno)
-//
-//go:noescape
-//
 func Syscall12(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 uintptr) (r1, r2 uintptr, err Errno)
-//
-//go:noescape
-//
 func Syscall15(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2 uintptr, err Errno)
-//
 func loadlibrary(filename *uint16) (handle uintptr, err Errno)
 func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno)

これらの変更は、システムコール引数のエスケープを//go:noescapeディレクティブに依存して制御するというアプローチを完全に撤回したことを意味します。これにより、コンパイラはこれらのシステムコール関数の引数に対して、通常の(より保守的な)エスケープ解析ルールを適用するようになります。これは、//go:noescapeの誤用による潜在的なリスクを排除し、より堅牢なメモリ管理メカニズムを将来的に導入するための準備と考えられます。

関連リンク

参考にした情報源リンク