Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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以上)を使用する傾向にあります。これは、ページサイズを大きくすることで、以下の利点が得られるためです。

  1. メタデータサイズの削減: メモリを管理するための内部データ構造(例えば、どのページが使用中か、空いているかなどの情報)の量が減ります。ページあたりの管理オーバーヘッドが相対的に小さくなるため、メモリ使用効率が向上します。
  2. 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ランタイムのメモリ管理サブシステムにおける複数の定数とロジックの変更に集約されます。

  1. ページサイズの変更 (src/pkg/runtime/malloc.h):

    • PageShift定数が12から13に変更されました。これにより、PageSize1 << 12 = 4096バイト(4KB)から1 << 13 = 8192バイト(8KB)に倍増します。これはコミットの核心となる変更です。
    • NumSizeClasses61から67に増加しました。これは、Goランタイムが管理するメモリ割り当てサイズクラスの数が増えたことを意味します。ページサイズが大きくなったことで、メモリをより効率的に細分化して割り当てるために、より多くのサイズクラスが必要になったと考えられます。
  2. 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.cruntime·ReadMemStats関数では、*stats = mstats;という直接的な構造体コピーから、runtime·memcopy(runtime·sizeof_C_MStats, stats, &mstats);という明示的なメモリコピーに変更されました。これは、前述のMStats構造体のサイズ不一致に対応し、C側が認識する正しいサイズでデータをコピーすることを保証します。
  3. 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の代わりにこれを使用するようになりました。
    • これらの変更は、特定の内部バッファやブロックのサイズを、メインのページサイズから独立させることで、より予測可能で安定したメモリ使用を保証するためのものです。これにより、これらのコンポーネントがページサイズ変更の影響を直接受けず、特定のパフォーマンス特性を維持できるようになります。
  4. 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関数でpersistentallocFinBlockSizeを使用するよう変更。
    • 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ランタイムのメモリ管理の根幹をなす部分です。PageShift12から13に変更されたことで、PageSize2^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側で定義されたMStatsby_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の内部動作に関連するメモリ割り当ての変更が行われています。 WorkbufSizeRootBlockSizeFinBlockSizeといった定数の導入は、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の一時停止時間を短縮し、全体的なパフォーマンスを向上させるための、細部にわたる調整と最適化を示しています。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分情報: https://github.com/golang/go/commit/6d603af6dc298d23a5100328f0006ccc8ac3a1ae
  • Goのメモリ管理に関する一般的な知識
  • オペレーティングシステムのメモリ管理(ページング、TLB)に関する一般的な知識
  • Tcmallocに関する一般的な知識
  • Goのガベージコレクションに関する一般的な知識
  • Goのソースコード(当時のバージョン)