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

[インデックス 16418] ファイルの概要

このコミットは、Goランタイムのメモリ割り当てメカニズムにおける重要な最適化を導入しています。具体的には、MCache_Alloc() 関数を mallocgc() 関数内にインライン化することで、メモリ割り当てのパフォーマンスを向上させています。これにより、関数呼び出しのオーバーヘッドが削減され、ベンチマークで示されるように、特に小規模なオブジェクトの割り当てにおいて顕著な速度改善が達成されています。

コミット

commit 5166013f75a7dbab53482292f99c3b6c26cddd0b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 28 11:05:55 2013 +0400

    runtime: inline MCache_Alloc() into mallocgc()
    benchmark                    old ns/op    new ns/op    delta
    BenchmarkMalloc8                    68           62   -8.63%
    BenchmarkMalloc16                   75           69   -7.94%
    BenchmarkMallocTypeInfo8           102           98   -3.73%
    BenchmarkMallocTypeInfo16          108          103   -4.63%
    
    R=golang-dev, dave, khr
    CC=golang-dev
    https://golang.org/cl/9790043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5166013f75a7dbab53482292f99c3b6c26cddd0b

元コミット内容

このコミットの目的は、Goランタイムのメモリ割り当て関数 mallocgc() 内で MCache_Alloc() 関数をインライン化することです。これにより、関数呼び出しのオーバーヘッドを削減し、メモリ割り当ての効率を向上させます。ベンチマーク結果は、BenchmarkMalloc および BenchmarkMallocTypeInfo の両方で、ナノ秒あたりの操作数が減少(パフォーマンス向上)していることを示しています。

変更の背景

Goランタイムのメモリ管理は、アプリケーションのパフォーマンスに直接影響を与える重要な要素です。特に、頻繁に発生する小規模なオブジェクトの割り当ては、その効率が全体の実行速度に大きく寄与します。以前の設計では、mallocgc() がメモリを割り当てる際に、MCache_Alloc() という別の関数を呼び出していました。この関数呼び出しには、スタックフレームの作成、引数の渡し、戻り値の処理といったオーバーヘッドが伴います。

このコミットの背景には、このような関数呼び出しのオーバーヘッドを排除し、メモリ割り当てパスをより直接的かつ高速にすることによって、Goプログラム全体のパフォーマンスを改善するという目的があります。インライン化は、コンパイラ最適化の一種であり、関数呼び出しをその関数の本体で置き換えることで、このオーバーヘッドを削減します。この変更は、Goのメモリ割り当てが非常に頻繁に行われる操作であるため、全体的なシステムパフォーマンスに大きな影響を与えます。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理に関するいくつかの基本的な概念を理解しておく必要があります。

  1. Goのメモリ管理の階層構造:

    • MHeap (Global Heap): 全てのGoプログラムが使用するグローバルなヒープ領域です。これはGoのガベージコレクタによって管理されます。
    • MCentral (Central Lists): MHeapから取得したメモリを、特定のサイズクラス(後述)ごとに管理する中央のフリーリストです。複数のP(プロセッサ)間で共有されます。
    • MCache (Per-P Cache): 各P(論理プロセッサ、Goスケジューラが管理するOSスレッド)に紐付けられたローカルなメモリキャッシュです。ゴルーチンは、まず自身のPのMCacheからメモリを割り当てようとします。これにより、グローバルなロックの競合を減らし、高速なメモリ割り当てを可能にします。MCacheが枯渇すると、MCentralからメモリを補充します。
  2. サイズクラス (Size Classes): Goのメモリ割り当ては、特定の固定サイズ(サイズクラス)に丸められます。これにより、メモリの断片化を減らし、効率的な管理を可能にします。例えば、8バイトのオブジェクトを要求しても、それが属する最小のサイズクラス(例: 8バイト)のチャンクが割り当てられます。

  3. mallocgc(): Goランタイムにおける主要なメモリ割り当て関数です。アプリケーションコードが newmake を使用してメモリを要求すると、最終的にこの mallocgc() が呼び出されます。この関数は、要求されたサイズに基づいて適切なサイズクラスを決定し、MCacheからメモリを割り当てようとします。

  4. MCache_Alloc() (変更前): 以前は、mallocgc() がMCacheからオブジェクトを割り当てる際に呼び出していたヘルパー関数です。この関数は、指定されたサイズクラスのMCacheからフリーオブジェクトを取得し、必要に応じてMCacheを補充するロジックを含んでいました。

  5. インライン化 (Inlining): コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換えることです。これにより、関数呼び出しに伴うオーバーヘッド(スタックフレームのセットアップ、レジスタの保存/復元など)が排除され、実行速度が向上します。特に、頻繁に呼び出される小さな関数に対して効果的です。

  6. MLink: Goランタイムの内部で、フリーリスト内のオブジェクトをリンクするために使用される構造体です。フリーオブジェクトの先頭には、次のフリーオブジェクトへのポインタが格納されます。

技術的詳細

このコミットの核心は、src/pkg/runtime/mcache.c にあった runtime·MCache_Alloc() 関数のロジックを、src/pkg/runtime/malloc.goc 内の runtime·mallocgc() 関数に直接移動(インライン化)した点にあります。

変更前: mallocgc() は、メモリ割り当て要求を受け取ると、まず適切なサイズクラスを決定し、その後 runtime·MCache_Alloc(c, sizeclass, size, zeroed) を呼び出して、実際にMCacheからメモリブロックを取得していました。

変更後: mallocgc() は、MCache_Alloc() を呼び出す代わりに、その内部で行われていた処理を直接実行します。

  1. MCacheList *l = &c->list[sizeclass]; で、現在のPのMCacheから該当するサイズクラスのフリーリストを取得します。
  2. if(l->list == nil) で、フリーリストが空であるかをチェックします。
  3. もし空であれば、runtime·MCache_Refill(c, sizeclass); を呼び出して、MCentralからMCacheを補充します。
  4. フリーリストから最初のオブジェクト v を取得し、リストのポインタを更新します (l->list = v->next; l->nlist--;)。
  5. 取得したオブジェクトの next ポインタを nil に設定し、必要に応じてメモリをゼロクリアします。
  6. MCacheの統計情報(local_cachealloc, local_objects, local_alloc, local_total_alloc, local_by_size)を更新します。

同時に、src/pkg/runtime/mcache.c から runtime·MCache_Alloc() 関数が削除され、runtime·MCache_Refill() 関数が独立した形で残されました。malloc.h の関数プロトタイプもこれに合わせて変更されています。

このインライン化により、mallocgc() がメモリを割り当てるたびに発生していた MCache_Alloc() への関数呼び出しが不要になります。これにより、関数呼び出しのオーバーヘッド(スタックフレームのプッシュ/ポップ、レジスタの保存/復元など)が削減され、特に頻繁に呼び出されるメモリ割り当て処理のパスが短縮され、高速化されます。ベンチマーク結果が示すように、この最適化は小規模なオブジェクトの割り当てにおいて特に効果を発揮します。

コアとなるコードの変更箇所

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -31,9 +31,10 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 	int32 sizeclass;
 	intgo rate;
 	MCache *c;
+	MCacheList *l; // 追加: MCacheListへのポインタ
 	uintptr npages;
 	MSpan *s;
-	void *v; // 変更: MLink* に型変更
+	MLink *v; // 変更: MLink* に型変更
 
 	if(runtime·gcwaiting && g != m->g0 && m->locks == 0 && dogc)
 		runtime·gosched();
@@ -56,9 +57,20 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 		else
 			sizeclass = runtime·size_to_class128[(size-1024+127) >> 7];
 		size = runtime·class_to_size[sizeclass];
-		v = runtime·MCache_Alloc(c, sizeclass, size, zeroed); // 削除: MCache_Allocの呼び出し
-		if(v == nil)
-			runtime·throw("out of memory");
+		l = &c->list[sizeclass]; // 追加: MCacheListの取得
+		if(l->list == nil) // 追加: フリーリストが空の場合のチェック
+			runtime·MCache_Refill(c, sizeclass); // 追加: MCacheの補充
+		v = l->list; // 追加: フリーリストからオブジェクトを取得
+		l->list = v->next; // 追加: フリーリストのポインタを更新
+		l->nlist--; // 追加: リスト内のオブジェクト数をデクリメント
+		if(zeroed) { // 追加: ゼロクリア処理
+			v->next = nil;
+			// block is zeroed iff second word is zero ...
+			if(size > sizeof(uintptr) && ((uintptr*)v)[1] != 0)
+				runtime·memclr((byte*)v, size);
+		}
+		c->local_cachealloc += size; // 追加: 統計情報の更新
+		c->local_objects++; // 追加: 統計情報の更新
 		c->local_alloc += size;
 		c->local_total_alloc += size;
 		c->local_by_size[sizeclass].nmalloc++;

src/pkg/runtime/malloc.h

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -305,7 +305,7 @@ struct MCache
 
 };
 
-void*	runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed); // 削除
+void	runtime·MCache_Refill(MCache *c, int32 sizeclass); // MCache_AllocからMCache_Refillに変更
 void	runtime·MCache_Free(MCache *c, void *p, int32 sizeclass, uintptr size);
 void	runtime·MCache_ReleaseAll(MCache *c);
 

src/pkg/runtime/mcache.c

--- a/src/pkg/runtime/mcache.c
+++ b/src/pkg/runtime/mcache.c
@@ -10,35 +10,18 @@
 #include "arch_GOARCH.h"
 #include "malloc.h"
 
-void* // 削除: MCache_Alloc関数全体が削除
-runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)
+void // MCache_Refill関数のみが残る
+runtime·MCache_Refill(MCache *c, int32 sizeclass)
 {
 	MCacheList *l;
-\tMLink *v; // 削除
 
-\t// Allocate from list. // 削除
+\t// Replenish using central lists. // 変更: コメント更新
 \tl = &c->list[sizeclass];
-\tif(l->list == nil) { // 削除
-\t\t// Replenish using central lists. // 削除
-\t\tl->nlist = runtime·MCentral_AllocList(&runtime·mheap->central[sizeclass], &l->list); // 削除
-\t\tif(l->list == nil) // 削除
-\t\t\truntime·throw(\"out of memory\"); // 削除
-\t} // 削除
-\tv = l->list; // 削除
-\tl->list = v->next; // 削除
-\tl->nlist--; // 削除
-\n-\t// v is zeroed except for the link pointer // 削除
-\t// that we used above; zero that. // 削除
-\tv->next = nil; // 削除
-\tif(zeroed) { // 削除
-\t\t// block is zeroed iff second word is zero ... // 削除
-\t\tif(size > sizeof(uintptr) && ((uintptr*)v)[1] != 0) // 削除
-\t\t\truntime·memclr((byte*)v, size); // 削除
-\t} // 削除
-\tc->local_cachealloc += size; // 削除
-\tc->local_objects++; // 削除
-\treturn v; // 削除
+\tif(l->list) // 追加: リストが空でない場合のチェック
+\t\truntime·throw(\"MCache_Refill: the list is not empty\"); // 追加: エラーハンドリング
+\tl->nlist = runtime·MCentral_AllocList(&runtime·mheap->central[sizeclass], &l->list); // MCentralからの補充ロジック
+\tif(l->list == nil) // 補充失敗時のエラーハンドリング
+\t\truntime·throw(\"out of memory\");
 }
 
 // Take n elements off l and return them to the central free list.

コアとなるコードの解説

このコミットの主要な変更は、runtime·mallocgc 関数がメモリを割り当てる際の内部ロジックを根本的に変更した点にあります。

変更前は、mallocgcruntime·MCache_Alloc という別の関数を呼び出して、MCacheからの実際のメモリ割り当て処理を行っていました。この MCache_Alloc 関数は、以下のステップを実行していました。

  1. 指定された sizeclass に対応する MCacheList を取得。
  2. もしそのリストが空であれば、runtime·MCentral_AllocList を呼び出して、中央の MCentral からメモリブロックを補充。
  3. リストの先頭から MLink オブジェクト(フリーブロック)を取得。
  4. 取得したブロックの next ポインタを nil に設定し、必要に応じてゼロクリア。
  5. MCacheの統計情報を更新。
  6. 取得したメモリブロックへのポインタを返す。

このコミットでは、上記の MCache_Alloc のロジック全体が mallocgc の内部に直接コピーされ、MCache_Alloc 関数自体は削除されました。これにより、mallocgc がメモリを割り当てるたびに発生していた関数呼び出しのオーバーヘッドが完全に排除されます。

特に注目すべきは、MCache_Refill 関数が独立した関数として残された点です。これは、MCacheの特定のサイズクラスのリストが完全に枯渇した場合にのみ呼び出される、比較的まれな操作であるため、インライン化の恩恵が少ないと判断されたためと考えられます。頻繁に実行される割り当てパス(MCacheに十分なメモリがある場合)はインライン化によって高速化され、まれなパス(MCacheの補充が必要な場合)は引き続き関数呼び出しを介して処理されます。

この最適化は、Goのメモリ割り当てが非常に頻繁に行われる操作であるため、全体的なアプリケーションのパフォーマンスに大きな影響を与えます。ベンチマーク結果が示すように、特に小規模なオブジェクトの割り当てにおいて、ナノ秒単位での改善が積み重なり、全体として顕著な速度向上に繋がっています。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/9790043 は、このGerritの変更リストへのリンクです。)
  • Goのメモリ管理に関する一般的な知識(Goの公式ドキュメント、Goのソースコード、関連する技術ブログや論文)
  • インライン化に関する一般的なコンパイラ最適化の知識
  • Goのベンチマークの読み方に関する知識