[インデックス 1632] ファイルの概要
コミット
commit 3e02987508d25db11b213e7ce1edd39000ade7c8
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 6 14:41:21 2009 -0800
tgs's gc bug.
R=r
DELTA=10 (7 added, 0 deleted, 3 changed)
OCL=24577
CL=24577
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3e02987508d25db11b213e7ce1edd39000ade7c8
元コミット内容
tgs's gc bug.
R=r
DELTA=10 (7 added, 0 deleted, 3 changed)
OCL=24577
CL=24577
変更の背景
このコミットは、Go言語の初期段階(2009年)におけるガベージコレクション(GC)のバグ修正を目的としています。当時のGoのGCはまだ成熟しておらず、Russ Cox自身もその性能について「かなり遅い」と認めていました。特に、バイナリツリーのベンチマークがGCの性能を測るものとして機能していたことからも、GCがボトルネックとなっていたことが伺えます。
コミットメッセージにある「tgs's gc bug」は、おそらくGoの開発者の一人である「tgs」によって発見された、GCに関連する特定の不具合を指しています。このバグは、メモリ管理の根幹をなすmlookup
関数におけるポインタの解釈、特にGCの参照カウント領域に関する誤った処理に起因していたと考えられます。GCが正しく動作しないと、メモリリーク、不正なメモリアクセス、クラッシュなど、深刻な問題を引き起こす可能性があります。この修正は、Goのランタイムにおけるメモリ管理の堅牢性を高めるための重要なステップでした。
前提知識の解説
- ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されなくなったものを自動的に解放する仕組みです。Go言語はトレース型GCを採用しており、到達可能なオブジェクトを特定し、それ以外のオブジェクトを解放します。
src/runtime/malloc.c
: Goランタイムのメモリ割り当て(malloc)と解放(free)に関する低レベルな処理が記述されているC言語のファイルです。Goのメモリ管理の根幹を担っています。mlookup
関数:src/runtime/malloc.c
内に定義されている関数で、与えられたポインタv
が指すメモリブロックの情報を検索します。具体的には、そのブロックのベースアドレス、サイズ、そしてGC参照カウントへのポインタなどを返します。GCがメモリをスキャンする際に、ポインタが有効なオブジェクトを指しているか、そのオブジェクトのサイズはどれくらいか、といった情報を得るために利用されます。MSpan
構造体: Goのメモリヒープを管理するための重要なデータ構造です。連続したページ(メモリブロックの最小単位)の集合を表し、特定のサイズのオブジェクトを格納するために使用されます。MSpan
には、そのスパンが管理するメモリ領域の開始アドレス、ページ数、オブジェクトのサイズクラスなどの情報が含まれます。PageShift
: メモリページのサイズをビットシフトで表す定数です。例えば、1 << PageShift
でページサイズが計算できます。sizeclass
: Goのメモリ割り当てにおいて、オブジェクトのサイズに応じて分類されるクラスです。異なるサイズのオブジェクトは、それぞれに適したMSpan
から割り当てられます。gcref
:MSpan
構造体の一部で、そのスパン内のオブジェクトのGC参照カウント情報が格納されているメモリ領域へのポインタです。GCは、このgcref
領域を参照して、各オブジェクトが参照されているかどうかを判断します。
技術的詳細
このコミットは、src/runtime/malloc.c
内のmlookup
関数に焦点を当てた修正です。mlookup
は、与えられたポインタv
がGoのヒープ内の有効なオブジェクトを指しているかを判断し、そのオブジェクトのメタデータ(ベースアドレス、サイズ、GC参照情報)を返す役割を担っています。
変更の核心は、GCが参照カウント情報を格納している領域自体を、誤って有効なオブジェクトへのポインタとして扱ってしまう可能性があった点を修正することにあります。
具体的には、以下の2つの主要な変更が行われています。
-
gcref
領域へのポインタの除外:+ if((byte*)v >= (byte*)s->gcref) { + // pointers into the gc ref counts + // do not count as pointers. + return 0; + }
この新しいチェックは、入力ポインタ
v
がMSpan
のgcref
領域(GC参照カウントが格納されているメモリ領域)内を指している場合に、即座に0
を返してmlookup
を終了させます。これは、「GC参照カウントへのポインタは、有効なオブジェクトへのポインタとしてはカウントしない」という明確な意図を示しています。もしこのチェックがないと、GCは自身の管理情報であるgcref
領域を、誤ってヒープ上のオブジェクトとして解釈し、不正確なGCスキャンやクラッシュを引き起こす可能性がありました。 -
gcref
領域の範囲チェックの修正:- if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift)) { + nobj = (s->npages << PageShift) / (n + RefcountOverhead); + if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift)) {
既存の
gcref
領域の範囲チェックが修正されました。nobj
という新しい変数が導入され、これは現在のMSpan
が保持できるオブジェクトの総数を計算します。この計算には、オブジェクトのサイズn
と、GC参照カウントのためのオーバーヘッドRefcountOverhead
が考慮されています。- 以前のチェックでは、
gcref
の開始アドレスがスパンの範囲内にあるかのみを確認していました。しかし、修正後はgcref
の開始アドレスだけでなく、gcref
領域の終了アドレス(s->gcref + nobj
)もスパンの有効なメモリ範囲(p
からp+(s->npages<<PageShift)
)内に収まっているかを厳密にチェックするようになりました。 - この変更により、
gcref
領域がMSpan
の割り当てられたメモリ範囲を逸脱していないことをより正確に保証し、メモリ破損やGCの誤動作を防ぎます。
これらの変更は、GCがメモリヒープを正確にスキャンし、有効なオブジェクトとGCの内部管理領域を区別するために不可欠です。特に、gcref
領域がヒープ上の通常のオブジェクトと混同されることを防ぎ、GCの堅牢性と正確性を向上させています。
コアとなるコードの変更箇所
--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -121,8 +121,8 @@ free(void *v)
int32
mlookup(void *v, byte **base, uintptr *size, uint32 **ref)
{
- uintptr n, i;
- byte *p;
+ uintptr n, nobj, i;
+ byte *p, *ep;
MSpan *s;
s = MHeap_LookupMaybe(&mheap, (uintptr)v>>PageShift);
@@ -148,13 +148,20 @@ mlookup(void *v, byte **base, uintptr *size, uint32 **ref)
return 1;
}
+ if((byte*)v >= (byte*)s->gcref) {
+ // pointers into the gc ref counts
+ // do not count as pointers.
+ return 0;
+ }
+
n = class_to_size[s->sizeclass];
i = ((byte*)v - p)/n;
if(base)
*base = p + i*n;
if(size)
*size = n;
- if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift)) {
+ nobj = (s->npages << PageShift) / (n + RefcountOverhead);
+ if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift)) {
printf("s->base sizeclass %d %p gcref %p block %D\\n",
s->sizeclass, p, s->gcref, s->npages<<PageShift);
throw("bad gcref");
コアとなるコードの解説
uintptr n, nobj, i;
とbyte *p, *ep;
:nobj
が新しく追加されました。これは、現在のMSpan
が保持するオブジェクトの数を計算するために使用されます。ep
は追加されましたが、このコミットでは使用されていません。これは、将来的な拡張のためのプレースホルダーか、以前の変更の名残である可能性があります。
- 新しい
if
ブロック (if((byte*)v >= (byte*)s->gcref)
):- このコードは、
mlookup
に渡されたポインタv
が、現在のMSpan
のGC参照カウント領域(s->gcref
が指す領域)内にあるかどうかをチェックします。 - もし
v
がgcref
領域内を指している場合、それはヒープ上の通常のオブジェクトへのポインタではないため、mlookup
は0
を返して処理を終了します。これにより、GCが自身の管理データを誤ってオブジェクトとして処理することを防ぎます。
- このコードは、
nobj = (s->npages << PageShift) / (n + RefcountOverhead);
:s->npages << PageShift
は、MSpan
が管理する全メモリ領域のバイトサイズを計算します。n
は、このMSpan
が割り当てるオブジェクトのサイズです。RefcountOverhead
は、各オブジェクトのGC参照カウントに必要な追加のメモリオーバーヘッドです。- この行は、
MSpan
が保持できるオブジェクトの総数nobj
を正確に計算します。
- 修正された
if
ブロック (if((byte*)s->gcref < p || (byte*)(s->gcref+nobj) > p+(s->npages<<PageShift))
):- 以前の条件
if((byte*)s->gcref < p || (byte*)s->gcref >= p+(s->npages<<PageShift))
は、gcref
の開始アドレスがスパンの範囲内にあるかのみをチェックしていました。 - 修正後では、
gcref
領域の開始アドレスがスパンの開始アドレスp
より小さいか、またはgcref
領域の終了アドレス(s->gcref + nobj
)がスパンの終了アドレスp+(s->npages<<PageShift)
を超えているかをチェックします。 - これにより、
gcref
領域全体がMSpan
の割り当てられたメモリ範囲内に完全に収まっていることを保証し、メモリの整合性を保ちます。もしこの条件が満たされない場合、「bad gcref」というエラーでプログラムが終了します。
- 以前の条件
これらの変更は、GoのGCがメモリをより正確に管理し、内部データ構造とユーザーデータ領域を明確に区別することで、GCのバグを防ぎ、ランタイムの安定性を向上させるために不可欠でした。
関連リンク
参考にした情報源リンク
- Go Data Structures - Russ Cox's blog post (swtch.com) (Web検索で得られた情報源。GoのGC性能に関するRuss Coxのコメントが含まれる。)