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

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

このコミットは、Go言語のランタイムにおける微細なメモリリークを修正するものです。具体的には、スタック分割境界を越えてC言語の戻り値を保持する m->cret フィールドが、使用後にクリアされずに残ってしまう問題を解決します。これにより、不要になった値が長く保持されることを防ぎ、メモリの効率的な利用を促進します。

コミット

commit 89b075cc90f260edaa4973bd25258ee653a37a2f
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 19 00:26:33 2012 -0500

    runtime: fix tiny memory leak
    
    The m->cret word holds the C return value when returning
    across a stack split boundary.  It was not being cleared after
    use, which means that the return value (if a C function)
    or else the value of AX/R0 at the time of the last stack unsplit
    was being kept alive longer than necessary.  Clear it.
    
    I think the effect here should be very small, but worth fixing
    anyway.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5677092

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

https://github.com/golang/go/commit/89b075cc90f260edaa4973bd25258ee653a37a2f

元コミット内容

このコミットの元の内容は、Goランタイムにおける小さなメモリリークの修正です。m->cret というフィールドが、スタック分割境界を越えてC言語の戻り値を保持する際に使用されますが、使用後にクリアされていなかったため、その値(C関数からの戻り値、または最後のスタックアン・スプリット時のAX/R0レジスタの値)が不要になった後も長く保持され続けていました。このコミットは、この m->cret をクリアすることで、この問題を解決します。コミットメッセージでは、この修正による影響は非常に小さいとしながらも、修正する価値があるとしています。

変更の背景

この変更の背景には、Goランタイムのメモリ管理とスタック管理の最適化があります。Goは、ゴルーチン(goroutine)と呼ばれる軽量な並行処理の単位を使用し、これらのゴルーチンは動的にサイズが変更されるスタックを持っています。スタックのサイズが不足した場合、Goランタイムは自動的にスタックを拡張(スタック分割)し、不要になった場合は縮小(スタックアン・スプリット)します。

m->cret は、GoランタイムがC言語の関数を呼び出し、その戻り値を受け取る際に一時的に使用されるフィールドです。特に、スタック分割が行われた後にC関数からGoコードに戻るようなシナリオで、このフィールドが重要な役割を果たします。問題は、この m->cret に格納された値が、その役割を終えた後もクリアされずに残ってしまうことでした。

Goのガベージコレクタは、到達可能なオブジェクトをメモリリークとみなしません。m->cret に値が残っていると、その値が参照している可能性のあるメモリ領域が、実際には不要であるにもかかわらず、ガベージコレクタによって「到達可能」と判断され、解放されない状態が続いてしまいます。これが「微細なメモリリーク」の原因でした。

このリークは、個々のゴルーチンやC関数呼び出しのコンテキストでは非常に小さいものですが、長期間稼働するサーバーアプリケーションや、多数のC関数呼び出しを伴うプログラムでは、累積的に無視できない量のメモリを消費する可能性がありました。そのため、影響は小さいとされながらも、ランタイムの健全性と効率性を保つために修正が必要と判断されました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とC言語との連携に関する知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、スタック管理、システムコールとの連携など、Goプログラムが効率的に動作するための基盤を提供します。C言語で書かれた部分とGo言語で書かれた部分が混在しています。

  2. ゴルーチン (Goroutine): Goにおける軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。各ゴルーチンは独自のスタックを持っています。

  3. M (Machine/Processor) と G (Goroutine) と P (Processor): Goのスケジューラは、M-P-Gモデルで動作します。

    • M (Machine): OSのスレッドに相当します。Goランタイムは、MをOSスレッドにマッピングし、その上でGoコードを実行します。
    • P (Processor): 論理的なプロセッサを表します。MがGoコードを実行するためのコンテキストを提供し、GをMにディスパッチします。Pの数は通常、CPUのコア数に設定されます。
    • G (Goroutine): ゴルーチンそのものです。 このコミットで言及される m は、このM(OSスレッド)のコンテキストを表す構造体であり、現在のMに関する様々な情報(レジスタの状態、スタック情報、C言語との連携情報など)を保持しています。
  4. スタック管理とスタック分割 (Stack Management and Stack Splitting): Goのゴルーチンは、最初は小さなスタック(数KB程度)で開始されます。関数呼び出しが深くネストしたり、大きなローカル変数が使用されたりしてスタックが不足しそうになると、Goランタイムは自動的にスタックを拡張します。このプロセスを「スタック分割(Stack Splitting)」と呼びます。スタック分割は、関数プロローグ(関数の冒頭)でスタックガード(stack guard)と呼ばれる領域をチェックすることで行われます。スタックが拡張されると、古いスタックの内容は新しい大きなスタックにコピーされ、実行は新しいスタック上で継続されます。逆に、関数が戻ってスタックが不要になると、スタックは縮小(Stack Unsplitting)されることもあります。

  5. m->cret: m 構造体(現在のOSスレッドのコンテキスト)のメンバーである cret は、"C return value" の略です。GoコードがC言語の関数を呼び出し、そのC関数が値を返した場合、その戻り値が一時的に m->cret に格納されます。特に、GoのスタックがC関数呼び出し中に分割されたり、C関数からGoコードに戻る際にスタックの切り替えが発生したりするような複雑なシナリオで、このフィールドがレジスタの値を一時的に保持する役割を担います。

  6. runtime·oldstack 関数: Goランタイム内部の関数で、スタックの切り替え(特にスタック分割やアン・スプリットの後)を処理する際に呼び出されます。この関数は、古いスタックの状態から新しいスタックの状態へ、ゴルーチンの実行コンテキストを安全に移行させる役割を担います。

  7. runtime·gogo 関数: Goランタイム内部の低レベルな関数で、指定された gobuf(ゴルーチンの実行コンテキストを保存した構造体)に格納された情報に基づいて、ゴルーチンの実行を再開します。これは、コンテキストスイッチやゴルーチンのスケジューリングにおいて中心的な役割を果たします。

技術的詳細

このコミットが修正する問題は、Goランタイムの src/pkg/runtime/proc.c ファイル内の runtime·oldstack 関数に存在していました。

runtime·oldstack 関数は、スタックの切り替え(例えば、スタックが拡張された後、古いスタックから新しいスタックへ実行コンテキストを移す際)に呼び出されます。この関数内で、m->cret の値が runtime·gogo 関数に渡され、ゴルーチンの実行が再開されます。

問題は、m->cretruntime·gogo に渡された後も、m->cret 自体の値がクリアされずに残っていた点です。m->cret は、C関数からの戻り値や、スタックアン・スプリット時のレジスタ(AX/R0)の値を一時的に保持するためのものです。これらの値は、runtime·gogo が呼び出されてゴルーチンの実行が再開された時点で、その役割を終えるべきです。

しかし、クリアされないまま残っていると、m->cret が指し示すメモリ領域(例えば、C関数が返したポインタや、単なる整数値であっても、それがメモリ上のどこかの値を指していると解釈されうる場合)が、ガベージコレクタによって「まだ参照されている」と誤って判断される可能性がありました。これにより、実際には不要になったメモリが解放されず、微細なメモリリークが発生していました。

このコミットでは、runtime·gogo を呼び出す直前に m->cret の値を一時変数 cret にコピーし、その後すぐに m->cret = 0; とすることで、m->cret を明示的にクリアしています。これにより、m->cret が不要な値を保持し続けることがなくなり、ガベージコレクタが正しくメモリを解放できるようになります。

この修正は、Goランタイムの低レベルな部分、特にスタック管理とC言語との連携メカニズムの理解に基づいています。Goのガベージコレクタは、到達可能性に基づいてメモリを管理するため、このような「見かけ上の参照」がリークの原因となることがあります。

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

変更は src/pkg/runtime/proc.c ファイルの runtime·oldstack 関数内で行われています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1011,6 +1011,7 @@ runtime·oldstack(void)
 {\n \tStktop *top, old;\n \tuint32 argsize;\n+\tuintptr cret;\n \tbyte *sp;\n \tG *g1;\n \tint32 goid;\
@@ -1034,7 +1035,9 @@ runtime·oldstack(void)\
 \tg1->stackbase = old.stackbase;\n \tg1->stackguard = old.stackguard;\n \n-\truntime·gogo(&old.gobuf, m->cret);\n+\tcret = m->cret;\n+\tm->cret = 0;  // drop reference\n+\truntime·gogo(&old.gobuf, cret);\
 }\n \n // Called from reflect·call or from runtime·morestack when a new

具体的には、以下の3行が追加・変更されています。

  1. uintptr cret;
  2. cret = m->cret;
  3. m->cret = 0; // drop reference

そして、元の runtime·gogo(&old.gobuf, m->cret);runtime·gogo(&old.gobuf, cret); に変更されています。

コアとなるコードの解説

変更されたコードは、runtime·oldstack 関数内で、runtime·gogo を呼び出す直前の処理を修正しています。

  • uintptr cret;: cret という名前の uintptr 型のローカル変数を宣言しています。uintptr は、ポインタを保持できる十分な大きさを持つ符号なし整数型であり、メモリアドレスや、このケースのようにレジスタの値(C言語の戻り値など)を安全に扱うために使用されます。

  • cret = m->cret;: m 構造体(現在のM、つまりOSスレッドのコンテキスト)の cret フィールドの値を、新しく宣言したローカル変数 cret にコピーしています。これにより、m->cret の元の値が一時的に保存されます。

  • m->cret = 0; // drop reference: これがこのコミットの核心的な変更です。m->cret フィールドに 0 を代入することで、その値を明示的にクリアしています。コメント // drop reference が示すように、これは m->cret が保持していた可能性のある参照を解除し、ガベージコレクタがその参照先のメモリを解放できるようにするためです。これにより、不要になった値が長く保持されることによるメモリリークが防止されます。

  • runtime·gogo(&old.gobuf, cret);: runtime·gogo 関数を呼び出し、ゴルーチンの実行コンテキストを切り替えます。ここで、m->cret からコピーしたローカル変数 cret の値が引数として渡されます。これにより、runtime·gogo は必要な値を受け取りつつ、m->cret は既にクリアされた状態になります。

この一連の変更により、m->cret が一時的な目的で値を保持した後、その役割を終えた時点で速やかにクリアされるようになり、Goランタイムのメモリ管理がより正確かつ効率的になります。

関連リンク

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/proc.c)
  • Goのコミットメッセージ
  • Goのランタイムに関する一般的なドキュメントやブログ記事(Goのスケジューラ、スタック管理、ガベージコレクションに関する解説)
    • "Go's work-stealing scheduler" by Dmitry Vyukov (Goスケジューラに関する詳細な解説)
    • "Go's runtime: Goroutine stacks" (Goのゴルーチンスタックに関する解説)
    • "The Go Memory Model" (Goのメモリモデルに関する公式ドキュメント)
  • C言語とGoの連携 (cgo) に関するドキュメント