[インデックス 18383] ファイルの概要
このコミットは、Goランタイムのメモリ管理におけるページサイズを4KBから8KBに増加させる変更を導入しています。この変更は、ガベージコレクション(GC)の一時停止時間を短縮し、その他のパフォーマンス指標をわずかに改善することを目的としています。特に、Tcmallocなどの他のメモリ管理システムがより大きなページサイズを使用している傾向に合わせることで、メタデータサイズとDTLB(Data Translation Lookaside Buffer)の圧力を軽減し、全体的なシステム効率を向上させます。
コミット
commit e48751e217d3f97c92ec210f605454aa97202c3b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 30 13:28:19 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%
This is the exact reincarnation of already LGTMed:
https://golang.org/cl/45770044
which must not break darwin/freebsd after:
https://golang.org/cl/56630043
TBR=iant
LGTM=khr, iant
R=iant, khr
CC=golang-codereviews
https://golang.org/cl/58230043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e48751e217d3f97c92ec210f605454aa97202c3b
元コミット内容
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%
This is the exact reincarnation of already LGTMed:
https://golang.org/cl/45770044
which must not break darwin/freebsd after:
https://golang.org/cl/56630043
TBR=iant
LGTM=khr, iant
R=iant, khr
CC=golang-codereviews
https://golang.org/cl/58230043
変更の背景
このコミットの主な背景には、Goランタイムのメモリ管理におけるパフォーマンス最適化があります。特に、ガベージコレクション(GC)の一時停止時間の削減と、全体的なメモリ効率の向上が挙げられます。
コミットメッセージによると、Goランタイムが当時使用していた4KBのページサイズは、Tcmallocのような他の高性能メモリ管理ライブラリが8KB、32KB、64KB、さらには256KBといったより大きなページサイズを使用している傾向と対照的でした。4KBページを使用しているのは主にChromiumの「遅いが小さい」設定のみであり、一般的な傾向としてはページサイズを大きくすることで、以下の利点が得られると認識されていました。
- メタデータサイズの削減: メモリをページ単位で管理する際、各ページには管理用のメタデータが付随します。ページサイズを大きくすると、同じ量のメモリを管理するために必要なページ数が減り、結果としてメタデータの総量が削減されます。これにより、メモリ使用量が効率化されます。
- DTLB(Data Translation Lookaside Buffer)圧力の軽減: DTLBは、仮想アドレスから物理アドレスへの変換を高速化するためのCPUキャッシュです。ページサイズが小さいと、より多くのページエントリがDTLBにロードされる必要があり、DTLBミス(キャッシュに目的のエントリがない状態)が発生しやすくなります。DTLBミスは、メモリ変換のためにメインメモリへのアクセスが必要となり、パフォーマンスの低下を招きます。ページサイズを大きくすることで、DTLBがカバーできるメモリ範囲が広がり、DTLBミスの発生頻度が減少し、結果としてメモリアクセスのパフォーマンスが向上します。
これらの理由から、Goランタイムのページサイズを8KBに増やすことで、GCの一時停止時間を約10%削減し、その他のパフォーマンス指標もわずかに改善するという具体的な効果が期待されました。コミットメッセージに記載されているベンチマーク結果(json-1とgarbage-1)は、この変更が実際にGC一時停止時間(gc-pause-one
、gc-pause-total
)やCPU時間(cputime
)、総実行時間(time
)などの改善に寄与していることを示しています。
また、この変更は以前に承認された(LGTMed)コミットの再実装であり、特定のプラットフォーム(darwin/freebsd)での互換性を維持しつつ導入されることが強調されています。これは、変更の安定性と広範な適用可能性を確保するための配慮を示しています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
1. メモリページとページング
現代のオペレーティングシステム(OS)は、仮想メモリという概念を使用しています。これは、プログラムが物理メモリを直接扱うのではなく、仮想的なアドレス空間を扱う仕組みです。この仮想アドレス空間は、固定サイズのブロックに分割されており、これを「ページ(Page)」と呼びます。同様に、物理メモリも「ページフレーム(Page Frame)」と呼ばれる同じサイズのブロックに分割されます。
プログラムが仮想アドレスにアクセスしようとすると、OSはページテーブルを使用してその仮想アドレスを対応する物理アドレスに変換します。このプロセスを「ページング(Paging)」と呼びます。ページサイズは、この変換の粒度を決定します。一般的なページサイズは4KBですが、システムによってはより大きなページサイズ(例:2MB、1GB)もサポートされています。
2. TLB(Translation Lookaside Buffer)とDTLB(Data TLB)
ページング処理において、仮想アドレスから物理アドレスへの変換は頻繁に行われるため、この変換を高速化するためのキャッシュがCPU内に存在します。これが「TLB(Translation Lookaside Buffer)」です。TLBは、最近使用された仮想アドレスと物理アドレスの対応関係を記憶しています。
TLBには、命令フェッチのためのITLB(Instruction TLB)と、データアクセス(読み書き)のためのDTLB(Data TLB)があります。このコミットで言及されているのはDTLBであり、データアクセス時のページ変換の効率に直接関係します。
- TLBヒット: 目的の仮想アドレスと物理アドレスの対応がTLB内に見つかる場合。変換が高速に行われます。
- TLBミス: 目的の対応がTLB内に見つからない場合。OSはページテーブルを検索して対応する物理アドレスを見つける必要があり、これはメインメモリへのアクセスを伴うため、非常にコストの高い操作となります。
ページサイズが小さいと、同じ量のメモリをカバーするために必要なページエントリの数が多くなります。これにより、TLBが保持できるエントリ数を超過しやすくなり、TLBミスが増加する可能性があります。TLBミスが増えると、プログラムの実行速度が低下します。
3. ガベージコレクション(GC)と一時停止時間
Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが不要になったメモリ領域を自動的に解放し、再利用可能にするプロセスです。これにより、開発者は手動でのメモリ管理の複雑さから解放されます。
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。GCの実行中、特に「マークフェーズ」の一部では、プログラムの実行を一時的に停止させる必要があります。この停止期間を「GC一時停止(GC Pause)」と呼びます。GC一時停止は、アプリケーションの応答性やレイテンシに直接影響を与えるため、可能な限り短くすることが望ましいとされています。
このコミットでは、ページサイズを大きくすることで、GCの一時停止時間を削減する効果が期待されています。これは、メモリ管理の効率化(メタデータ削減、DTLB圧力軽減)が、GCがメモリをスキャンしたり管理したりする際のオーバーヘッドを減らすことに繋がるためと考えられます。
4. Tcmalloc
Tcmalloc(Thread-Caching Malloc)は、Googleが開発した高性能なメモリ割り当てライブラリです。C++プログラムなどで広く使用されており、マルチスレッド環境でのメモリ割り当てのパフォーマンスを最適化することに特化しています。Tcmallocは、スレッドローカルなキャッシュと中央のヒープを組み合わせることで、ロックの競合を減らし、高速なメモリ割り当てと解放を実現します。
コミットメッセージでTcmallocが参照されているのは、そのメモリ管理戦略、特にページサイズの選択が、Goランタイムのメモリ管理設計の参考になっていることを示唆しています。Tcmallocがより大きなページサイズを使用しているという事実は、Goランタイムも同様の最適化を追求する動機付けとなっています。
技術的詳細
このコミットの技術的詳細を掘り下げると、Goランタイムがメモリページをどのように扱っているか、そしてページサイズ変更がシステム全体にどのような影響を与えるかが明らかになります。
ページサイズの変更 (PageShift
, PageSize
, PageMask
)
最も直接的な変更は、src/pkg/runtime/malloc.h
ファイルにおけるページ関連定数の定義です。
PageShift
は、ページサイズを2のべき乗で表すためのシフト量です。- 変更前:
PageShift = 12
(2^12 = 4096バイト = 4KB) - 変更後:
PageShift = 13
(2^13 = 8192バイト = 8KB)
- 変更前:
PageSize
は、実際のページサイズです。- 変更前:
PageSize = 1<<PageShift
(4KB) - 変更後:
PageSize = 1<<PageShift
(8KB)
- 変更前:
PageMask
は、ページのアドレスをアラインメントするためのマスクです。- 変更前:
PageMask = PageSize - 1
(4KB - 1) - 変更後:
PageMask = PageSize - 1
(8KB - 1)
- 変更前:
この変更により、Goランタイムがメモリを割り当て、管理する際の最小単位が4KBから8KBに倍増します。
サイズクラスの変更 (NumSizeClasses
)
src/pkg/runtime/malloc.h
では、メモリ割り当ての「サイズクラス」の数も変更されています。
NumSizeClasses
は、Goランタイムが管理するオブジェクトのサイズカテゴリの数です。- 変更前:
NumSizeClasses = 61
- 変更後:
NumSizeClasses = 67
- 変更前:
これは、ページサイズが大きくなったことに伴い、メモリ割り当ての粒度や効率を維持するために、より多くのサイズクラスが必要になったことを示唆しています。Goのメモリ管理は、小さなオブジェクトを効率的に管理するために、様々なサイズの「スパン」(連続したページ群)を使用します。ページサイズが大きくなると、スパンの構成も調整する必要があり、それがサイズクラスの増加に繋がったと考えられます。
MStats
構造体のサイズ計算の調整
src/pkg/runtime/malloc.goc
と src/pkg/runtime/mgc0.c
では、MStats
構造体のサイズ計算に関する調整が行われています。
MStats
は、Goランタイムのメモリ統計情報を保持する構造体です。この構造体には、by_size
という配列が含まれており、これは各サイズクラスごとの統計情報を格納します。NumSizeClasses
が変更されたため、by_size
配列のサイズも論理的に変更されます。
しかし、GoとCの間の互換性の問題から、runtime·sizeof_C_MStats
の計算が調整されています。これは、CコードがGoのMStats
構造体を扱う際に、Go側で変更されたNumSizeClasses
の値を直接反映できないため、オフセットを調整して正しいサイズをC側に伝えるためのものです。
// src/pkg/runtime/malloc.goc
uintptr runtime·sizeof_C_MStats = sizeof(MStats) - (NumSizeClasses - 61) * sizeof(mstats.by_size[0]);
この行は、C言語側から見たMStats
構造体のサイズを計算しています。NumSizeClasses
が61から67に増えた場合、by_size
配列のサイズが大きくなりますが、Goの構造体定義がC側と直接同期できないため、差分を引くことでC側が期待するサイズに調整しています。これは、GoとCのランタイムコードが密接に連携しているGoの特性を示しています。
ワークバッファとファイナライザブロックのサイズ調整
src/pkg/runtime/mgc0.c
では、GCのワークバッファ(WorkbufSize
)とファイナライザブロック(FinBlockSize
)のサイズが、以前のPageSize
に依存する形から、固定値に変更されています。
WorkbufSize = 16*1024
(16KB)RootBlockSize = 4*1024
(4KB)FinBlockSize = 4*1024
(4KB)
以前はWorkbuf
のサイズが2*PageSize
に依存していましたが、新しいWorkbufSize
定数を使用するように変更されています。これにより、ワークバッファのサイズがページサイズに直接依存しなくなり、より柔軟なメモリ管理が可能になります。同様に、ファイナライザブロックの割り当てもPageSize
ではなくFinBlockSize
を使用するように変更されています。
これは、GCの内部的なメモリ管理単位を、システム全体のページサイズ変更からある程度独立させる意図があると考えられます。これにより、GCの動作がより予測可能になり、特定のページサイズに強く結合されることを避けることができます。
ネットポーラーのブロックサイズ調整
src/pkg/runtime/netpoll.goc
では、ネットポーラーが使用するPollBlockSize
が定義され、allocPollDesc
関数でこの新しい定数が使用されています。
PollBlockSize = 4*1024
(4KB)
以前はPageSize
を使用していましたが、PollBlockSize
という新しい固定値を使用することで、ネットポーラーの内部メモリ割り当てがページサイズ変更の影響を受けにくくなります。これは、特定のサブシステムが独自のメモリ割り当て戦略を持つことを可能にし、全体的なシステムの安定性とパフォーマンスを向上させるための一般的なパターンです。
変更の全体的な影響
これらの変更は、Goランタイムのメモリ管理の根幹に影響を与えます。
- メモリ効率の向上: ページサイズを大きくすることで、ページテーブルエントリの数が減り、OSが管理するメタデータが削減されます。これにより、メモリフットプリントがわずかに減少する可能性があります。
- CPUキャッシュ効率の向上: DTLBミスが減少することで、CPUが仮想アドレスを物理アドレスに変換する際のオーバーヘッドが減り、全体的なCPUパフォーマンスが向上します。
- GCパフォーマンスの改善: GCがメモリをスキャンしたり、解放したりする際に、より大きなページ単位で操作できるようになるため、GCの効率が向上し、結果としてGC一時停止時間が短縮されます。これは、特にレイテンシに敏感なアプリケーションにとって重要な改善です。
- メモリ割り当ての粒度: ページサイズが大きくなることで、小さなオブジェクトの割り当てにおいて内部的な断片化が増える可能性もわずかにありますが、Goのメモリ管理はスモールオブジェクトアロケータによってこの問題を緩和するように設計されています。
NumSizeClasses
の増加も、この断片化を管理するための一環と考えられます。
このコミットは、Goランタイムが継続的にパフォーマンス最適化に取り組んでいること、特にメモリ管理とGCの効率向上に注力していることを明確に示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にGoランタイムのメモリ管理に関連する定数と、それらを使用する内部構造体のサイズ計算、およびメモリ割り当てロジックの調整です。
1. 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,
PageShift
が12
から13
に変更され、これによりPageSize
が 4KB (2^12) から 8KB (2^13) に倍増しました。NumSizeClasses
が61
から67
に変更され、メモリ割り当てのサイズクラスの数が増加しました。
2. src/pkg/runtime/malloc.goc
MStats
構造体のサイズ計算が調整されています。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -417,7 +418,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]);
runtime·sizeof_C_MStats
の計算式が変更され、GoとCの間でのMStats
構造体の互換性を維持するために、NumSizeClasses
の変更を考慮したオフセットが導入されました。
3. src/pkg/runtime/mgc0.c
GCの内部で使用されるワークバッファとファイナライザブロックのサイズ定義が追加され、それらを使用するように変更されました。
--- 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,
@@ -154,11 +158,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))\n+#define SIZE (WorkbufSize-sizeof(LFNode)-sizeof(uintptr))\n \tLFNode node; // must be first
\tuintptr nobj;
\tObj obj[SIZE/sizeof(Obj) - 1];
// ...
@@ -737,7 +740,7 @@ scanblock(Workbuf *wbuf, bool keepworking)
ChanType *chantype;
Obj *wp;
-\tif(sizeof(Workbuf) % PageSize != 0)\n+\tif(sizeof(Workbuf) % WorkbufSize != 0)\n \t\truntime·throw(\"scanblock: size of Workbuf is suboptimal\");
// ...
@@ -1601,8 +1604,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);\n-\t\t\tfinc->cap = (PageSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;\n+\t\t\tfinc = runtime·persistentalloc(FinBlockSize, 0, &mstats.gc_sys);\n+\t\t\tfinc->cap = (FinBlockSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;\n \tfinc->alllink = allfin;
allfin = finc;
}
// ...
@@ -2233,6 +2236,8 @@ gc(struct gc_args *args)
runtime·MProf_GC();
}
+extern uintptr runtime·sizeof_C_MStats;
+
void
runtime·ReadMemStats(MStats *stats)
{
@@ -2244,7 +2249,9 @@ runtime·ReadMemStats(MStats *stats)
m->gcing = 1;
runtime·stoptheworld();
updatememstats(nil);
-\t*stats = mstats;\n+\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);\n m->gcing = 0;
m->locks++;
runtime·semrelease(&runtime·worldsema);
WorkbufSize
,RootBlockSize
,FinBlockSize
の新しい定数が追加されました。Workbuf
のサイズ計算が2*PageSize
からWorkbufSize
を使用するように変更されました。- ファイナライザブロックの割り当てが
PageSize
からFinBlockSize
を使用するように変更されました。 runtime·ReadMemStats
関数内で、mstats
からstats
へのコピーにruntime·memcopy
が使用され、runtime·sizeof_C_MStats
をサイズとして渡すように変更されました。これは、GoとCの構造体サイズの違いを考慮したものです。
4. 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);\n+\t\tn = PollBlockSize/sizeof(*pd);\n if(n == 0)
n = 1;
// Must be in non-GC memory because can be referenced
PollBlockSize
という新しい定数が追加されました。allocPollDesc
関数内で、PageSize
の代わりにPollBlockSize
を使用してポーリングディスクリプタの数を計算するように変更されました。
コアとなるコードの解説
1. PageShift
と NumSizeClasses
の変更 (src/pkg/runtime/malloc.h
)
PageShift
の変更:PageShift
は、メモリページのサイズを決定する最も基本的な定数です。12
から13
への変更は、Goランタイムが管理するメモリの最小単位が4KBから8KBに倍増したことを意味します。これは、OSのページングシステムとGoランタイムのメモリ管理が連携する際の粒度を変更します。ページサイズを大きくすることで、ページテーブルエントリの数が減り、TLBミスが減少する可能性が高まります。これにより、メモリアクセスの効率が向上し、全体的なパフォーマンス、特にGCの効率に寄与します。NumSizeClasses
の変更:NumSizeClasses
は、Goランタイムのメモリ割り当て器がオブジェクトを分類するために使用するサイズクラスの総数です。61から67への増加は、ページサイズが大きくなったことに伴い、様々なサイズのオブジェクトを効率的に管理するために、より多くの粒度が必要になったことを示唆しています。Goのメモリ割り当て器は、小さなオブジェクトをスパン(連続したページ)にパックすることでメモリ効率を高めますが、ページサイズが大きくなると、このパッキング戦略を調整する必要が生じ、それがサイズクラスの増加に繋がったと考えられます。
2. MStats
構造体のサイズ計算の調整 (src/pkg/runtime/malloc.goc
および src/pkg/runtime/mgc0.c
)
runtime·sizeof_C_MStats
の調整:MStats
構造体は、Goランタイムのメモリ統計情報を保持します。この構造体には、各サイズクラスごとの統計を格納するby_size
配列が含まれています。NumSizeClasses
が変更されたため、by_size
配列のサイズも論理的に変更されます。しかし、GoとCのコードが混在するGoランタイムでは、CコードがGoの構造体を扱う際に、Go側で変更されたNumSizeClasses
の値を直接反映できない場合があります。uintptr runtime·sizeof_C_MStats = sizeof(MStats) - (NumSizeClasses - 61) * sizeof(mstats.by_size[0]);
この行は、C言語側から見たMStats
構造体のサイズを計算しています。NumSizeClasses
が61から67に増えた場合、by_size
配列のサイズが大きくなりますが、Goの構造体定義がC側と直接同期できないため、差分を引くことでC側が期待するサイズに調整しています。これは、GoとCのランタイムコードが密接に連携しているGoの特性と、その間の互換性を維持するための工夫を示しています。runtime·ReadMemStats
でのruntime·memcopy
の使用: 以前は*stats = mstats;
という直接代入が行われていましたが、変更後はruntime·memcopy(runtime·sizeof_C_MStats, stats, &mstats);
が使用されています。これは、GoとCの間でMStats
構造体のサイズが異なる可能性があるため、C側が認識している正しいサイズ(runtime·sizeof_C_MStats
)を使ってメモリをコピーすることで、データの一貫性を保証するための変更です。
3. ワークバッファとファイナライザブロックのサイズ調整 (src/pkg/runtime/mgc0.c
)
- 固定サイズの導入: GCの内部で使用されるワークバッファ(
WorkbufSize
)とファイナライザブロック(FinBlockSize
)のサイズが、以前のPageSize
に依存する形から、16KB
や4KB
といった固定値に変更されました。#define SIZE (WorkbufSize-sizeof(LFNode)-sizeof(uintptr))
finc = runtime·persistentalloc(FinBlockSize, 0, &mstats.gc_sys);
この変更は、GCの内部的なメモリ管理単位を、システム全体のページサイズ変更からある程度独立させる意図があります。これにより、GCの動作がより予測可能になり、特定のページサイズに強く結合されることを避けることができます。これは、GCのチューニングや将来的な変更の柔軟性を高める上で重要です。
4. ネットポーラーのブロックサイズ調整 (src/pkg/runtime/netpoll.goc
)
PollBlockSize
の導入: ネットポーラーが使用するメモリブロックのサイズも、PageSize
に依存するのではなく、PollBlockSize = 4*1024
という固定値を使用するように変更されました。n = PollBlockSize/sizeof(*pd);
これは、ネットポーラーのような特定のサブシステムが、システム全体のページサイズ変更の影響を受けずに、自身のメモリ割り当て戦略を最適化できるようにするためのものです。これにより、各コンポーネントが独立して効率を追求できるようになります。
これらの変更は、Goランタイムがメモリ管理の効率を向上させ、特にGCの一時停止時間を削減するための多角的なアプローチを示しています。ページサイズの増加は、より大きな粒度でのメモリ操作を可能にし、TLB効率を改善します。同時に、内部的なバッファサイズを固定化することで、特定のサブシステムがページサイズ変更の影響を受けにくくし、システムの安定性とパフォーマンスを両立させています。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時の情報源を探す必要がありますが、一般的な概念は現在のドキュメントでも理解できます)
- Tcmallocの公式ドキュメントや関連情報
- TLB (Translation Lookaside Buffer) に関するコンピュータアーキテクチャの資料
参考にした情報源リンク
- https://github.com/golang/go/commit/e48751e217d3f97c92ec210f605454aa97202c3b (本コミットのGitHubページ)
- https://golang.org/cl/45770044 (元となった変更リスト)
- https://golang.org/cl/56630043 (darwin/freebsdの互換性に関連する変更リスト)
- https://golang.org/cl/58230043 (本コミットのGo Code Reviewページ)
- Tcmallocに関する一般的な情報源 (例: Google Developers Blog, Wikipediaなど)
- 仮想メモリ、ページング、TLBに関するコンピュータサイエンスの教科書やオンラインリソース
- Go言語のガベージコレクションに関する技術ブログや論文 (当時のGoのGCアルゴリズムに関する情報)
[インデックス 18383] ファイルの概要
このコミットは、Goランタイムのメモリ管理におけるページサイズを4KBから8KBに増加させる変更を導入しています。この変更は、ガベージコレクション(GC)の一時停止時間を短縮し、その他のパフォーマンス指標をわずかに改善することを目的としています。特に、Tcmallocなどの他のメモリ管理システムがより大きなページサイズを使用している傾向に合わせることで、メタデータサイズとDTLB(Data Translation Lookaside Buffer)の圧力を軽減し、全体的なシステム効率を向上させます。
コミット
commit e48751e217d3f97c92ec210f605454aa97202c3b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 30 13:28:19 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%
This is the exact reincarnation of already LGTMed:
https://golang.org/cl/45770044
which must not break darwin/freebsd after:
https://golang.org/cl/56630043
TBR=iant
LGTM=khr, iant
R=iant, khr
CC=golang-codereviews
https://golang.org/cl/58230043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e48751e217d3f97c92ec210f605454aa97202c3b
元コミット内容
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%
This is the exact reincarnation of already LGTMed:
https://golang.org/cl/45770044
which must not break darwin/freebsd after:
https://golang.org/cl/56630043
TBR=iant
LGTM=khr, iant
R=iant, khr
CC=golang-codereviews
https://golang.org/cl/58230043
変更の背景
このコミットの主な背景には、Goランタイムのメモリ管理におけるパフォーマンス最適化があります。特に、ガベージコレクション(GC)の一時停止時間の削減と、全体的なメモリ効率の向上が挙げられます。
コミットメッセージによると、Goランタイムが当時使用していた4KBのページサイズは、Tcmallocのような他の高性能メモリ管理ライブラリが8KB、32KB、64KB、さらには256KBといったより大きなページサイズを使用している傾向と対照的でした。4KBページを使用しているのは主にChromiumの「遅いが小さい」設定のみであり、一般的な傾向としてはページサイズを大きくすることで、以下の利点が得られると認識されていました。
- メタデータサイズの削減: メモリをページ単位で管理する際、各ページには管理用のメタデータが付随します。ページサイズを大きくすると、同じ量のメモリを管理するために必要なページ数が減り、結果としてメタデータの総量が削減されます。これにより、メモリ使用量が効率化されます。
- DTLB(Data Translation Lookaside Buffer)圧力の軽減: DTLBは、仮想アドレスから物理アドレスへの変換を高速化するためのCPUキャッシュです。ページサイズが小さいと、より多くのページエントリがDTLBにロードされる必要があり、DTLBミス(キャッシュに目的のエントリがない状態)が発生しやすくなります。DTLBミスは、メモリ変換のためにメインメモリへのアクセスが必要となり、パフォーマンスの低下を招きます。ページサイズを大きくすることで、DTLBがカバーできるメモリ範囲が広がり、DTLBミスの発生頻度が減少し、結果としてメモリアクセスのパフォーマンスが向上します。
これらの理由から、Goランタイムのページサイズを8KBに増やすことで、GCの一時停止時間を約10%削減し、その他のパフォーマンス指標もわずかに改善するという具体的な効果が期待されました。コミットメッセージに記載されているベンチマーク結果(json-1とgarbage-1)は、この変更が実際にGC一時停止時間(gc-pause-one
、gc-pause-total
)やCPU時間(cputime
)、総実行時間(time
)などの改善に寄与していることを示しています。
また、この変更は以前に承認された(LGTMed)コミットの再実装であり、特定のプラットフォーム(darwin/freebsd)での互換性を維持しつつ導入されることが強調されています。これは、変更の安定性と広範な適用可能性を確保するための配慮を示しています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
1. メモリページとページング
現代のオペレーティングシステム(OS)は、仮想メモリという概念を使用しています。これは、プログラムが物理メモリを直接扱うのではなく、仮想的なアドレス空間を扱う仕組みです。この仮想アドレス空間は、固定サイズのブロックに分割されており、これを「ページ(Page)」と呼びます。同様に、物理メモリも「ページフレーム(Page Frame)」と呼ばれる同じサイズのブロックに分割されます。
プログラムが仮想アドレスにアクセスしようとすると、OSはページテーブルを使用してその仮想アドレスを対応する物理アドレスに変換します。このプロセスを「ページング(Paging)」と呼びます。ページサイズは、この変換の粒度を決定します。一般的なページサイズは4KBですが、システムによってはより大きなページサイズ(例:2MB、1GB)もサポートされています。
2. TLB(Translation Lookaside Buffer)とDTLB(Data TLB)
ページング処理において、仮想アドレスから物理アドレスへの変換は頻繁に行われるため、この変換を高速化するためのキャッシュがCPU内に存在します。これが「TLB(Translation Lookaside Buffer)」です。TLBは、最近使用された仮想アドレスと物理アドレスの対応関係を記憶しています。
TLBには、命令フェッチのためのITLB(Instruction TLB)と、データアクセス(読み書き)のためのDTLB(Data TLB)があります。このコミットで言及されているのはDTLBであり、データアクセス時のページ変換の効率に直接関係します。
- TLBヒット: 目的の仮想アドレスと物理アドレスの対応がTLB内に見つかる場合。変換が高速に行われます。
- TLBミス: 目的の対応がTLB内に見つからない場合。OSはページテーブルを検索して対応する物理アドレスを見つける必要があり、これはメインメモリへのアクセスを伴うため、非常にコストの高い操作となります。
ページサイズが小さいと、同じ量のメモリをカバーするために必要なページエントリの数が多くなります。これにより、TLBが保持できるエントリ数を超過しやすくなり、TLBミスが増加する可能性があります。TLBミスが増えると、プログラムの実行速度が低下します。
3. ガベージコレクション(GC)と一時停止時間
Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが不要になったメモリ領域を自動的に解放し、再利用可能にするプロセスです。これにより、開発者は手動でのメモリ管理の複雑さから解放されます。
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。GCの実行中、特に「マークフェーズ」の一部では、プログラムの実行を一時的に停止させる必要があります。この停止期間を「GC一時停止(GC Pause)」と呼びます。GC一時停止は、アプリケーションの応答性やレイテンシに直接影響を与えるため、可能な限り短くすることが望ましいとされています。
このコミットでは、ページサイズを大きくすることで、GCの一時停止時間を削減する効果が期待されています。これは、メモリ管理の効率化(メタデータ削減、DTLB圧力軽減)が、GCがメモリをスキャンしたり管理したりする際のオーバーヘッドを減らすことに繋がるためと考えられます。
4. Tcmalloc
Tcmalloc(Thread-Caching Malloc)は、Googleが開発した高性能なメモリ割り当てライブラリです。C++プログラムなどで広く使用されており、マルチスレッド環境でのメモリ割り当てのパフォーマンスを最適化することに特化しています。Tcmallocは、スレッドローカルなキャッシュと中央のヒープを組み合わせることで、ロックの競合を減らし、高速なメモリ割り当てと解放を実現します。
Web検索の結果によると、Tcmallocは内部的に「ページ」という単位でメモリを管理しており、これはOSのTLBページサイズとは異なる論理的な概念です。Tcmallocは、4KB、8KB(デフォルト)、32KB、256KBといった複数の論理ページサイズをサポートしています。小さいページサイズは内部断片化が少ないためメモリ効率が良い傾向がありますが、大きいページサイズはページヒープからのフェッチ回数を減らし、同じサイズのオブジェクトをメモリ内でより良くクラスタリングできるため、パフォーマンスが向上する可能性があります。また、Tcmallocはヒュージページも利用してTLBミスを減らすことができます。
コミットメッセージでTcmallocが参照されているのは、そのメモリ管理戦略、特にページサイズの選択が、Goランタイムのメモリ管理設計の参考になっていることを示唆しています。Tcmallocがより大きなページサイズを使用しているという事実は、Goランタイムも同様の最適化を追求する動機付けとなっています。
技術的詳細
このコミットの技術的詳細を掘り下げると、Goランタイムがメモリページをどのように扱っているか、そしてページサイズ変更がシステム全体にどのような影響を与えるかが明らかになります。
ページサイズの変更 (PageShift
, PageSize
, PageMask
)
最も直接的な変更は、src/pkg/runtime/malloc.h
ファイルにおけるページ関連定数の定義です。
PageShift
は、ページサイズを2のべき乗で表すためのシフト量です。- 変更前:
PageShift = 12
(2^12 = 4096バイト = 4KB) - 変更後:
PageShift = 13
(2^13 = 8192バイト = 8KB)
- 変更前:
PageSize
は、実際のページサイズです。- 変更前:
PageSize = 1<<PageShift
(4KB) - 変更後:
PageSize = 1<<PageShift
(8KB)
- 変更前:
PageMask
は、ページのアドレスをアラインメントするためのマスクです。- 変更前:
PageMask = PageSize - 1
(4KB - 1) - 変更後:
PageMask = PageSize - 1
(8KB - 1)
- 変更前:
この変更により、Goランタイムがメモリを割り当て、管理する際の最小単位が4KBから8KBに倍増します。
サイズクラスの変更 (NumSizeClasses
)
src/pkg/runtime/malloc.h
では、メモリ割り当ての「サイズクラス」の数も変更されています。
NumSizeClasses
は、Goランタイムが管理するオブジェクトのサイズカテゴリの数です。- 変更前:
NumSizeClasses = 61
- 変更後:
NumSizeClasses = 67
- 変更前:
これは、ページサイズが大きくなったことに伴い、メモリ割り当ての粒度や効率を維持するために、より多くのサイズクラスが必要になったことを示唆しています。Goのメモリ管理は、小さなオブジェクトを効率的に管理するために、様々なサイズの「スパン」(連続したページ群)を使用します。ページサイズが大きくなると、スパンの構成も調整する必要があり、それがサイズクラスの増加に繋がったと考えられます。
MStats
構造体のサイズ計算の調整
src/pkg/runtime/malloc.goc
と src/pkg/runtime/mgc0.c
では、MStats
構造体のサイズ計算に関する調整が行われています。
MStats
は、Goランタイムのメモリ統計情報を保持する構造体です。この構造体には、by_size
という配列が含まれており、これは各サイズクラスごとの統計情報を格納します。NumSizeClasses
が変更されたため、by_size
配列のサイズも論理的に変更されます。
しかし、GoとCの間の互換性の問題から、runtime·sizeof_C_MStats
の計算が調整されています。これは、CコードがGoのMStats
構造体を扱う際に、Go側で変更されたNumSizeClasses
の値を直接反映できないため、オフセットを調整して正しいサイズをC側に伝えるためのものです。
// src/pkg/runtime/malloc.goc
uintptr runtime·sizeof_C_MStats = sizeof(MStats) - (NumSizeClasses - 61) * sizeof(mstats.by_size[0]);
この行は、C言語側から見たMStats
構造体のサイズを計算しています。NumSizeClasses
が61から67に増えた場合、by_size
配列のサイズが大きくなりますが、Goの構造体定義がC側と直接同期できないため、差分を引くことでC側が期待するサイズに調整しています。これは、GoとCのランタイムコードが密接に連携しているGoの特性を示しています。
ワークバッファとファイナライザブロックのサイズ調整
src/pkg/runtime/mgc0.c
では、GCのワークバッファ(WorkbufSize
)とファイナライザブロック(FinBlockSize
)のサイズが、以前のPageSize
に依存する形から、固定値に変更されています。
WorkbufSize = 16*1024
(16KB)RootBlockSize = 4*1024
(4KB)FinBlockSize = 4*1024
(4KB)
以前はWorkbuf
のサイズが2*PageSize
に依存していましたが、新しいWorkbufSize
定数を使用するように変更されています。これにより、ワークバッファのサイズがページサイズに直接依存しなくなり、より柔軟なメモリ管理が可能になります。同様に、ファイナライザブロックの割り当てもPageSize
ではなくFinBlockSize
を使用するように変更されています。
これは、GCの内部的なメモリ管理単位を、システム全体のページサイズ変更からある程度独立させる意図があると考えられます。これにより、GCの動作がより予測可能になり、特定のページサイズに強く結合されることを避けることができます。
ネットポーラーのブロックサイズ調整
src/pkg/runtime/netpoll.goc
では、ネットポーラーが使用するPollBlockSize
が定義され、allocPollDesc
関数でこの新しい定数が使用されています。
PollBlockSize = 4*1024
(4KB)
以前はPageSize
を使用していましたが、PollBlockSize
という新しい固定値を使用することで、ネットポーラーの内部メモリ割り当てがページサイズ変更の影響を受けにくくなります。これは、特定のサブシステムが独自のメモリ割り当て戦略を持つことを可能にし、全体的なシステムの安定性とパフォーマンスを向上させるための一般的なパターンです。
変更の全体的な影響
これらの変更は、Goランタイムのメモリ管理の根幹に影響を与えます。
- メモリ効率の向上: ページサイズを大きくすることで、ページテーブルエントリの数が減り、OSが管理するメタデータが削減されます。これにより、メモリフットプリントがわずかに減少する可能性があります。
- CPUキャッシュ効率の向上: DTLBミスが減少することで、CPUが仮想アドレスを物理アドレスに変換する際のオーバーヘッドが減り、全体的なCPUパフォーマンスが向上します。
- GCパフォーマンスの改善: GCがメモリをスキャンしたり、解放したりする際に、より大きなページ単位で操作できるようになるため、GCの効率が向上し、結果としてGC一時停止時間が短縮されます。これは、特にレイテンシに敏感なアプリケーションにとって重要な改善です。
- メモリ割り当ての粒度: ページサイズが大きくなることで、小さなオブジェクトの割り当てにおいて内部的な断片化が増える可能性もわずかにありますが、Goのメモリ管理はスモールオブジェクトアロケータによってこの問題を緩和するように設計されています。
NumSizeClasses
の増加も、この断片化を管理するための一環と考えられます。
このコミットは、Goランタイムが継続的にパフォーマンス最適化に取り組んでいること、特にメモリ管理とGCの効率向上に注力していることを明確に示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にGoランタイムのメモリ管理に関連する定数と、それらを使用する内部構造体のサイズ計算、およびメモリ割り当てロジックの調整です。
1. 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,
PageShift
が12
から13
に変更され、これによりPageSize
が 4KB (2^12) から 8KB (2^13) に倍増しました。NumSizeClasses
が61
から67
に変更され、メモリ割り当てのサイズクラスの数が増加しました。
2. src/pkg/runtime/malloc.goc
MStats
構造体のサイズ計算が調整されています。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -417,7 +418,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]);
runtime·sizeof_C_MStats
の計算式が変更され、GoとCの間でのMStats
構造体の互換性を維持するために、NumSizeClasses
の変更を考慮したオフセットが導入されました。
3. src/pkg/runtime/mgc0.c
GCの内部で使用されるワークバッファとファイナライザブロックのサイズ定義が追加され、それらを使用するように変更されました。
--- 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,
@@ -154,11 +158,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))\n+#define SIZE (WorkbufSize-sizeof(LFNode)-sizeof(uintptr))\n \tLFNode node; // must be first
\tuintptr nobj;
\tObj obj[SIZE/sizeof(Obj) - 1];
// ...
@@ -737,7 +740,7 @@ scanblock(Workbuf *wbuf, bool keepworking)
ChanType *chantype;
Obj *wp;
-\tif(sizeof(Workbuf) % PageSize != 0)\n+\tif(sizeof(Workbuf) % WorkbufSize != 0)\n \t\truntime·throw(\"scanblock: size of Workbuf is suboptimal\");
// ...
@@ -1601,8 +1604,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);\n-\t\t\tfinc->cap = (PageSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;\n+\t\t\tfinc = runtime·persistentalloc(FinBlockSize, 0, &mstats.gc_sys);\n+\t\t\tfinc->cap = (FinBlockSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;\n \tfinc->alllink = allfin;
allfin = finc;
}
// ...
@@ -2233,6 +2236,8 @@ gc(struct gc_args *args)
runtime·MProf_GC();
}
+extern uintptr runtime·sizeof_C_MStats;
+
void
runtime·ReadMemStats(MStats *stats)
{
@@ -2244,7 +2249,9 @@ runtime·ReadMemStats(MStats *stats)
m->gcing = 1;
runtime·stoptheworld();
updatememstats(nil);
-\t*stats = mstats;\n+\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);\n m->gcing = 0;
m->locks++;
runtime·semrelease(&runtime·worldsema);
WorkbufSize
,RootBlockSize
,FinBlockSize
の新しい定数が追加されました。Workbuf
のサイズ計算が2*PageSize
からWorkbufSize
を使用するように変更されました。- ファイナライザブロックの割り当てが
PageSize
からFinBlockSize
を使用するように変更されました。 runtime·ReadMemStats
関数内で、mstats
からstats
へのコピーにruntime·memcopy
が使用され、runtime·sizeof_C_MStats
をサイズとして渡すように変更されました。これは、GoとCの構造体サイズの違いを考慮したものです。
4. 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);\n+\t\tn = PollBlockSize/sizeof(*pd);\n if(n == 0)
n = 1;
// Must be in non-GC memory because can be referenced
PollBlockSize
という新しい定数が追加されました。allocPollDesc
関数内で、PageSize
の代わりにPollBlockSize
を使用してポーリングディスクリプタの数を計算するように変更されました。
コアとなるコードの解説
1. PageShift
と NumSizeClasses
の変更 (src/pkg/runtime/malloc.h
)
PageShift
の変更:PageShift
は、メモリページのサイズを決定する最も基本的な定数です。12
から13
への変更は、Goランタイムが管理するメモリの最小単位が4KBから8KBに倍増したことを意味します。これは、OSのページングシステムとGoランタイムのメモリ管理が連携する際の粒度を変更します。ページサイズを大きくすることで、ページテーブルエントリの数が減り、TLBミスが減少する可能性が高まります。これにより、メモリアクセスの効率が向上し、全体的なパフォーマンス、特にGCの効率に寄与します。NumSizeClasses
の変更:NumSizeClasses
は、Goランタイムのメモリ割り当て器がオブジェクトを分類するために使用するサイズクラスの総数です。61から67への増加は、ページサイズが大きくなったことに伴い、様々なサイズのオブジェクトを効率的に管理するために、より多くの粒度が必要になったことを示唆しています。Goのメモリ割り当て器は、小さなオブジェクトをスパン(連続したページ)にパックすることでメモリ効率を高めますが、ページサイズが大きくなると、このパッキング戦略を調整する必要が生じ、それがサイズクラスの増加に繋がったと考えられます。
2. MStats
構造体のサイズ計算の調整 (src/pkg/runtime/malloc.goc
および src/pkg/runtime/mgc0.c
)
runtime·sizeof_C_MStats
の調整:MStats
構造体は、Goランタイムのメモリ統計情報を保持します。この構造体には、各サイズクラスごとの統計を格納するby_size
配列が含まれています。NumSizeClasses
が変更されたため、by_size
配列のサイズも論理的に変更されます。しかし、GoとCのコードが混在するGoランタイムでは、CコードがGoの構造体を扱う際に、Go側で変更されたNumSizeClasses
の値を直接反映できない場合があります。uintptr runtime·sizeof_C_MStats = sizeof(MStats) - (NumSizeClasses - 61) * sizeof(mstats.by_size[0]);
この行は、C言語側から見たMStats
構造体のサイズを計算しています。NumSizeClasses
が61から67に増えた場合、by_size
配列のサイズが大きくなりますが、Goの構造体定義がC側と直接同期できないため、差分を引くことでC側が期待するサイズに調整しています。これは、GoとCのランタイムコードが密接に連携しているGoの特性と、その間の互換性を維持するための工夫を示しています。runtime·ReadMemStats
でのruntime·memcopy
の使用: 以前は*stats = mstats;
という直接代入が行われていましたが、変更後はruntime·memcopy(runtime·sizeof_C_MStats, stats, &mstats);
が使用されています。これは、GoとCの間でMStats
構造体のサイズが異なる可能性があるため、C側が認識している正しいサイズ(runtime·sizeof_C_MStats
)を使ってメモリをコピーすることで、データの一貫性を保証するための変更です。
3. ワークバッファとファイナライザブロックのサイズ調整 (src/pkg/runtime/mgc0.c
)
- 固定サイズの導入: GCの内部で使用されるワークバッファ(
WorkbufSize
)とファイナライザブロック(FinBlockSize
)のサイズが、以前のPageSize
に依存する形から、16KB
や4KB
といった固定値に変更されました。#define SIZE (WorkbufSize-sizeof(LFNode)-sizeof(uintptr))
finc = runtime·persistentalloc(FinBlockSize, 0, &mstats.gc_sys);
この変更は、GCの内部的なメモリ管理単位を、システム全体のページサイズ変更からある程度独立させる意図があります。これにより、GCの動作がより予測可能になり、特定のページサイズに強く結合されることを避けることができます。これは、GCのチューニングや将来的な変更の柔軟性を高める上で重要です。
4. ネットポーラーのブロックサイズ調整 (src/pkg/runtime/netpoll.goc
)
PollBlockSize
の導入: ネットポーラーが使用するメモリブロックのサイズも、PageSize
に依存するのではなく、PollBlockSize = 4*1024
という固定値を使用するように変更されました。n = PollBlockSize/sizeof(*pd);
これは、ネットポーラーのような特定のサブシステムが、システム全体のページサイズ変更の影響を受けずに、自身のメモリ割り当て戦略を最適化できるようにするためのものです。これにより、各コンポーネントが独立して効率を追求できるようになります。
これらの変更は、Goランタイムがメモリ管理の効率を向上させ、特にGCの一時停止時間を削減するための多角的なアプローチを示しています。ページサイズの増加は、より大きな粒度でのメモリ操作を可能にし、TLB効率を改善します。同時に、内部的なバッファサイズを固定化することで、特定のサブシステムがページサイズ変更の影響を受けにくくし、システムの安定性とパフォーマンスを両立させています。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時の情報源を探す必要がありますが、一般的な概念は現在のドキュメントでも理解できます)
- Tcmallocの公式ドキュメントや関連情報
- TLB (Translation Lookaside Buffer) に関するコンピュータアーキテクチャの資料
参考にした情報源リンク
- https://github.com/golang/go/commit/e48751e217d3f97c92ec210f605454aa97202c3b (本コミットのGitHubページ)
- https://golang.org/cl/45770044 (元となった変更リスト)
- https://golang.org/cl/56630043 (darwin/freebsdの互換性に関連する変更リスト)
- https://golang.org/cl/58230043 (本コミットのGo Code Reviewページ)
- Tcmallocに関する一般的な情報源 (例: Google Developers Blog, Wikipediaなど)
- 仮想メモリ、ページング、TLBに関するコンピュータサイエンスの教科書やオンラインリソース
- Go言語のガベージコレクションに関する技術ブログや論文 (当時のGoのGCアルゴリズムに関する情報)