[インデックス 18340] ファイルの概要
このコミットは、Goランタイムのメモリ管理におけるページサイズを4KBから8KBに増加させる重要な変更を導入しています。この変更は、ガベージコレクション(GC)の一時停止時間を短縮し、データTLB(Translation Lookaside Buffer)のプレッシャーを軽減することを目的としています。
コミット
commit 6d603af6dc298d23a5100328f0006ccc8ac3a1ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 23 18:59:43 2014 +0400
runtime: increase page size to 8K
Tcmalloc uses 8K, 32K and 64K pages, and in custom setups 256K pages.
Only Chromium uses 4K pages today (in "slow but small" configuration).
The general tendency is to increase page size, because it reduces
metadata size and DTLB pressure.
This change reduces GC pause by ~10% and slightly improves other metrics.
json-1
allocated 8037492 8038689 +0.01%
allocs 105762 105573 -0.18%
cputime 158400000 155800000 -1.64%
gc-pause-one 4412234 4135702 -6.27%
gc-pause-total 2647340 2398707 -9.39%
rss 54923264 54525952 -0.72%
sys-gc 3952624 3928048 -0.62%
sys-heap 46399488 46006272 -0.85%
sys-other 5597504 5290304 -5.49%
sys-stack 393216 393216 +0.00%
sys-total 56342832 55617840 -1.29%
time 158478890 156046916 -1.53%
virtual-mem 256548864 256593920 +0.02%
garbage-1
allocated 2991113 2986259 -0.16%
allocs 62844 62652 -0.31%
cputime 16330000 15860000 -2.88%
gc-pause-one 789108229 725555211 -8.05%
gc-pause-total 3945541 3627776 -8.05%
rss 1143660544 1132253184 -1.00%
sys-gc 65609600 65806208 +0.30%
sys-heap 1032388608 1035599872 +0.31%
sys-other 37501632 22777664 -39.26%
sys-stack 8650752 8781824 +1.52%
sys-total 1144150592 1132965568 -0.98%
time 16364602 15891994 -2.89%
virtual-mem 1327296512 1313746944 -1.02%
R=golang-codereviews, dave, khr, rsc, khr
CC=golang-codereviews
https://golang.org/cl/45770044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6d603af6dc298d23a5100328f0006ccc8ac3a1ae
元コミット内容
Goランタイムのページサイズを8KBに増加させる。 Tcmallocは8KB、32KB、64KB、そしてカスタム設定では256KBのページを使用している。 現在、Chromiumのみが4KBページを使用している(「遅いが小さい」設定)。 ページサイズを大きくする一般的な傾向は、メタデータサイズとDTLBのプレッシャーを軽減するためである。 この変更により、GCの一時停止時間が約10%短縮され、他のメトリクスもわずかに改善される。
json-1およびgarbage-1ベンチマークの結果が示されており、特にgc-pause-total
が大幅に改善されていることが強調されている。
変更の背景
この変更の主な背景には、メモリ管理の効率化とパフォーマンスの向上が挙げられます。コミットメッセージにもあるように、Tcmalloc(Googleが開発した高性能なメモリ割り当てライブラリ)のような現代的なメモリ管理システムは、より大きなページサイズ(8KB以上)を使用する傾向にあります。これは、ページサイズを大きくすることで、以下の利点が得られるためです。
- メタデータサイズの削減: メモリを管理するための内部データ構造(例えば、どのページが使用中か、空いているかなどの情報)の量が減ります。ページあたりの管理オーバーヘッドが相対的に小さくなるため、メモリ使用効率が向上します。
- DTLB (Data Translation Lookaside Buffer) プレッシャーの軽減: DTLBは、仮想アドレスから物理アドレスへの変換を高速化するためのCPUキャッシュです。ページサイズが小さいと、同じ量のメモリにアクセスするためにより多くのTLBエントリが必要となり、TLBミスが増加する可能性があります。TLBミスはCPUがメインメモリにアクセスしてページテーブルをウォークする必要があるため、パフォーマンスの低下を招きます。ページサイズを大きくすることで、TLBがカバーできる仮想アドレス空間の範囲が広がり、TLBミスが減少するため、DTLBのプレッシャーが軽減され、全体的なパフォーマンスが向上します。
Goランタイムは、効率的なメモリ管理と低レイテンシのガベージコレクションを重視しています。このコミットは、これらの目標を達成するために、当時の一般的な傾向と他の高性能メモリ管理システムのベストプラクティス(Tcmallocの8KBページ使用など)に合わせたものです。ベンチマーク結果が示すように、GCの一時停止時間の顕著な削減は、この変更がGoアプリケーションの応答性とスループットに直接的な好影響を与えることを意味します。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
- Goランタイム: Goプログラムの実行を管理するシステム。ガベージコレクタ、スケジューラ、メモリ割り当てなどが含まれます。
- メモリページ (Memory Page): オペレーティングシステムがメモリを管理する最小単位。通常、4KB、8KB、またはそれ以上の固定サイズです。仮想メモリシステムでは、プログラムは仮想アドレスを使用し、OSがこれを物理アドレスにマッピングします。このマッピングはページ単位で行われます。
- ページサイズ (Page Size): メモリページの大きさ。一般的なシステムでは4KBがデフォルトですが、より大きなページサイズ(Huge Pagesなど)も利用可能です。
- ガベージコレクション (Garbage Collection, GC): プログラムが不要になったメモリを自動的に解放するプロセス。GoのGCは並行・並行型であり、アプリケーションの実行と同時に動作しますが、完全に一時停止しないわけではありません。GCの一時停止時間(GC Pause)は、アプリケーションの応答性に直接影響します。
- Tcmalloc: Googleが開発したスレッドキャッシュ付きの高性能なメモリ割り当てライブラリ(Thread-Caching Malloc)。C++プログラムで広く使用されており、特にマルチスレッド環境でのメモリ割り当て性能に優れています。Tcmallocは、様々なサイズのオブジェクトを効率的に管理するために、異なるサイズのページ(8KB、32KBなど)を使用します。
- TLB (Translation Lookaside Buffer): CPU内部にあるキャッシュの一種で、仮想アドレスから物理アドレスへの変換情報を格納します。TLBは、メモリ参照のたびにページテーブルをウォークするオーバーヘッドを避けるために使用されます。
- DTLB (Data Translation Lookaside Buffer): データアクセスに特化したTLB。命令アクセス用のITLB(Instruction TLB)と区別されます。
- DTLBプレッシャー (DTLB Pressure): DTLBのキャッシュミス率が高い状態を指します。DTLBミスが発生すると、CPUはメインメモリ上のページテーブルを辿ってアドレス変換を行う必要があり、これがパフォーマンスのボトルネックとなります。ページサイズが小さいほど、同じ量のメモリをカバーするためにより多くのTLBエントリが必要となり、TLBミスが発生しやすくなります。
- メモリ割り当てのメタデータ (Metadata): メモリ割り当てシステムが、割り当てられたメモリブロックのサイズ、状態、所有者などを追跡するために使用する内部データ。
技術的詳細
このコミットの技術的詳細は、Goランタイムのメモリ管理サブシステムにおける複数の定数とロジックの変更に集約されます。
-
ページサイズの変更 (
src/pkg/runtime/malloc.h
):PageShift
定数が12
から13
に変更されました。これにより、PageSize
が1 << 12 = 4096
バイト(4KB)から1 << 13 = 8192
バイト(8KB)に倍増します。これはコミットの核心となる変更です。NumSizeClasses
が61
から67
に増加しました。これは、Goランタイムが管理するメモリ割り当てサイズクラスの数が増えたことを意味します。ページサイズが大きくなったことで、メモリをより効率的に細分化して割り当てるために、より多くのサイズクラスが必要になったと考えられます。
-
MStats
構造体の互換性 (src/pkg/runtime/malloc.goc
およびsrc/pkg/runtime/mgc0.c
):src/pkg/runtime/malloc.goc
では、runtime·sizeof_C_MStats
の計算ロジックが変更されました。これは、GoのMemStats
構造体とCのMStats
構造体の間で、by_size
配列のサイズが異なる場合に互換性を保つためのものです。NumSizeClasses
の変更により、この配列のサイズがGoとCで食い違う可能性が生じたため、C側でGoの構造体のサイズを正しく認識するための調整が行われました。src/pkg/runtime/mgc0.c
のruntime·ReadMemStats
関数では、*stats = mstats;
という直接的な構造体コピーから、runtime·memcopy(runtime·sizeof_C_MStats, stats, &mstats);
という明示的なメモリコピーに変更されました。これは、前述のMStats
構造体のサイズ不一致に対応し、C側が認識する正しいサイズでデータをコピーすることを保証します。
-
GC内部バッファとブロックサイズの固定化 (
src/pkg/runtime/mgc0.c
およびsrc/pkg/runtime/netpoll.goc
):src/pkg/runtime/mgc0.c
では、GCの内部で使用されるWorkbufSize
(16KB)、RootBlockSize
(4KB)、FinBlockSize
(4KB)といった定数が導入されました。Workbuf
のサイズ計算がPageSize
に依存する形から、新しく定義されたWorkbufSize
に依存する形に変更されました。これにより、Workbuf
のサイズがメインのページサイズ変更の影響を受けずに固定されます。runtime·queuefinalizer
関数では、ファイナライザブロックの割り当てにPageSize
ではなくFinBlockSize
(4KB)が使用されるようになりました。src/pkg/runtime/netpoll.goc
では、ネットワークポーラーのディスクリプタブロックの割り当てにPollBlockSize
(4KB)が導入され、PageSize
の代わりにこれを使用するようになりました。- これらの変更は、特定の内部バッファやブロックのサイズを、メインのページサイズから独立させることで、より予測可能で安定したメモリ使用を保証するためのものです。これにより、これらのコンポーネントがページサイズ変更の影響を直接受けず、特定のパフォーマンス特性を維持できるようになります。
-
memStats
変数の初期化タイミングの変更 (src/pkg/runtime/mem.go
):var memStats MemStats
の宣言がグローバルスコープからinit()
関数内に移動しました。これは、memStats
の初期化がinit()
関数内で明示的に行われるように変更されたことを示唆しています。これにより、sizeof_C_MStats
とのサイズチェックがより適切に行われるようになります。
これらの変更は、Goランタイムがより大きなメモリページを効率的に利用し、GCのオーバーヘッドを削減し、全体的なパフォーマンスを向上させるための包括的なアプローチを示しています。特に、MStats
の互換性維持と、特定の内部バッファサイズの固定化は、ページサイズ変更による潜在的な問題を回避し、システムの安定性を保つための重要な考慮事項です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/runtime/malloc.h
:PageShift
定数の値が12
から13
に変更。NumSizeClasses
定数の値が61
から67
に変更。
-
src/pkg/runtime/malloc.goc
:runtime·sizeof_C_MStats
の計算ロジックが変更され、NumSizeClasses
の変更に対応。
-
src/pkg/runtime/mem.go
:memStats
変数の宣言がグローバルからinit()
関数内に移動。
-
src/pkg/runtime/mgc0.c
:WorkbufSize
,RootBlockSize
,FinBlockSize
といった新しい定数の追加。Workbuf
構造体のSIZE
マクロの定義変更。scanblock
関数内のWorkbuf
サイズチェックの条件変更。runtime·queuefinalizer
関数でpersistentalloc
にFinBlockSize
を使用するよう変更。runtime·ReadMemStats
関数でruntime·memcopy
を使用するよう変更。
-
src/pkg/runtime/netpoll.goc
:PollBlockSize
定数の追加。allocPollDesc
関数でPollBlockSize
を使用するよう変更。
コアとなるコードの解説
src/pkg/runtime/malloc.h
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -90,7 +90,7 @@ typedef struct GCStats GCStats;
enum
{
- PageShift = 12,
+ PageShift = 13,
PageSize = 1<<PageShift,
PageMask = PageSize - 1,
};
@@ -103,7 +103,7 @@ enum
// size classes. NumSizeClasses is that number. It's needed here
// because there are static arrays of this length; when msize runs its
// size choosing algorithm it double-checks that NumSizeClasses agrees.
- NumSizeClasses = 61,
+ NumSizeClasses = 67,
// Tunable constants.
MaxSmallSize = 32<<10,
この変更は、Goランタイムのメモリ管理の根幹をなす部分です。PageShift
が12
から13
に変更されたことで、PageSize
は2^12 = 4096
バイト(4KB)から2^13 = 8192
バイト(8KB)に倍増します。これにより、GoランタイムがOSから取得し、内部で管理するメモリの最小単位が8KBになります。これは、前述のメタデータ削減とDTLBプレッシャー軽減に直接寄与します。
NumSizeClasses
の増加は、8KBページという新しい基盤に合わせて、Goランタイムが管理するオブジェクトサイズクラスの粒度を調整したことを示しています。これにより、様々なサイズのオブジェクトに対してより効率的なメモリ割り当てが可能になります。
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -320,7 +321,10 @@ runtime·purgecachedstats(MCache *c)
}
}
-uintptr runtime·sizeof_C_MStats = sizeof(MStats);
+// Size of the trailing by_size array differs between Go and C,
+// NumSizeClasses was changed, but we can not change Go struct because of backward compatibility.
+// sizeof_C_MStats is what C thinks about size of Go struct.
+uintptr runtime·sizeof_C_MStats = sizeof(MStats) - (NumSizeClasses - 61) * sizeof(mstats.by_size[0]);
この変更は、GoとCのコード間でMStats
構造体の互換性を維持するためのものです。NumSizeClasses
が変更されたため、Go側で定義されたMemStats
(GoのMStats
に対応)とC側で定義されたMStats
のby_size
配列のサイズが異なる可能性があります。runtime·sizeof_C_MStats
は、CコードがGoのMStats
構造体のサイズを正しく認識するために使用されます。この計算式は、NumSizeClasses
の変更によって生じるby_size
配列のサイズ差を補正し、GoとCの間のデータ交換が正しく行われるようにします。
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -25,6 +25,10 @@ enum {
wordsPerBitmapWord = sizeof(void*)*8/4,
bitShift = sizeof(void*)*8/4,
+ WorkbufSize = 16*1024,
+ RootBlockSize = 4*1024,
+ FinBlockSize = 4*1024,
+
handoffThreshold = 4,
IntermediateBufferCapacity = 64,
@@ -143,11 +147,10 @@ struct Obj
uintptr ti; // type info
};
-// The size of Workbuf is N*PageSize.
typedef struct Workbuf Workbuf;
struct Workbuf
{
-#define SIZE (2*PageSize-sizeof(LFNode)-sizeof(uintptr))
+#define SIZE (WorkbufSize-sizeof(LFNode)-sizeof(uintptr))
LFNode node; // must be first
uintptr nobj;
Obj obj[SIZE/sizeof(Obj) - 1];
@@ -726,7 +729,7 @@ scanblock(Workbuf *wbuf, bool keepworking)
ChanType *chantype;
Obj *wp;
-\tif(sizeof(Workbuf) % PageSize != 0)
+\tif(sizeof(Workbuf) % WorkbufSize != 0)
\truntime·throw("scanblock: size of Workbuf is suboptimal");
// Memory arena parameters.
@@ -1587,8 +1590,8 @@ runtime·queuefinalizer(byte *p, FuncVal *fn, uintptr nret, Type *fint, PtrType
runtime·lock(&finlock);
if(finq == nil || finq->cnt == finq->cap) {
if(finc == nil) {
-\t\t\tfinc = runtime·persistentalloc(PageSize, 0, &mstats.gc_sys);
-\t\t\tfinc->cap = (PageSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;
+\t\t\tfinc = runtime·persistentalloc(FinBlockSize, 0, &mstats.gc_sys);
+\t\t\tfinc->cap = (FinBlockSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;
\t\tfinc->alllink = allfin;
\t\tallfin = finc;
}
@@ -2215,6 +2218,8 @@ gc(struct gc_args *args)
runtime·MProf_GC();
}
+extern uintptr runtime·sizeof_C_MStats;
+
void
runtime·ReadMemStats(MStats *stats)
{
@@ -2226,7 +2231,9 @@ runtime·ReadMemStats(MStats *stats)
m->gcing = 1;
runtime·stoptheworld();
updatememstats(nil);
-\t*stats = mstats;
+\t// Size of the trailing by_size array differs between Go and C,
+\t// NumSizeClasses was changed, but we can not change Go struct because of backward compatibility.
+\truntime·memcopy(runtime·sizeof_C_MStats, stats, &mstats);
m->gcing = 0;
m->locks++;
runtime·semrelease(&runtime·worldsema);
このファイルでは、GCの内部動作に関連するメモリ割り当ての変更が行われています。
WorkbufSize
、RootBlockSize
、FinBlockSize
といった定数の導入は、GCが使用する特定のバッファやブロックのサイズを、メインのPageSize
から独立して固定することを目的としています。これにより、GCの内部メモリ管理がより安定し、予測可能になります。例えば、Workbuf
のサイズがPageSize
の変更に影響されなくなったことで、GCのワーキングメモリの挙動が安定します。
runtime·memcopy
の使用は、前述のMStats
構造体の互換性問題に対応するためのものです。
src/pkg/runtime/netpoll.goc
--- a/src/pkg/runtime/netpoll.goc
+++ b/src/pkg/runtime/netpoll.goc
@@ -34,6 +34,11 @@ package net
#define READY ((G*)1)
#define WAIT ((G*)2)
+enum
+{
+ PollBlockSize = 4*1024,
+};
+
struct PollDesc
{
PollDesc* link; // in pollcache, protected by pollcache.Lock
@@ -422,7 +427,7 @@ allocPollDesc(void)
runtime·lock(&pollcache);
if(pollcache.first == nil) {
-\t\tn = PageSize/sizeof(*pd);\
+\t\tn = PollBlockSize/sizeof(*pd);\
if(n == 0)\
n = 1;\
// Must be in non-GC memory because can be referenced
このファイルでは、ネットワークポーラーが使用するPollDesc
(ポーリングディスクリプタ)の割り当て方法が変更されています。PollBlockSize
(4KB)が導入され、allocPollDesc
関数がPageSize
の代わりにこれを使用するようになりました。これは、ネットワークI/Oに関連する内部データ構造の割り当てを、メインのページサイズ変更から独立させ、特定のパフォーマンス特性を維持するためのものです。
これらの変更は全体として、Goランタイムがより大きなメモリページを効率的に利用し、GCの一時停止時間を短縮し、全体的なパフォーマンスを向上させるための、細部にわたる調整と最適化を示しています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのガベージコレクションに関する情報: https://golang.org/doc/gc-guide (当時の情報とは異なる可能性があります)
- TcmallocのGitHubリポジトリ: https://github.com/gperftools/gperftools
参考にした情報源リンク
- コミットメッセージと差分情報: https://github.com/golang/go/commit/6d603af6dc298d23a5100328f0006ccc8ac3a1ae
- Goのメモリ管理に関する一般的な知識
- オペレーティングシステムのメモリ管理(ページング、TLB)に関する一般的な知識
- Tcmallocに関する一般的な知識
- Goのガベージコレクションに関する一般的な知識
- Goのソースコード(当時のバージョン)