[インデックス 14720] ファイルの概要
このコミットは、Goコンパイラにおけるzerostack
という実験的な機能の削除に関するものです。この機能は、関数エントリ時にスタックフレームをゼロクリアすることを目的としていましたが、その実装がコンパイル時ではなくリンク時に行われるように変更されたため、削除されました。
コミット
commit e431398e0947c455e1c34fdd7c0571d17a1365d0
Author: Russ Cox <rsc@golang.org>
Date: Sat Dec 22 11:18:04 2012 -0500
undo CL 6938073 / 1542912cf09d
remove zerostack compiler experiment; will do at link time instead
««« original CL description
cmd/gc: add GOEXPERIMENT=zerostack to clear stack on function entry
This is expensive but it might be useful in cases where
people are suffering from false positives during garbage
collection and are willing to trade the CPU time for getting
rid of the false positives.
On the other hand it only eliminates false positives caused
by other function calls, not false positives caused by dead
temporaries stored in the current function call.
The 5g/6g/8g changes were pulled out of the history, from
the last time we needed to do this (to work around a goto bug).
The code in go.h, lex.c, pgen.c is new but tiny.
R=ken2
CC=golang-dev
https://golang.org/cl/6938073
»»»
R=ken2
CC=golang-dev
https://golang.org/cl/7002051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e431398e0947c455e1c34fdd7c0571d17a1365d0
元コミット内容
このコミットは、以前のコミット CL 6938073 / 1542912cf09d
を元に戻すものです。元のコミットは、Goコンパイラ(cmd/gc
)にGOEXPERIMENT=zerostack
という実験的なオプションを追加し、関数エントリ時にスタックをゼロクリアする機能を提供していました。
元のコミットの説明によると、この機能はガベージコレクション(GC)における「偽陽性(false positives)」に悩まされている場合に有用であるとされていました。偽陽性を排除するためにCPU時間を犠牲にするトレードオフが許容されるケースを想定していました。ただし、この機能は他の関数呼び出しによって引き起こされる偽陽性のみを排除し、現在の関数呼び出し内に存在する不要な一時変数によって引き起こされる偽陽性には効果がないことも指摘されていました。
5g
/6g
/8g
(それぞれARM、x86-64、x86アーキテクチャ向けのGoコンパイラバックエンド)への変更は、過去に同様の目的(gotoバグの回避)で必要とされた履歴から引き出されたものであり、go.h
、lex.c
、pgen.c
のコードは新規だがごくわずかであると説明されていました。
変更の背景
このコミットの背景には、Goのガベージコレクションの正確性と効率性に関する継続的な改善努力があります。GoのGCは、実行中のプログラムが使用しているメモリを自動的に解放する機能ですが、その過程で「偽陽性」という問題が発生することがあります。
「偽陽性」とは、GCが実際には使用されていないメモリ領域を、誤って参照されている(つまり、まだ解放できない)と判断してしまう状況を指します。これは、スタック上に残された古いポインタ値が、たまたま有効なメモリアドレスを指しているように見えてしまう場合に発生しやすいです。このような偽陽性があると、GCが不要なメモリを解放できず、メモリリークのように見えたり、ヒープサイズが不必要に増大したりする可能性があります。
元のzerostack
実験は、この偽陽性問題を緩和するための一つの試みでした。関数が呼び出された際に、そのスタックフレームをゼロで埋めることで、古いポインタ値が残る可能性を減らし、GCの正確性を向上させようとしました。しかし、このゼロクリア処理は実行時にオーバーヘッドを伴うため、パフォーマンスへの影響が懸念されました。
このコミットでzerostack
実験が元に戻されたのは、スタックのゼロクリアという目的自体は維持しつつも、その実装方法をコンパイル時ではなく「リンク時」に行うという、より効率的または適切なアプローチが見つかったためです。コンパイル時に各関数エントリでゼロクリアコードを挿入するよりも、リンカがプログラム全体の構造を把握した上で、より最適化された方法でスタック領域を初期化する方が、全体的なパフォーマンスやコードの複雑さの点で優れていると判断されたと考えられます。
前提知識の解説
ガベージコレクション (GC) と偽陽性 (False Positives)
ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはや到達不可能(参照されていない)になったものを自動的に検出し、解放する仕組みです。これにより、プログラマは手動でのメモリ管理から解放され、メモリリークのリスクを低減できます。
GoのGCは、主に「マーク&スイープ」方式を採用しています。これは、プログラムが使用しているオブジェクトを「マーク」し、マークされなかった(つまり、到達不可能な)オブジェクトを「スイープ」(解放)するプロセスです。このマークフェーズにおいて、GCはプログラムのルート(グローバル変数、レジスタ、スタックなど)から到達可能なすべてのオブジェクトを辿っていきます。
ここで問題となるのが「偽陽性」です。関数呼び出しが行われると、その関数に必要なローカル変数や引数などを格納するための「スタックフレーム」がスタック上に確保されます。関数が終了しても、そのスタックフレームが使用していたメモリ領域はすぐに上書きされるとは限りません。もし、その古いスタックフレームの領域に、たまたま有効なメモリアドレスを指すようなビットパターンが残っていた場合、GCはそれを有効なポインタだと誤認し、そのポインタが指す先のオブジェクトを「到達可能」とマークしてしまう可能性があります。これがGCの偽陽性です。
偽陽性が発生すると、実際には不要なオブジェクトが解放されず、メモリ使用量が増加したり、GCの効率が低下したりします。
スタックとスタックフレーム
スタックは、プログラムの実行中に一時的なデータを格納するために使用されるメモリ領域です。関数呼び出しが行われるたびに、その関数に必要な情報(引数、ローカル変数、戻りアドレスなど)を格納するための「スタックフレーム」がスタックの先頭にプッシュされます。関数が終了すると、そのスタックフレームはスタックからポップされ、その領域は解放されます。
コンパイラとリンカ
- コンパイラ: ソースコード(Go言語)を機械語(または中間コード)に変換するプログラムです。Goのコンパイラは
cmd/gc
(Go Compiler)として知られています。アーキテクチャごとに5g
(ARM),6g
(x86-64),8g
(x86) などのバックエンドが存在します。 - リンカ: コンパイラによって生成された複数のオブジェクトファイルやライブラリを結合し、実行可能なプログラムを生成するプログラムです。リンカは、異なるファイルに分散している関数や変数の参照を解決し、最終的な実行ファイルのメモリレイアウトを決定します。
GOEXPERIMENT
GOEXPERIMENT
は、Go言語のツールチェイン(コンパイラ、リンカなど)における実験的な機能を有効にするための環境変数です。これにより、開発者はまだ安定版に組み込まれていない新機能を試すことができます。
技術的詳細
このコミットは、Goコンパイラが関数エントリ時にスタックをゼロクリアする実験的な機能(GOEXPERIMENT=zerostack
)を削除します。この機能は、GCの偽陽性を減らすことを目的としていましたが、その実装方法がコンパイル時ではなくリンク時に行われるように変更されたため、コンパイラ側のコードが不要になりました。
具体的には、以下の変更が行われています。
-
clearstk
関数の削除:src/cmd/5g/gsubr.c
(ARMアーキテクチャ向け)src/cmd/6g/gsubr.c
(x86-64アーキテクチャ向け)src/cmd/8g/gsubr.c
(x86アーキテクチャ向け) これらのファイルから、スタックをゼロクリアするためのアセンブリコードを生成するclearstk
関数が完全に削除されています。各アーキテクチャの特性に合わせて、スタックポインタ(SP)、デスティネーションインデックス(DI)、カウントレジスタ(CX)、アキュムレータ(AX)などのレジスタを操作し、STOSQ
(Store String Quadword) やSTOSL
(Store String Long) といった命令を使ってメモリをゼロで埋める処理が含まれていました。
-
zerostack_enabled
変数の削除:src/cmd/gc/go.h
:zerostack_enabled
というグローバル変数の宣言が削除されています。この変数は、zerostack
実験が有効かどうかをコンパイラ全体で管理するために使用されていました。src/cmd/gc/lex.c
:GOEXPERIMENT
環境変数を解析し、zerostack_enabled
変数を設定する部分が削除されています。これにより、GOEXPERIMENT=zerostack
オプションは認識されなくなります。
-
clearstk
呼び出しの削除:src/cmd/gc/pgen.c
: コンパイラのコード生成フェーズにおいて、zerostack_enabled
が真の場合にclearstk()
関数を呼び出すロジックが削除されています。これにより、コンパイル時にスタックゼロクリアコードが挿入されることはなくなります。
この変更は、スタックゼロクリアの責任がコンパイラからリンカに移管されたことを明確に示しています。リンカは、プログラム全体のメモリレイアウトをより広範に把握できるため、スタックの初期化をより効率的に、あるいはより適切なタイミングで行うことが可能になります。例えば、リンカは、プログラムの起動時に一度に大きなスタック領域をゼロクリアしたり、特定のセグメントを初期化したりするような最適化を行うことができます。これにより、各関数呼び出しのオーバーヘッドを削減しつつ、GCの偽陽性対策という目的を達成できると考えられます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルからコードが削除されています。
src/cmd/5g/gsubr.c
: ARMアーキテクチャ向けのスタックゼロクリア関数clearstk
の実装が削除。src/cmd/6g/gsubr.c
: x86-64アーキテクチャ向けのスタックゼロクリア関数clearstk
の実装が削除。src/cmd/8g/gsubr.c
: x86アーキテクチャ向けのスタックゼロクリア関数clearstk
の実装が削除。src/cmd/gc/go.h
:zerostack_enabled
グローバル変数の宣言が削除。src/cmd/gc/lex.c
:GOEXPERIMENT=zerostack
オプションの解析とzerostack_enabled
変数への設定ロジックが削除。src/cmd/gc/pgen.c
: コンパイル時にclearstk()
を呼び出す条件分岐が削除。
これらの変更は、zerostack
機能に関連するコンパイラ側のコードパスを完全に削除し、その責任をリンカに移管したことを示しています。
コアとなるコードの解説
削除されたコードは、各アーキテクチャのGoコンパイラバックエンド(5g
, 6g
, 8g
)に存在していたclearstk
関数と、その機能を制御するzerostack_enabled
フラグおよび関連ロジックです。
clearstk
関数の例 (削除された src/cmd/5g/gsubr.c
から)
void
clearstk(void)
{
Plist *pl;
Prog *p, *p1, *p2, *p3;
Node dst, end, zero, con;
if(plast->firstpc->to.offset <= 0)
return;
// reestablish context for inserting code
// at beginning of function.
pl = plast;
p1 = pl->firstpc;
p2 = p1->link;
pc = mal(sizeof(*pc));
clearp(pc);
p1->link = pc;
// zero stack frame
// MOVW $4(SP), R1
nodreg(&dst, types[tptr], 1);
p = gins(AMOVW, N, &dst);
p->from.type = D_CONST;
p->from.reg = REGSP;
p->from.offset = 4;
// MOVW $n(R1), R2
nodreg(&end, types[tptr], 2);
p = gins(AMOVW, N, &end);
p->from.type = D_CONST;
p->from.reg = 1;
p->from.offset = p1->to.offset;
// MOVW $0, R3
nodreg(&zero, types[TUINT32], 3);
nodconst(&con, types[TUINT32], 0);
gmove(&con, &zero);
// L:
// MOVW.P R3, 0(R1) +4
// CMP R1, R2
// BNE L
p = gins(AMOVW, &zero, &dst);
p->to.type = D_OREG;
p->to.offset = 4;
p->scond |= C_PBIT;
p3 = p;
p = gins(ACMP, &dst, N);
raddr(&end, p);
patch(gbranch(ABNE, T, 0), p3);
// continue with original code.
gins(ANOP, N, N)->link = p2;
pc = P;
}
このclearstk
関数は、Goコンパイラのバックエンドがアセンブリコードを生成する際に使用される内部関数です。上記はARMアーキテクチャ(5g
)の例ですが、他のアーキテクチャ(6g
, 8g
)でも同様の目的で、それぞれの命令セットに合わせたアセンブリコードを生成していました。
この関数は、現在の関数のスタックフレームの先頭に、スタックをゼロクリアするためのアセンブリ命令を挿入していました。具体的には、スタックポインタ(SP)から始まる領域を、ループを使ってゼロで埋める処理を行っていました。これにより、関数が呼び出された直後に、そのスタックフレーム内の古いデータ(特にポインタ値)が確実にゼロに初期化され、GCの偽陽性を防ぐことを目指していました。
zerostack_enabled
とその利用 (削除された src/cmd/gc/go.h
, src/cmd/gc/lex.c
, src/cmd/gc/pgen.c
から)
-
src/cmd/gc/go.h
で宣言されていたzerostack_enabled
は、int
型のグローバル変数で、GOEXPERIMENT=zerostack
が指定された場合に1
に設定され、この機能が有効であることを示していました。 -
src/cmd/gc/lex.c
では、コンパイラのコマンドライン引数や環境変数を解析する際に、GOEXPERIMENT
の値にzerostack
が含まれているかをチェックし、含まれていればzerostack_enabled
を1
に設定していました。 -
src/cmd/gc/pgen.c
のcompile
関数(Go関数のコンパイルを担当)内では、以下のような条件分岐がありました。if(zerostack_enabled) clearstk();
このコードは、
zerostack_enabled
が真の場合にのみ、上記で説明したclearstk()
関数を呼び出し、スタックゼロクリアのアセンブリコードを生成するようにしていました。
これらのコードの削除は、コンパイラがスタックゼロクリアの責任を完全に放棄し、この処理がGoのツールチェインの別の段階(リンカ)で、より効率的かつ透過的に行われるようになったことを意味します。
関連リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事:
- Goのコンパイラとリンカの内部構造に関する情報源:
- Go Compiler Internals (古い記事ですが、基本的な概念は参考になります)
- The Go Programming Language Specification - Calls (スタックフレームの概念に関連)
参考にした情報源リンク
- コミットメッセージと差分情報: https://github.com/golang/go/commit/e431398e0947c455e1c34fdd7c0571d17a1365d0
- 元のコミット
CL 6938073
: https://golang.org/cl/6938073 (現在はGoのコードレビューシステムGerritにリダイレクトされます) - Go言語のガベージコレクションに関する一般的な知識
- コンパイラとリンカの役割に関する一般的な知識
- スタックとスタックフレームに関する一般的な知識
- Go言語の
GOEXPERIMENT
に関する一般的な知識