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

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

このコミットは、Goランタイムにおける特定のメモリリークを修正するものです。morebufmoreargpというランタイム内部の構造体が、スタック分割、reflect.callpanic/recoverの際に不要な参照を保持し続けることで、関連するメモリがガベージコレクションされない問題に対処しています。特に、クロージャやreflect.callによって割り当てられた引数リストがリークの原因となっていました。

コミット

commit 8a4c2b3cc45edb4a263c775683947709e9b4c50d
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 19 11:05:19 2012 -0500

    runtime: fix another memory leak
    
    morebuf holds a pc/sp from the last stack split or
    reflect.call or panic/recover.  If the pc is a closure,
    the reference will keep it from being collected.
    
    moreargp holds a pointer to the arguments from the
    last stack split or reflect.call or panic/recover.
    Normally it is a stack pointer and thus not of interest,
    but in the case of reflect.call it is an allocated argument
    list and holds up the arguments to the call.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5674109

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

https://github.com/golang/go/commit/8a4c2b3cc45edb4a263c775683947709e9b4c50d

元コミット内容

runtime: fix another memory leak

morebuf holds a pc/sp from the last stack split or
reflect.call or panic/recover.  If the pc is a closure,
the reference will keep it from being collected.

moreargp holds a pointer to the arguments from the
last stack split or reflect.call or panic/recover.
Normally it is a stack pointer and thus not of interest,
but in the case of reflect.call it is an allocated argument
list and holds up the arguments to the call.

変更の背景

Goランタイムは、プログラムの実行中にスタックの拡張(スタック分割)や、リフレクションによる関数呼び出し(reflect.call)、あるいはパニックとリカバリーの処理を行います。これらの操作の際、ランタイム内部のm (machine/processor) 構造体には、morebufmoreargpというフィールドが一時的に使用されます。

morebufは、スタックポインタ(sp)とプログラムカウンタ(pc)を保持します。特にpcがクロージャ(関数とその環境をキャプチャしたもの)を指している場合、このmorebufがクロージャへの参照を保持し続けると、クロージャがガベージコレクションの対象から外れてしまい、メモリリークが発生します。

一方、moreargpは、関数呼び出しの引数へのポインタを保持します。通常の関数呼び出しでは、これはスタック上の引数を指すため、ガベージコレクションの観点からは問題になりません。しかし、reflect.callの場合、引数リストはヒープ上に動的に割り当てられることがあります。このmoreargpが、reflect.callによって割り当てられた引数リストへの参照を保持し続けると、その引数リストがガベージコレクションされずに残り、これもメモリリークの原因となります。

このコミットは、これらのmorebufmoreargpが不要になった時点で明示的にnilに設定することで、これらの不要な参照を解除し、メモリリークを防ぐことを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とガベージコレクションの仕組みについて理解が必要です。

  1. Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、ガベージコレクタ、スタック管理、プリミティブな同期機構などが含まれます。C言語で書かれた部分が多く、Goプログラムのパフォーマンスと効率に直結します。

  2. ガベージコレクション (Garbage Collection, GC): Goは自動メモリ管理を採用しており、不要になったメモリ領域を自動的に解放するガベージコレクタを備えています。GCは、到達可能性(Reachability)に基づいて動作します。つまり、プログラムがアクセス可能な(参照されている)オブジェクトは「生きている」と判断され、そうでないオブジェクトは「死んでいる」と判断されて回収されます。メモリリークは、本来不要になったはずのオブジェクトが、何らかの理由で「生きている」と誤って判断され、GCによって回収されない場合に発生します。

  3. スタック分割 (Stack Splitting): Goのgoroutineは、最初は小さなスタック(数KB)で開始されます。関数呼び出しが深くネストするなどしてスタックが不足しそうになると、ランタイムは自動的に現在のスタックよりも大きな新しいスタックを割り当て、古いスタックの内容を新しいスタックにコピーします。このプロセスをスタック分割と呼びます。これにより、Goは固定サイズの大きなスタックを事前に割り当てる必要がなくなり、メモリ効率が向上します。

  4. reflect.call: Goのreflectパッケージは、実行時に型情報を調べたり、値の操作を行ったり、メソッドを呼び出したりする機能を提供します。reflect.Value.Call()メソッドは、リフレクションを使って関数を呼び出す際に使用されます。この際、引数はreflect.Valueのスライスとして渡され、ランタイム内部で実際の関数呼び出しのために適切な形式に変換・配置されます。この引数の配置がヒープ上で行われる場合があり、そのメモリがリークの原因となることがあります。

  5. panicrecover: panicは、プログラムの異常終了を引き起こすGoの組み込み関数です。recoverは、deferされた関数内でpanicからの回復を試みるために使用されます。panicが発生すると、ランタイムは現在のgoroutineのスタックを巻き戻し(unwind)、deferされた関数を順次実行します。このスタックの巻き戻し処理中にも、ランタイムは一時的な情報を保持するためにmorebufmoreargpを使用します。

  6. クロージャ (Closures): クロージャは、それが定義された環境(レキシカルスコープ)の変数を「キャプチャ」する関数です。クロージャがキャプチャした変数は、クロージャが実行される間、その変数がスコープ外に出ても存続し続けます。クロージャ自体もメモリ上に割り当てられ、そのpc(プログラムカウンタ)はクロージャのコードエントリポイントを指します。morebuf.pcがクロージャを指している場合、その参照が解除されないと、クロージャとそのキャプチャした環境がGCされずに残ってしまいます。

  7. m (Machine/Processor) 構造体: Goランタイムの内部では、mはOSのスレッド(カーネルスレッド)を表す構造体です。各mは、現在実行中のgoroutineや、スタック分割、reflect.callpanic/recoverなどのランタイム操作に必要な一時的な状態を保持します。morebufmoreargpは、このm構造体の一部として定義されています。

技術的詳細

このメモリリークは、runtime·newstack関数内で発生していました。runtime·newstackは、スタック分割やreflect.callpanic/recoverなどの際に、新しいスタックフレームを設定するGoランタイムの重要な関数です。

この関数が実行される際、m->morebufm->moreargpには、それぞれ古いスタックフレームのpc/spや、reflect.callによって割り当てられた引数リストへのポインタが一時的に格納されます。これらの情報は、新しいスタックフレームを正しく構築するために必要です。

問題は、これらの情報が新しいスタックフレームにコピーされた後も、m->morebufm->moreargpが以前の値を保持し続けていた点にあります。

  • morebufのリーク: morebuf.pcがクロージャのコードエントリポイントを指している場合、この参照が残っていると、クロージャオブジェクト自体がガベージコレクタによって到達可能と判断され、回収されません。これにより、クロージャがキャプチャしていた変数なども含めてメモリがリークします。
  • moreargpのリーク: reflect.callの場合、引数リストはヒープ上に動的に割り当てられます。moreargpがこのヒープ上の引数リストへのポインタを保持し続けると、引数リストがガベージコレクタによって到達可能と判断され、回収されません。これにより、引数リストが占めていたメモリがリークします。

このコミットでは、runtime·newstack関数内で、新しいスタックフレームへの情報のコピーが完了した直後に、m->moreargpm->morebuf.pcm->morebuf.spを明示的にnilに設定することで、これらの不要な参照を解除しています。これにより、関連するメモリがガベージコレクタによって正しく回収されるようになり、メモリリークが解消されます。

また、runtime·memmoveのソース引数がm->moreargpからtop->argpに変更されています。これは、top->argpが直前の行でm->moreargpの値を受け取っているため、冗長なm構造体へのアクセスを避けるためのリファクタリングであり、直接的なリーク修正というよりはコードの整合性を高める変更です。

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

変更はsrc/pkg/runtime/proc.cファイル内のruntime·newstack関数に集中しています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1103,6 +1103,9 @@ runtime·newstack(void)\n  	top->argp = m->moreargp;\n  	top->argsize = argsize;\n  	top->free = free;\n+\tm->moreargp = nil;\n+\tm->morebuf.pc = nil;\n+\tm->morebuf.sp = nil;\n \n  	// copy flag from panic\n  	top->panic = g1->ispanic;\n@@ -1114,7 +1117,7 @@ runtime·newstack(void)\n  	sp = (byte*)top;\n  	if(argsize > 0) {\n  	\tsp -= argsize;\n-\t\truntime·memmove(sp, m->moreargp, argsize);\n+\t\truntime·memmove(sp, top->argp, argsize);\n  	}\n  	if(thechar == \'5\') {\n  	\t// caller would have saved its LR below args.\n```

## コアとなるコードの解説

1.  **`m->moreargp = nil;`**
    *   `m`構造体の`moreargp`フィールドを`nil`(ヌルポインタ)に設定します。これにより、`reflect.call`によってヒープ上に割り当てられた引数リストへの不要な参照が解除され、そのメモリがガベージコレクションの対象となります。

2.  **`m->morebuf.pc = nil;`**
    *   `m`構造体の`morebuf`フィールド内の`pc`(プログラムカウンタ)を`nil`に設定します。これにより、スタック分割、`reflect.call`、`panic`/`recover`の際に一時的に保持されていたクロージャの`pc`への参照が解除され、クロージャオブジェクトがガベージコレクションの対象となります。

3.  **`m->morebuf.sp = nil;`**
    *   `m`構造体の`morebuf`フィールド内の`sp`(スタックポインタ)を`nil`に設定します。これは`pc`と同様に、不要な参照を解除し、メモリリークを防ぐための措置です。

4.  **`runtime·memmove(sp, m->moreargp, argsize);` から `runtime·memmove(sp, top->argp, argsize);` への変更**
    *   `runtime·memmove`はメモリブロックをコピーするランタイム関数です。
    *   変更前は、`m->moreargp`から直接メモリをコピーしていました。
    *   変更後は、`top->argp`からメモリをコピーするように変わっています。
    *   この変更の数行前で、`top->argp = m->moreargp;`という行があり、`m->moreargp`の値は既に`top->argp`にコピーされています。したがって、この変更は機能的な違いをもたらすものではなく、既にローカル変数`top->argp`に格納されている値を使用することで、コードの整合性を高め、`m`構造体への再度のアクセスを避けるためのリファクタリングと考えられます。これにより、コードがより明確になり、将来的な変更に対する堅牢性が増します。

これらの変更により、`runtime·newstack`が完了した後に`m`構造体内の`morebuf`と`moreargp`がクリーンアップされ、関連するメモリリークが解消されます。

## 関連リンク

*   Go CL (Change List): [https://golang.org/cl/5674109](https://golang.org/cl/5674109)

## 参考にした情報源リンク

*   Goのガベージコレクションについて:
    *   [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide)
    *   [https://go.dev/blog/go15gc](https://go.dev/blog/go15gc)
*   Goのスタック管理とスタック分割について:
    *   [https://go.dev/doc/articles/go_mem.html](https://go.dev/doc/articles/go_mem.html)
    *   [https://go.dev/blog/go-stacks](https://go.dev/blog/go-stacks)
*   Goの`reflect`パッケージについて:
    *   [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
    *   [https://go.dev/blog/laws-of-reflection](https://go.dev/blog/laws-of-reflection)
*   Goの`panic`と`recover`について:
    *   [https://go.dev/blog/defer-panic-and-recover](https://go.dev/blog/defer-panic-and-recover)
*   Goランタイムのソースコード(特に`proc.c`):
    *   [https://github.com/golang/go/blob/master/src/runtime/proc.go](https://github.com/golang/go/blob/master/src/runtime/proc.go) (現在のGoでは`proc.c`は`proc.go`に置き換えられています)
    *   (当時の`proc.c`のコードベースは、Goのバージョン管理システムで確認する必要がありますが、概念は共通です。)
*   Goのクロージャについて:
    *   [https://go.dev/tour/moretypes/25](https://go.dev/tour/moretypes/25)
    *   [https://go.dev/blog/closures](https://go.dev/blog/closures)
*   Goの`m`構造体(P, M, Gモデル)について:
    *   [https://go.dev/blog/go-concurrency-patterns-pipelines](https://go.dev/blog/go-concurrency-patterns-pipelines) (直接的ではないが、Goのスケジューラモデルの理解に役立つ)
    *   [https://go.dev/src/runtime/runtime2.go](https://go.dev/src/runtime/runtime2.go) (現在のランタイム構造体の定義)
*   `memmove`関数について:
    *   C言語の標準ライブラリ関数であり、メモリコピーに使用されます。GoランタイムのCコード部分で利用されています。
    *   [https://en.cppreference.com/w/c/string/byte/memmove](https://en.cppreference.com/w/c/string/byte/memmove)