[インデックス 19264] ファイルの概要
このコミットは、Goランタイムのインターフェース処理に関連するファイル src/pkg/runtime/iface.goc
に変更を加えています。このファイルは、Go言語におけるインターフェースの内部的な挙動、特にインターフェース値のメモリ割り当てと管理に関わるランタイム関数を定義していると考えられます。
コミット
runtime: correctly type interface data.
The backing memory for >1 word interfaces was being scanned
conservatively.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/94000043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e9977dad45aa85afe8864a565f4825d8ea70706d
元コミット内容
runtime: correctly type interface data.
The backing memory for >1 word interfaces was being scanned
conservatively.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/94000043
変更の背景
このコミットの背景には、Goランタイムにおけるガベージコレクション(GC)の正確性に関する問題がありました。具体的には、Goのインターフェースが内部的に保持するデータのうち、1ワード(ポインタサイズ)を超えるサイズのデータ(例えば、大きな構造体や配列など)のメモリ領域が、GCによって「保守的にスキャン」されていたという問題です。
保守的スキャンとは、GCがメモリ領域内のどこにポインタがあるかを正確に識別できない場合に、その領域全体をポインタが含まれている可能性があるものとして扱う方式です。これは、実際にはポインタではないデータがポインタとして誤認識され、その結果、本来は到達不可能で解放されるべきメモリが解放されずに残り続ける(メモリリークに似た状況)という問題を引き起こす可能性があります。
インターフェースのデータが保守的にスキャンされると、GCの効率が低下するだけでなく、メモリ使用量が増加し、プログラムのパフォーマンスに悪影響を与える可能性がありました。このコミットは、この問題を解決し、インターフェースのデータがGCによって正確に型付けされ、適切にスキャンされるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語のランタイムとガベージコレクションに関する前提知識が必要です。
Goのインターフェースの内部表現
Goのインターフェースは、内部的には2つのワード(ポインタサイズ)で構成されています。
- 型情報 (Type Information): インターフェースが保持している具体的な値の型(
_type
構造体へのポインタ)を指します。 - データポインタ (Data Pointer): インターフェースが保持している具体的な値そのもの(またはその値が格納されているメモリ領域)を指します。
インターフェースに格納される値が1ワードに収まるようなプリミティブ型(例: int
, bool
, string
のヘッダ部分)の場合、データポインタは直接その値を保持することがあります。しかし、値が1ワードを超える場合(例: 構造体、配列、スライス、マップ、チャネルなど)、データポインタはヒープ上に割り当てられた実際の値のメモリ領域を指します。
Goのガベージコレクション (GC) の仕組み
GoのGCは、主に「正確なGC(Precise GC)」を目指しています。正確なGCとは、GCがメモリ領域内のどこにポインタがあり、どこにポインタではないデータがあるかを正確に識別できる方式です。これにより、GCは到達可能なオブジェクトのみを正確にマークし、到達不可能なオブジェクトを確実に解放できます。
しかし、特定の状況下では、GoのGCも「保守的スキャン」を使用することがあります。これは、コンパイラやランタイムがポインタの正確な位置を特定できない場合に、安全のためにそのメモリ領域をポインタが含まれている可能性があるものとして扱うフォールバックメカニズムです。保守的スキャンは、誤ってポインタではないデータをポインタとして認識し、メモリリークを引き起こすリスクがあります。
runtime·mal
と runtime·cnew
の違い
Goランタイムには、メモリを割り当てるためのいくつかの内部関数があります。
runtime·mal(size)
: これは、指定されたsize
のメモリを割り当てる汎用的な関数です。この関数で割り当てられたメモリは、その内容に関する型情報がGCに明示的に伝えられない場合があります。そのため、GCは割り当てられたメモリ領域を保守的にスキャンする必要があるかもしれません。runtime·cnew(t)
: これは、特定の型t
に基づいてメモリを割り当てる関数です。この関数は、割り当てるメモリ領域がどのような型のデータ(特にポインタが含まれるかどうか)を保持するかという情報をGCに提供します。これにより、GCは割り当てられたメモリを正確にスキャンし、ポインタを正確に識別できるようになります。
このコミットの文脈では、runtime·mal
が使用されていたために、インターフェースのデータ領域がGCに正確な型情報を提供できず、保守的スキャンが行われていたと考えられます。
技術的詳細
このコミットの技術的な核心は、Goランタイムがインターフェースの内部データをヒープに割り当てる際に使用するメモリ割り当て関数を、型情報をGCに提供しない汎用的なruntime·mal
から、型情報を提供するruntime·cnew
に変更した点にあります。
変更前は、インターフェースに格納される値が1ワードを超えるサイズの場合、その値のコピー先としてruntime·mal(size)
が呼び出されていました。runtime·mal
は単に指定されたサイズのメモリブロックを確保するだけで、そのメモリブロックがどのような型のデータ(特にポインタ)を含むかという情報はGCに明示的に伝えられませんでした。このため、GCは安全のために、このメモリブロックを「ポインタが含まれているかもしれない」と判断し、保守的にスキャンしていました。
保守的スキャンは、GCがメモリ内のすべてのワードをポインタである可能性のあるものとして扱い、それらが指す可能性のあるオブジェクトをマークします。これにより、実際にはポインタではない整数値などが誤ってポインタと解釈され、その結果、本来は到達不可能であるはずのオブジェクトが到達可能と見なされてしまい、メモリが解放されないという問題が発生していました。
このコミットでは、runtime·mal(size)
の代わりにruntime·cnew(t)
を使用するように変更されました。runtime·cnew(t)
は、割り当てるメモリがt
という型のデータであるという情報をGCに伝えます。Goの型システムは、特定の型がポインタを含むかどうか、そしてポインタを含む場合はそのオフセットがどこにあるかを正確に知っています。runtime·cnew(t)
を通じて型情報がGCに渡されることで、GCは割り当てられたインターフェースデータ領域を正確にスキャンできるようになります。つまり、ポインタがどこにあるかを正確に識別し、ポインタではないデータは無視できるようになります。
これにより、インターフェースのデータ領域に対するGCの処理が「正確なスキャン」に移行し、不要なメモリの保持が解消され、GCの効率とメモリ使用量が改善されます。
コアとなるコードの変更箇所
変更は src/pkg/runtime/iface.goc
ファイルの copyin
関数内で行われています。
--- a/src/pkg/runtime/iface.goc
+++ b/src/pkg/runtime/iface.goc
@@ -161,7 +161,7 @@ copyin(Type *t, void *src, void **dst)
if(size <= sizeof(*dst))
alg->copy(size, dst, src);
else {
- p = runtime·mal(size);
+ p = runtime·cnew(t);
alg->copy(size, p, src);
*dst = p;
}
コアとなるコードの解説
copyin
関数は、インターフェースに値をコピーする際に使用されるランタイム関数の一部であると推測されます。この関数は、Type *t
(コピーされる値の型)、void *src
(コピー元のデータ)、void **dst
(コピー先のポインタ)を引数に取ります。
変更が行われたのは、size <= sizeof(*dst)
の条件が偽、つまりコピーされる値のサイズが1ワード(ポインタサイズ)を超える場合です。この場合、インターフェースのデータポインタが直接値を保持するのではなく、ヒープ上に別途メモリを割り当ててそこに値をコピーする必要があります。
-
変更前:
p = runtime·mal(size);
runtime·mal(size)
は、size
バイトのメモリを割り当てます。この割り当ては、GCに対してそのメモリ領域の型情報を提供しませんでした。そのため、GCは安全のために、この領域を保守的にスキャンしていました。
-
変更後:
p = runtime·cnew(t);
runtime·cnew(t)
は、t
で指定された型に基づいてメモリを割り当てます。このt
は、インターフェースに格納される具体的な値の型です。runtime·cnew
は、この型情報を使って、割り当てられたメモリ領域がポインタを含むかどうか、そしてポインタが含まれる場合はその正確な位置をGCに伝えます。これにより、GCはインターフェースのデータ領域を正確にスキャンできるようになり、保守的スキャンが不要になります。
この変更により、Goのガベージコレクタは、インターフェースが保持する1ワードを超えるサイズのデータについて、より効率的かつ正確にメモリを管理できるようになりました。これは、メモリリークのリスクを低減し、全体的なランタイムパフォーマンスを向上させる上で重要な改善です。
関連リンク
- Go Gerrit Code Review: https://golang.org/cl/94000043
参考にした情報源リンク
- コミットメッセージ自体
- Go言語のインターフェースの内部構造に関する一般的な知識
- Go言語のガベージコレクションに関する一般的な知識
- Goランタイムのメモリ割り当て関数に関する一般的な知識
- (Web検索結果は直接的な内容取得には至らなかったが、関連キーワードの確認に利用)