[インデックス 18350] ファイルの概要
このコミットは、Goランタイムのメモリ管理、特にアロケータの最適化に関するものです。Goのガベージコレクタ(GC)の効率を向上させるため、非常に小さな(16バイト未満の)ポインタを含まない(NoScan)オブジェクトのアロケーションを結合する「タイニーアロケータ(Tiny Allocator)」を導入しています。これにより、アロケーション回数の削減、CPU時間の短縮、GCポーズ時間の改善、メモリ使用量の削減といった顕著なパフォーマンス向上が実現されています。
コミット
commit 1fa702942582645efc71a44a4899f51af759694e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Jan 24 22:35:11 2014 +0400
runtime: combine small NoScan allocations
Combine NoScan allocations < 16 bytes into a single memory block.
Reduces number of allocations on json/garbage benchmarks by 10+%.
json-1
allocated 8039872 7949194 -1.13%
allocs 105774 93776 -11.34%
cputime 156200000 100700000 -35.53%
gc-pause-one 4908873 3814853 -22.29%
gc-pause-total 2748969 2899288 +5.47%
rss 52674560 43560960 -17.30%
sys-gc 3796976 3256304 -14.24%
sys-heap 43843584 35192832 -19.73%
sys-other 5589312 5310784 -4.98%
sys-stack 393216 393216 +0.00%
sys-total 53623088 44153136 -17.66%
time 156193436 100886714 -35.41%
virtual-mem 256548864 256540672 -0.00%
garbage-1
allocated 2996885 2932982 -2.13%
allocs 62904 55200 -12.25%
cputime 17470000 17400000 -0.40%
gc-pause-one 932757485 925806143 -0.75%
gc-pause-total 4663787 4629030 -0.75%
rss 1151074304 1133670400 -1.51%
sys-gc 66068352 65085312 -1.49%
sys-heap 1039728640 1024065536 -1.51%
sys-other 38038208 37485248 -1.45%
sys-stack 8650752 8781824 +1.52%
sys-total 1152485952 1135417920 -1.48%
time 17478088 17418005 -0.34%
virtual-mem 1343709184 1324204032 -1.45%
LGTM=iant, bradfitz
R=golang-codereviews, dave, iant, rsc, bradfitz
CC=golang-codereviews, khr
https://golang.org/cl/38750047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1fa702942582645efc71a44a4899f51af759694e
元コミット内容
このコミットは、Goランタイムにおいて、16バイト未満の小さなポインタを含まない(NoScan)アロケーションを単一のメモリブロックに結合する「タイニーアロケータ」を導入するものです。これにより、json
およびgarbage
ベンチマークにおいて、アロケーション回数が10%以上削減されることが示されています。
ベンチマーク結果は以下の通りです。
json-1 ベンチマーク:
allocated
: -1.13%allocs
: -11.34% (アロケーション回数が大幅に減少)cputime
: -35.53% (CPU時間が大幅に短縮)gc-pause-one
: -22.29% (単一GCポーズ時間が短縮)rss
: -17.30% (Resident Set Size、実メモリ使用量が減少)sys-heap
: -19.73% (ヒープメモリ使用量が減少)time
: -35.41% (実行時間が大幅に短縮)
garbage-1 ベンチマーク:
allocated
: -2.13%allocs
: -12.25% (アロケーション回数が大幅に減少)rss
: -1.51%sys-heap
: -1.51%
これらの結果は、タイニーアロケータの導入がGoプログラムのパフォーマンス、特にメモリ使用量とGC効率に大きな改善をもたらすことを示しています。
変更の背景
Goのガベージコレクタは、ヒープ上に割り当てられたオブジェクトを追跡し、不要になったメモリを解放する役割を担っています。しかし、非常に小さなオブジェクトが頻繁に割り当てられる場合、個々のオブジェクトのアロケーションと解放のオーバーヘッドが無視できないものとなります。特に、ポインタを含まない(NoScan)小さなオブジェクト(例:短い文字列、小さな構造体など)は、GCがポインタをスキャンする必要がないため、特別な最適化の対象となり得ます。
従来のGoランタイムでは、すべてのオブジェクトが個別にアロケートされ、GCの対象となっていました。これにより、多数の小さなオブジェクトが生成されるアプリケーションでは、アロケーションの頻度が高くなり、GCの負荷が増大し、結果としてCPU時間やGCポーズ時間の増加につながっていました。
このコミットの背景には、このような小さなオブジェクトのアロケーションオーバーヘッドを削減し、Goプログラム全体のパフォーマンスを向上させるという目的があります。具体的には、複数の小さなNoScanオブジェクトをより大きな単一のメモリブロックにまとめて割り当てることで、アロケーション回数を減らし、GCの効率を高めることが狙いです。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとメモリ管理に関する基本的な知識が必要です。
-
Goのメモリ管理とガベージコレクション (GC):
- Goは自動メモリ管理を採用しており、開発者が手動でメモリを解放する必要はありません。
- GoのGCは、主にマーク&スイープ方式を採用しています。これは、到達可能なオブジェクトをマークし、マークされなかった(到達不可能な)オブジェクトをスイープ(解放)するプロセスです。
- GCは、プログラムの実行中に一時的に停止(ポーズ)することがあり、このポーズ時間がアプリケーションのレイテンシに影響を与えることがあります。GoのGCは、ポーズ時間を最小限に抑えるように設計されています。
- NoScanオブジェクト: Goの型システムは、オブジェクトがポインタを含むかどうかを識別できます。ポインタを含まないオブジェクト(例:
int
,float64
,byte
の配列、string
など)は「NoScan」オブジェクトと呼ばれます。GCはこれらのオブジェクトの内部をスキャンして他のオブジェクトへの参照を探す必要がないため、GCの処理が高速になります。
-
Goのアロケータ (runtime.mallocgc):
- Goプログラムがメモリを要求すると、
runtime.mallocgc
関数が呼び出されます。 mallocgc
は、要求されたサイズに基づいて、適切なメモリ領域をヒープから割り当てます。- Goのアロケータは、様々なサイズのオブジェクトに対応するために、複数のフリーリストとメモリ管理戦略を持っています。
- MHeap, MSpan, MCache: Goのメモリヒープは、
MHeap
によって管理されます。MHeap
は、ページ(通常8KB)の単位でメモリを管理し、これらのページをMSpan
という構造体で表現します。各ゴルーチン(またはP, Processor)は、ローカルなメモリキャッシュであるMCache
を持ち、小さなオブジェクトのアロケーションを高速化します。これにより、グローバルなヒープロックを避けることができます。
- Goプログラムがメモリを要求すると、
-
Finalizer:
- Goでは、
runtime.SetFinalizer
関数を使って、オブジェクトがGCによって回収される直前に実行される関数(ファイナライザ)を設定できます。 - ファイナライザは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップするのに役立ちます。
- ファイナライザが設定されたオブジェクトは、GCがそのオブジェクトを回収可能と判断した後、特別なキューに入れられ、ファイナライザが実行されるまでメモリが解放されません。
- Goでは、
-
アロケーションのオーバーヘッド:
- メモリを割り当てる際には、アロケータの内部処理(フリーリストの探索、メモリブロックの初期化、GCメタデータの更新など)にCPU時間とメモリ帯域が消費されます。
- 小さなオブジェクトを個別に頻繁に割り当てる場合、このオーバーヘッドが累積され、パフォーマンスのボトルネックとなることがあります。
これらの知識を前提として、このコミットがどのようにGoランタイムのメモリ管理を改善しているかを深く掘り下げていきます。
技術的詳細
このコミットの核心は、Goランタイムに「タイニーアロケータ(Tiny Allocator)」を導入することです。タイニーアロケータは、特定の条件を満たす非常に小さなオブジェクトのアロケーションを最適化します。
タイニーアロケータの動作原理:
-
対象オブジェクト:
- サイズが
TinySize
(このコミットでは16バイト)未満であること。 - ポインタを含まない(
FlagNoScan
)オブジェクトであること。 - GCの対象外ではない(
FlagNoGC
ではない)こと。 これらの条件を満たすオブジェクトは、タイニーアロケータの対象となります。
- サイズが
-
アロケーションの結合:
- 各
P
(プロセッサ、Goスケジューラにおける論理的なCPU)は、tiny
とtinysize
というフィールドをP
構造体内に持ちます。 tiny
は現在使用中のタイニーブロックの先頭ポインタを指し、tinysize
はそのブロックの残りの空きサイズを示します。runtime.mallocgc
がタイニーアロケータの対象となるオブジェクトのアロケーション要求を受け取ると、まず現在のP
のtiny
ブロックに十分な空きがあるかを確認します。- もし空きがあれば、そのブロックから必要なサイズを切り出し、
tiny
ポインタとtinysize
を更新します。これにより、複数の小さなオブジェクトが同じ16バイトのブロック内に連続して配置されます。 - 空きがない場合、または要求されたオブジェクトが現在の
tiny
ブロックに収まらない場合、新しいTinySize
(16バイト)のブロックがMCache
のフリーリストから割り当てられます。この新しいブロックがP
のtiny
ブロックとして設定され、要求されたオブジェクトがその先頭に配置されます。
- 各
-
メモリの節約とオーバーヘッド削減:
- 複数の小さなオブジェクトを1つの16バイトブロックにまとめることで、個々のアロケーションに必要なアロケータの処理(フリーリストの探索、メタデータの更新など)が大幅に削減されます。
- これにより、
allocs
(アロケーション回数)が減少し、結果としてCPU時間も削減されます。 - GCの観点からは、16バイトのブロック全体が1つのオブジェクトとして扱われるため、GCがスキャンする必要があるオブジェクトの総数が減り、GCの効率が向上します。特にNoScanオブジェクトであるため、ブロック全体をスキャンする必要がなく、GCポーズ時間の短縮に寄与します。
-
Finalizerとの連携:
- タイニーアロケータによって割り当てられたオブジェクトは、実際にはより大きなブロックの一部です。
runtime.SetFinalizer
は、オブジェクトの先頭アドレスに対してファイナライザを設定することを期待しますが、タイニーアロケータの場合、ユーザーがファイナライザを設定しようとするアドレスがブロックの先頭ではない可能性があります。- このコミットでは、
runtime.SetFinalizer
、GCのマークフェーズ (markroot
)、およびスイープフェーズ (sweepspan
) において、ファイナライザが設定されたオブジェクトがタイニーアロケータによって割り当てられたものである可能性を考慮し、実際のオブジェクトの開始アドレスを正しく特定するようにロジックが修正されています。具体的には、special->offset/size*size
のような計算を用いて、ファイナライザが設定されたバイトが属するオブジェクトの先頭アドレスを導出しています。 - また、明示的に解放されるオブジェクト(
runtime.free
)は、TinySize
以上でなければならないという制約が追加されました。これは、タイニーアロケータによって割り当てられたオブジェクトが個別に解放されることを防ぐためです。タイニーアロケータのオブジェクトは、そのブロック全体がGCによって回収されるまで解放されません。
-
チューニングとトレードオフ:
TinySize
(16バイト)はチューニング可能な定数です。コミットメッセージには、8バイト、16バイト、32バイトの選択肢とその影響が説明されています。- 8バイト: メモリの無駄が全くないが、結合の機会が少ない。
- 16バイト: 最悪の場合2倍のメモリの無駄が発生する可能性があるが、結合の機会が増える。
- 32バイト: 最悪の場合4倍のメモリの無駄が発生する可能性があるが、結合の機会がさらに増える。
- この選択は、アロケーション回数の削減とメモリの無駄のバランスに基づいています。16バイトは、
json
ベンチマークでアロケーション回数を約12%削減し、ヒープサイズを約20%削減するという良好な結果をもたらしました。
このタイニーアロケータは、Goのメモリ管理戦略における重要な改善であり、特に小さなデータ構造を多用するアプリケーションにおいて、顕著なパフォーマンス向上をもたらします。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/runtime/malloc.goc
ファイルに集中しています。
-
src/pkg/runtime/malloc.goc
:runtime·mallocgc
関数内に、FlagNoScan
かつsize < TinySize
の条件を満たすオブジェクトに対する新しいアロケーションパスが追加されました。これがタイニーアロケータの主要なロジックです。P
構造体(m->p
)のtiny
とtinysize
フィールドを利用して、現在のタイニーブロックからのアロケーションを試みます。- 新しいタイニーブロックが必要な場合、
MCache
のTinySizeClass
フリーリストから取得します。 - 大きなオブジェクトのアロケーションロジックが、新しく抽出された
largealloc
関数に移動されました。 runtime·free
関数に、解放されるブロックのサイズがTinySize
未満である場合にパニックを起こすチェックが追加されました。これは、タイニーアロケータによって管理されるオブジェクトが誤って個別に解放されるのを防ぐためです。
-
src/pkg/runtime/malloc.h
:TinySize
とTinySizeClass
という新しい定数が定義されました。TinySize
は16バイトに設定されています。
-
src/pkg/runtime/runtime.h
:P
構造体に、タイニーアロケータの状態を保持するためのbyte* tiny
とuintptr tinysize
フィールドが追加されました。
-
src/pkg/runtime/mgc0.c
:- GCのルートスキャン (
markroot
) およびスイープ (sweepspan
) 処理において、ファイナライザが設定されたオブジェクトがタイニーアロケータによって割り当てられたものである場合、そのオブジェクトの実際の開始アドレスを正しく特定するためのロジックが修正されました。これは、ファイナライザがオブジェクトの内部バイトに設定される可能性があるためです。 clearpools
関数で、P
のtiny
とtinysize
フィールドがクリアされるようになりました。
- GCのルートスキャン (
-
src/pkg/runtime/mheap.c
:removespecial
とruntime·freeallspecials
関数が、ファイナライザのオフセット処理をより柔軟に扱うように調整されました。
-
テストファイル (
sync/pool_test.go
,test/deferfin.go
,test/fixedbugs/issue4618.go
,test/fixedbugs/issue4667.go
,test/tinyfin.go
):- 既存のテストがタイニーアロケータの変更に対応するように修正されました(例:
new(int)
をnew(string)
に変更するなど)。 test/tinyfin.go
という新しいテストファイルが追加され、タイニーアロケータによって結合されたオブジェクトに対するファイナライザの動作が正しく行われることを検証しています。
- 既存のテストがタイニーアロケータの変更に対応するように修正されました(例:
コアとなるコードの解説
src/pkg/runtime/malloc.goc
におけるruntime·mallocgc
関数の変更がこのコミットの最も重要な部分です。
// src/pkg/runtime/malloc.goc (抜粋)
void*
runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
{
// ... (既存の初期化とサイズ0のハンドリング) ...
// debug.efenceが有効でなく、かつサイズがMaxSmallSize以下の場合
if(!runtime·debug.efence && size <= MaxSmallSize) {
// タイニーアロケータの条件チェック
// FlagNoScan (ポインタを含まない) かつ size < TinySize (16バイト未満)
if((flag&(FlagNoScan|FlagNoGC)) == FlagNoScan && size < TinySize) {
// Tiny allocator.
// ... (タイニーアロケータに関する詳細なコメント) ...
p = m->p; // 現在のP (プロセッサ) を取得
tinysize = p->tinysize; // Pが持つ現在のタイニーブロックの残りサイズ
if(size <= tinysize) {
tiny = p->tiny; // Pが持つ現在のタイニーブロックのポインタ
// アライメント調整
if((size&7) == 0)
tiny = (byte*)ROUND((uintptr)tiny, 8);
else if((size&3) == 0)
tiny = (byte*)ROUND((uintptr)tiny, 4);
else if((size&1) == 0)
tiny = (byte*)ROUND((uintptr)tiny, 2);
size1 = size + (tiny - p->tiny); // アライメント調整後のサイズ
if(size1 <= tinysize) {
// オブジェクトが既存のタイニーブロックに収まる場合
v = (MLink*)tiny; // 割り当てるオブジェクトのポインタ
p->tiny += size1; // tinyポインタを進める
p->tinysize -= size1; // 残りサイズを減らす
// ... (ロックとプリエンプションの処理) ...
return v; // 割り当てられたポインタを返す
}
}
// 新しいTinySizeブロックを割り当てる
l = &c->list[TinySizeClass]; // MCacheからTinySizeClassのフリーリストを取得
if(l->list == nil)
runtime·MCache_Refill(c, TinySizeClass); // フリーリストが空なら補充
v = l->list; // 新しいブロックを取得
l->list = v->next;
l->nlist--;
// 新しいブロックの先頭2ワードをゼロクリア (ポインタを含まないため)
((uint64*)v)[0] = 0;
((uint64*)v)[1] = 0;
// 既存のタイニーブロックよりも新しいブロックの方が空きが多い場合、置き換える
if(TinySize-size > tinysize) {
p->tiny = (byte*)v + size; // 新しいtinyポインタを設定
p->tinysize = TinySize - size; // 新しい残りサイズを設定
}
size = TinySize; // 割り当てられたブロックの実際のサイズはTinySize
goto done; // 割り当て完了後の共通処理へジャンプ
}
// ... (既存のSmallSizeオブジェクトのアロケーションロジック) ...
} else {
// Largeオブジェクトのアロケーションはlargealloc関数に委譲
v = largealloc(flag, &size);
}
// ... (プロファイリング、GCメタデータ更新、ゼロクリアなど共通処理) ...
return v;
}
// src/pkg/runtime/malloc.goc (抜粋)
// runtime·free関数内の変更
void
runtime·free(void *v)
{
// ... (既存の処理) ...
// TinySize未満のブロックが明示的に解放されようとした場合のチェック
if(size < TinySize)
runtime·throw("freeing too small block"); // パニック
// ... (既存の処理) ...
}
解説:
-
runtime·mallocgc
内のタイニーアロケータロジック:if((flag&(FlagNoScan|FlagNoGC)) == FlagNoScan && size < TinySize)
: この条件がタイニーアロケータの対象となるオブジェクトを識別します。FlagNoScan
はオブジェクトがポインタを含まないことを示し、size < TinySize
はオブジェクトが非常に小さいことを示します。p = m->p;
: 現在のゴルーチンが実行されている論理プロセッサ(P)の情報を取得します。各Pは独自のタイニーアロケータキャッシュを持ちます。if(size <= tinysize)
: Pの現在のタイニーブロックに、要求されたオブジェクトを配置するのに十分な空きがあるかをチェックします。- 既存ブロックへの配置: 空きがある場合、
tiny
ポインタをアライメント調整し、そこからオブジェクトを切り出します。p->tiny
とp->tinysize
を更新して、次のアロケーションのためにブロックの残りの部分を準備します。これにより、複数の小さなオブジェクトが1つの16バイトブロック内に連続して割り当てられます。
- 既存ブロックへの配置: 空きがある場合、
- 新しいブロックの割り当て: 既存のタイニーブロックに収まらない場合、またはPがまだタイニーブロックを持っていない場合、
MCache
から新しいTinySize
(16バイト)のブロックを要求します。MCache_Refill
は、必要に応じてヒープから新しいページを取得し、フリーリストを補充します。新しいブロックが取得されたら、その先頭にオブジェクトを配置し、残りの部分をp->tiny
とp->tinysize
に設定して、将来のタイニーアロケーションに備えます。 goto done;
: タイニーアロケータによる割り当てが完了した後、mallocgc
の共通の終了処理(プロファイリング、GCメタデータ更新など)にジャンプします。
-
runtime·free
の変更:if(size < TinySize) runtime·throw("freeing too small block");
: このガードは非常に重要です。タイニーアロケータによって割り当てられたオブジェクトは、個別にruntime·free
で解放されることを意図していません。それらは、より大きな16バイトブロックの一部であり、そのブロック全体がGCによって回収されるときに解放されます。もしユーザーが誤ってタイニーアロケータによって割り当てられたオブジェクトを明示的に解放しようとすると、このチェックによってパニックが発生し、不正なメモリ操作を防ぎます。
これらの変更により、Goランタイムは小さなNoScanオブジェクトのアロケーションを効率的に結合し、メモリ使用量とGCのオーバーヘッドを削減することで、全体的なパフォーマンスを向上させています。
関連リンク
- Goのメモリ管理に関する公式ドキュメントやブログ記事 (当時の情報源を探すのは難しいですが、Goのメモリモデルの進化を追う上で重要です)
- Goのガベージコレクションに関する詳細な解説記事
- Goの
sync.Pool
など、他のメモリ最適化メカニズムに関する情報
参考にした情報源リンク
- Goの公式ドキュメント (Goのメモリ管理とGCの基本的な概念について)
- Goのソースコード (特に
src/runtime
ディレクトリ) - GoのIssueトラッカーやメーリングリスト (このコミットに関連する議論や背景情報)
- Dmitriy Vyukov氏の他のGoランタイム関連のコミットや発表 (Goの並行処理とパフォーマンス最適化に関する彼の貢献は多岐にわたります)
- Goのベンチマークツール (
go test -bench=.
) の使用方法と結果の解釈に関する情報