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

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

このコミットは、Goランタイムにおけるメモリ管理の最適化に関するものです。具体的には、巨大なメモリブロックの不要なゼロ初期化(zeroization)を回避し、さらにゼロ初期化処理をヒープミューテックスの保護下から移動させることで、並行処理性能を向上させることを目的としています。

コミット

commit c1c851bbe806d8fb3f483a32e8dfac48522dfe21
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 2 18:01:11 2012 +0400

    runtime: avoid unnecessary zeroization of huge memory blocks
    +move zeroization out of the heap mutex
    
    R=golang-dev, iant, rsc
    CC=golang-dev
    https://golang.org/cl/6094050

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

https://github.com/golang/go/commit/c1c851bbe806d8fb3f483a32e8dfac48522dfe21

元コミット内容

runtime: avoid unnecessary zeroization of huge memory blocks
+move zeroization out of the heap mutex

変更の背景

Go言語のランタイムは、効率的なメモリ管理を追求しています。メモリを確保する際、セキュリティ上の理由や予測可能な動作を保証するために、新しく確保されたメモリ領域をゼロで初期化(ゼロ初期化、zeroization)することが一般的です。これにより、以前のデータが残存することによる情報漏洩のリスクを防ぎ、プログラムが未初期化のデータに依存するバグを回避できます。

しかし、特に巨大なメモリブロックを確保する場合、このゼロ初期化処理はかなりの時間を要し、パフォーマンスのボトルネックとなる可能性があります。また、Goランタイムのヒープ管理は、複数のゴルーチンからの並行アクセスを調整するためにミューテックス(runtime.mheapのロック)によって保護されています。ゼロ初期化処理がこのミューテックスの保護下で行われると、その間、他のメモリ割り当て要求がブロックされ、並行性が著しく低下します。

このコミットの背景には、以下の2つの主要な課題がありました。

  1. 不要なゼロ初期化の回避: 特定のシナリオでは、割り当てられたメモリがすぐに上書きされることが分かっている場合、ゼロ初期化は不要なオーバーヘッドとなります。例えば、ガベージコレクタがメモリを再利用する際、そのメモリがすぐに新しいデータで埋められるのであれば、事前にゼロ初期化する必要はありません。
  2. ヒープミューテックスの解放: ゼロ初期化のような時間のかかる処理がヒープミューテックスを保持したまま実行されると、メモリ割り当ての並行性が阻害されます。ミューテックスの保持時間を最小限に抑えることは、マルチコア環境でのスケーラビリティを向上させる上で非常に重要です。

このコミットは、これらの課題に対処し、Goランタイムのメモリ割り当て性能、特に大規模なメモリ割り当てにおける性能と並行性を改善することを目的としています。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理、特にヒープアロケータの基本的な概念と、ゼロ初期化、ミューテックスの役割について理解しておく必要があります。

Goランタイムのメモリ管理の基礎

Goランタイムは、独自のメモリ管理システムを持っています。これは、OSからのメモリ確保(mmapなど)と、そのメモリをGoプログラムが利用できる小さなチャンクに分割・管理するメカニズムから構成されます。

  • MHeap (Memory Heap): Goランタイム全体のヒープを表す構造体です。OSから取得したメモリを管理し、必要に応じてMSpanに分割して提供します。runtime.mheapというグローバルなインスタンスが存在し、ヒープ全体へのアクセスはミューテックスによって保護されています。
  • MSpan (Memory Span): 連続したページ(通常は4KB)のブロックです。MHeapから割り当てられ、特定のサイズクラスのオブジェクトを格納するために使用されるか、あるいは大きなオブジェクトのために直接割り当てられます。
  • MCentral (Memory Central): 特定のサイズクラス(例えば、8バイトオブジェクト用、16バイトオブジェクト用など)のMSpanを管理する構造体です。MCentralは、MHeapからMSpanを取得し、それをMcache(各P/論理プロセッサに紐づくキャッシュ)に提供します。
  • PageShift, PageMask: メモリページサイズに関連する定数です。PageShiftはページサイズの2の対数(例えば、4KBページなら12)、PageMaskはページサイズから1を引いた値(ページ内のオフセットを計算するのに使われる)です。s->start<<PageShiftMSpanの開始アドレスをバイト単位で計算します。s->npages<<PageShiftMSpanの合計サイズをバイト単位で計算します。

ゼロ初期化 (Zeroization)

メモリ割り当てにおいて、新しく確保されたメモリ領域の内容をすべてゼロ(または特定のパターン)で埋める処理を指します。

  • 目的:
    • セキュリティ: 以前のプログラムが使用していたデータが残存する「情報リーク」を防ぎます。これにより、機密データが誤って新しいプログラムに公開されるリスクを低減します。
    • 予測可能性: プログラムが未初期化のメモリを読み込んだ際に、不定な値ではなく常にゼロが返されることを保証します。これにより、デバッグが容易になり、バグの発生を抑制します。
  • コスト: 特に大きなメモリブロックの場合、ゼロ初期化はCPUサイクルを消費し、メモリ帯域を占有するため、無視できないオーバーヘッドとなります。

ミューテックス (Mutex)

ミューテックス(Mutual Exclusion、相互排他)は、複数のスレッドやゴルーチンが共有リソース(この場合はGoランタイムのヒープ)に同時にアクセスするのを防ぐための同期プリミティブです。

  • 役割: 共有データの一貫性を保ち、競合状態(Race Condition)を防ぎます。
  • ロックとアンロック: 共有リソースにアクセスする前にミューテックスを「ロック」し、アクセスが完了したら「アンロック」します。
  • パフォーマンスへの影響: ミューテックスがロックされている間、他のスレッドやゴルーチンはそのリソースにアクセスできず、待機状態になります。ロックの粒度が粗すぎたり、ロックの保持時間が長すぎたりすると、並行性が低下し、プログラム全体のパフォーマンスに悪影響を与えます。

このコミットは、これらの概念を基盤として、Goランタイムのメモリ割り当てパスにおけるゼロ初期化のタイミングと条件を調整することで、全体的な性能向上を図っています。

技術的詳細

このコミットの技術的な核心は、runtime.MHeap_Alloc関数におけるメモリのゼロ初期化のロジック変更にあります。

変更前は、runtime.MHeap_Alloc関数内で、新しく割り当てられたMSpanのメモリ領域が常にゼロ初期化されていました。このゼロ初期化は、runtime.mheapミューテックス(ヒープロック)が保持されている間に行われていました。

変更後の主なポイントは以下の通りです。

  1. zeroed引数の導入:

    • runtime.MHeap_Alloc関数のシグネチャにint32 zeroedという新しい引数が追加されました。この引数は、割り当てられたメモリをゼロ初期化する必要があるかどうかを示すフラグです。
    • runtime.mallocgc(Goプログラムからのメモリ割り当て要求を処理する高レベル関数)は、このzeroedフラグをruntime.MHeap_Allocに渡すようになりました。これにより、メモリ割り当てのコンテキストに応じて、ゼロ初期化の必要性を制御できるようになります。
    • MCentral_GrowMCentralが新しいMSpanMHeapから取得する際に呼び出される)は、常にzeroed=1(ゼロ初期化が必要)を渡すように変更されました。これは、MCentralから割り当てられる小さなオブジェクトは通常、ユーザーコードによって直接使用されるため、ゼロ初期化が期待されるためです。
  2. ゼロ初期化の条件付き実行とロック外への移動:

    • runtime.MHeap_Alloc関数内で、ヒープミューテックス(runtime.mheapのロック)がアンロックされた後に、条件付きでゼロ初期化が実行されるようになりました。
    • 具体的には、if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)という条件が追加されました。
      • s != nil: MSpanが正常に割り当てられたことを確認します。
      • *(uintptr*)(s->start<<PageShift) != 0: これは、割り当てられたメモリブロックの先頭が既にゼロでない場合にのみゼロ初期化を実行するという最適化です。Goのガベージコレクタは、メモリを再利用する際に、そのメモリをゼロクリアすることがあります。もし既にゼロクリアされているのであれば、再度ゼロクリアする必要はありません。このチェックは、不要なmemclr呼び出しを避けるためのものです。
      • zeroed: 新しく追加された引数で、呼び出し元がゼロ初期化を要求しているかどうかを示します。
    • この条件が真の場合にのみ、runtime.memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);が呼び出され、メモリブロックがゼロ初期化されます。
    • 変更前は、runtime.unlock(h);に無条件でruntime.memclrが呼び出されていました。この変更により、時間のかかるmemclr操作がヒープミューテックスの保護下から外され、ミューテックスの保持時間が大幅に短縮されます。

性能への影響

  • 並行性の向上: ヒープミューテックスの保持時間が短縮されることで、複数のゴルーチンが同時にメモリ割り当てを試みる際のロック競合が減少します。これにより、特にマルチコアプロセッサ環境でのメモリ割り当てのスループットが向上します。
  • 不要なゼロ初期化の回避: zeroedフラグと、メモリが既にゼロであるかどうかのチェックにより、本当に必要な場合にのみゼロ初期化が行われるようになります。これにより、CPUサイクルとメモリ帯域の無駄な消費が削減されます。
  • 巨大なメモリブロックの最適化: この変更は特に巨大なメモリブロックの割り当てにおいて顕著な効果を発揮します。なぜなら、ゼロ初期化のコストはメモリブロックのサイズに比例するため、大きなブロックほどその影響が大きくなるからです。

このコミットは、Goランタイムのメモリ管理における細かな最適化ですが、システム全体のパフォーマンス、特に高負荷なアプリケーションにおけるメモリ割り当ての効率に大きな影響を与える可能性があります。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/runtime/malloc.goc

runtime·mallocgc関数内でruntime·MHeap_Allocを呼び出す箇所にzeroed引数が追加されました。

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -60,7 +60,7 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 		npages = size >> PageShift;
 		if((size & PageMask) != 0)
 			npages++;
-		s = runtime·MHeap_Alloc(&runtime·mheap, npages, 0, 1);
+		s = runtime·MHeap_Alloc(&runtime·mheap, npages, 0, 1, zeroed);
 		if(s == nil)
 			runtime·throw("out of memory");
 		size = npages<<PageShift;

src/pkg/runtime/malloc.h

runtime·MHeap_Alloc関数のプロトタイプ宣言にzeroed引数が追加されました。

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -380,7 +380,7 @@ struct MHeap
 extern MHeap runtime·mheap;
 
 void	runtime·MHeap_Init(MHeap *h, void *(*allocator)(uintptr));
-MSpan*	runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct);
+MSpan*	runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed);
 void	runtime·MHeap_Free(MHeap *h, MSpan *s, int32 acct);
 MSpan*	runtime·MHeap_Lookup(MHeap *h, void *v);
 MSpan*	runtime·MHeap_LookupMaybe(MHeap *h, void *v);

src/pkg/runtime/mcentral.c

MCentral_Grow関数内でruntime·MHeap_Allocを呼び出す箇所にzeroed引数として1が追加されました。

--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -207,7 +207,7 @@ MCentral_Grow(MCentral *c)
 
 	runtime·unlock(c);
 	runtime·MGetSizeClassInfo(c->sizeclass, &size, &npages, &n);
-	s = runtime·MHeap_Alloc(&runtime·mheap, npages, c->sizeclass, 0);
+	s = runtime·MHeap_Alloc(&runtime·mheap, npages, c->sizeclass, 0, 1);
 	if(s == nil) {
 		// TODO(rsc): Log out of memory
 		runtime·lock(c);

src/pkg/runtime/mheap.c

runtime·MHeap_Alloc関数の実装が変更されました。

  • 関数のシグネチャにzeroed引数が追加。
  • ヒープロック(runtime·unlock(h);)のに、zeroedtrueかつメモリが既にゼロでない場合にのみruntime·memclrを呼び出す条件付きゼロ初期化ロジックが追加。
  • ヒープロックのにあった無条件のruntime·memclr呼び出しが削除。
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -66,7 +66,7 @@ runtime·MHeap_Init(MHeap *h, void *(*alloc)(uintptr))
 // Allocate a new span of npage pages from the heap
 // and record its size class in the HeapMap and HeapMapCache.
 MSpan*
-runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct)
+runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed)
 {
 	MSpan *s;
 
@@ -81,9 +81,6 @@ runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct)
 		}
 	}
 	runtime·unlock(h);
+	if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)
+		runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
 	return s;
 }
 
@@ -138,9 +136,6 @@ HaveSpan:
 		MHeap_FreeLocked(h, t);
 	}
 
-	if(*(uintptr*)(s->start<<PageShift) != 0)
-		runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
-
 	// Record span info, because gc needs to be
 	// able to map interior pointer to containing span.
 	s->sizeclass = sizeclass;

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/runtime/mheap.c内のruntime·MHeap_Alloc関数に集約されています。

  1. runtime·MHeap_Alloc関数のシグネチャ変更: MSpan* runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed) 新たにint32 zeroedという引数が追加されました。これは、このメモリ割り当てにおいてゼロ初期化が必要かどうかを示すフラグです。このフラグの導入により、呼び出し元(runtime·mallocgcMCentral_Growなど)がゼロ初期化の要否を制御できるようになりました。

  2. ゼロ初期化ロジックの移動と条件化: 変更前は、runtime·MHeap_Allocの内部で、ヒープミューテックス(hに対するロック)がまだ保持されている状態で、無条件にruntime·memclrが呼び出されていました。

    // 変更前の削除されたコード
    if(*(uintptr*)(s->start<<PageShift) != 0)
        runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
    

    このコードは、runtime·unlock(h);の前に存在していました。つまり、ゼロ初期化処理がヒープロックを保持したまま実行されていたため、その間、他のゴルーチンからのメモリ割り当て要求がブロックされていました。

    変更後は、このゼロ初期化ロジックがruntime·unlock(h);に移動され、さらに以下の条件が追加されました。

    if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)
        runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
    
    • runtime·unlock(h);: まずヒープミューテックスを解放します。これにより、runtime·memclrが実行されている間も、他のゴルーチンがヒープにアクセスできるようになり、並行性が向上します。
    • s != nil: MSpanが正常に割り当てられたことを確認します。
    • *(uintptr*)(s->start<<PageShift) != 0: これは重要な最適化です。Goのガベージコレクタは、メモリを解放する際に、そのメモリをゼロクリアすることがあります。もし、割り当てられたMSpanの先頭が既にゼロでない(つまり、以前のデータが残っている)場合にのみruntime·memclrを実行します。これにより、既にゼロクリアされているメモリに対して不要なゼロ初期化処理を行うことを回避し、パフォーマンスを向上させます。
    • zeroed: 新しく導入されたフラグです。このフラグがtrueの場合にのみゼロ初期化が実行されます。これにより、呼び出し元がゼロ初期化を不要と判断した場合(例えば、すぐに上書きされることが確実な場合)に、その処理をスキップできるようになります。

この変更により、Goランタイムは、メモリ割り当ての並行性を高めつつ、本当に必要な場合にのみゼロ初期化を行うという、より効率的なメモリ管理を実現しています。特に、巨大なメモリブロックの割り当てにおいて、この最適化は顕著な性能改善をもたらします。

関連リンク

参考にした情報源リンク

  • GoのChange List (CL) 6094050: https://golang.org/cl/6094050
    • このコミットの元となった変更提案であり、詳細な議論や背景情報が含まれている可能性があります。
  • Goのガベージコレクションとメモリ管理に関する公式ドキュメントやブログ記事(Goのバージョンによって内容は異なる可能性がありますが、基本的な概念は共通です)。
  • Goのソースコード(特にsrc/runtime/malloc.go, src/runtime/mheap.go, src/runtime/mcentral.goなど)

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

このコミットは、Goランタイムにおけるメモリ管理の最適化に関するものです。具体的には、巨大なメモリブロックの不要なゼロ初期化(zeroization)を回避し、さらにゼロ初期化処理をヒープミューテックスの保護下から移動させることで、並行処理性能を向上させることを目的としています。

コミット

commit c1c851bbe806d8fb3f483a32e8dfac48522dfe21
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 2 18:01:11 2012 +0400

    runtime: avoid unnecessary zeroization of huge memory blocks
    +move zeroization out of the heap mutex
    
    R=golang-dev, iant, rsc
    CC=golang-dev
    https://golang.org/cl/6094050

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

https://github.com/golang/go/commit/c1c851bbe806d8fb3f483a32e8dfac48522dfe21

元コミット内容

runtime: avoid unnecessary zeroization of huge memory blocks
+move zeroization out of the heap mutex

変更の背景

Go言語のランタイムは、効率的なメモリ管理を追求しています。メモリを確保する際、セキュリティ上の理由や予測可能な動作を保証するために、新しく確保されたメモリ領域をゼロで初期化(ゼロ初期化、zeroization)することが一般的です。これにより、以前のデータが残存することによる情報漏洩のリスクを防ぎ、プログラムが未初期化のデータに依存するバグを回避できます。

しかし、特に巨大なメモリブロックを確保する場合、このゼロ初期化処理はかなりの時間を要し、パフォーマンスのボトルネックとなる可能性があります。また、Goランタイムのヒープ管理は、複数のゴルーチンからの並行アクセスを調整するためにミューテックス(runtime.mheapのロック)によって保護されています。ゼロ初期化処理がこのミューテックスの保護下で行われると、その間、他のメモリ割り当て要求がブロックされ、並行性が著しく低下します。

このコミットの背景には、以下の2つの主要な課題がありました。

  1. 不要なゼロ初期化の回避: 特定のシナリオでは、割り当てられたメモリがすぐに上書きされることが分かっている場合、ゼロ初期化は不要なオーバーヘッドとなります。例えば、ガベージコレクタがメモリを再利用する際、そのメモリがすぐに新しいデータで埋められるのであれば、事前にゼロ初期化する必要はありません。Goでは、変数が宣言されたり、new組み込み関数が使用されたりすると、明示的に初期化されない限り、自動的にその型の「ゼロ値」で初期化されます。数値型は0、ブーリアンはfalse、文字列は""(空文字列)などです。これは、Goが変数を未初期化のままにしないことを保証するためです。
  2. ヒープミューテックスの解放: ゼロ初期化のような時間のかかる処理がヒープミューテックスを保持したまま実行されると、メモリ割り当ての並行性が阻害されます。ミューテックスの保持時間を最小限に抑えることは、マルチコア環境でのスケーラビリティを向上させる上で非常に重要です。

このコミットは、これらの課題に対処し、Goランタイムのメモリ割り当て性能、特に大規模なメモリ割り当てにおける性能と並行性を改善することを目的としています。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理、特にヒープアロケータの基本的な概念と、ゼロ初期化、ミューテックスの役割について理解しておく必要があります。

Goランタイムのメモリ管理の基礎

Goランタイムは、独自のメモリ管理システムを持っています。これは、OSからのメモリ確保(mmapなど)と、そのメモリをGoプログラムが利用できる小さなチャンクに分割・管理するメカニズムから構成されます。

  • MHeap (Memory Heap): Goランタイム全体のヒープを表す構造体です。OSから取得したメモリを管理し、必要に応じてMSpanに分割して提供します。runtime.mheapというグローバルなインスタンスが存在し、ヒープ全体へのアクセスはミューテックスによって保護されています。
  • MSpan (Memory Span): 連続したページ(通常は4KB)のブロックです。MHeapから割り当てられ、特定のサイズクラスのオブジェクトを格納するために使用されるか、あるいは大きなオブジェクトのために直接割り当てられます。
  • MCentral (Memory Central): 特定のサイズクラス(例えば、8バイトオブジェクト用、16バイトオブジェクト用など)のMSpanを管理する構造体です。MCentralは、MHeapからMSpanを取得し、それをMcache(各P/論理プロセッサに紐づくキャッシュ)に提供します。
  • PageShift, PageMask: メモリページサイズに関連する定数です。PageShiftはページサイズの2の対数(例えば、4KBページなら12)、PageMaskはページサイズから1を引いた値(ページ内のオフセットを計算するのに使われる)です。s->start<<PageShiftMSpanの開始アドレスをバイト単位で計算します。s->npages<<PageShiftMSpanの合計サイズをバイト単位で計算します。

ゼロ初期化 (Zeroization)

メモリ割り当てにおいて、新しく確保されたメモリ領域の内容をすべてゼロ(または特定のパターン)で埋める処理を指します。

  • 目的:
    • セキュリティ: 以前のプログラムが使用していたデータが残存する「情報リーク」を防ぎます。これにより、機密データが誤って新しいプログラムに公開されるリスクを低減します。
    • 予測可能性: プログラムが未初期化のメモリを読み込んだ際に、不定な値ではなく常にゼロが返されることを保証します。これにより、デバッグが容易になり、バグの発生を抑制します。
  • コスト: 特に大きなメモリブロックの場合、ゼロ初期化はCPUサイクルを消費し、メモリ帯域を占有するため、無視できないオーバーヘッドとなります。Goは一般的にメモリをゼロ初期化しますが、Go 1.24での回帰のように、OSによって既にゼロ化されているメモリであっても無条件にゼロ化してしまうケースがあり、メモリ使用量が増加する問題が発生したことがあります。これは、Goランタイムが通常、OSによって既にゼロ化されていることが保証されているメモリの不要な再ゼロ化を回避しようと最適化していることを示唆しています。

ミューテックス (Mutex)

ミューテックス(Mutual Exclusion、相互排他)は、複数のスレッドやゴルーチンが共有リソース(この場合はGoランタイムのヒープ)に同時にアクセスするのを防ぐための同期プリミティブです。

  • 役割: 共有データの一貫性を保ち、競合状態(Race Condition)を防ぎます。
  • ロックとアンロック: 共有リソースにアクセスする前にミューテックスを「ロック」し、アクセスが完了したら「アンロック」します。
  • パフォーマンスへの影響: ミューテックスがロックされている間、他のスレッドやゴルーチンはそのリソースにアクセスできず、待機状態になります。ロックの粒度が粗すぎたり、ロックの保持時間が長すぎたりすると、並行性が低下し、プログラム全体のパフォーマンスに悪影響を与えます。

このコミットは、これらの概念を基盤として、Goランタイムのメモリ割り当てパスにおけるゼロ初期化のタイミングと条件を調整することで、全体的な性能向上を図っています。

技術的詳細

このコミットの技術的な核心は、runtime.MHeap_Alloc関数におけるメモリのゼロ初期化のロジック変更にあります。

変更前は、runtime.MHeap_Alloc関数内で、新しく割り当てられたMSpanのメモリ領域が常にゼロ初期化されていました。このゼロ初期化は、runtime.mheapミューテックス(ヒープロック)が保持されている間に行われていました。

変更後の主なポイントは以下の通りです。

  1. zeroed引数の導入:

    • runtime.MHeap_Alloc関数のシグネチャにint32 zeroedという新しい引数が追加されました。この引数は、割り当てられたメモリをゼロ初期化する必要があるかどうかを示すフラグです。
    • runtime.mallocgc(Goプログラムからのメモリ割り当て要求を処理する高レベル関数)は、このzeroedフラグをruntime.MHeap_Allocに渡すようになりました。これにより、メモリ割り当てのコンテキストに応じて、ゼロ初期化の必要性を制御できるようになります。
    • MCentral_GrowMCentralが新しいMSpanMHeapから取得する際に呼び出される)は、常にzeroed=1(ゼロ初期化が必要)を渡すように変更されました。これは、MCentralから割り当てられる小さなオブジェクトは通常、ユーザーコードによって直接使用されるため、ゼロ初期化が期待されるためです。
  2. ゼロ初期化の条件付き実行とロック外への移動:

    • runtime.MHeap_Alloc関数内で、ヒープミューテックス(runtime.mheapのロック)がアンロックされた後に、条件付きでゼロ初期化が実行されるようになりました。
    • 具体的には、if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)という条件が追加されました。
      • s != nil: MSpanが正常に割り当てられたことを確認します。
      • *(uintptr*)(s->start<<PageShift) != 0: これは、割り当てられたメモリブロックの先頭が既にゼロでない場合にのみゼロ初期化を実行するという最適化です。Goのガベージコレクタは、メモリを再利用する際に、そのメモリをゼロクリアすることがあります。もし既にゼロクリアされているのであれば、再度ゼロクリアする必要はありません。このチェックは、不要なmemclr呼び出しを避けるためのものです。
      • zeroed: 新しく追加された引数で、呼び出し元がゼロ初期化を要求しているかどうかを示します。
    • この条件が真の場合にのみ、runtime.memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);が呼び出され、メモリブロックがゼロ初期化されます。
    • 変更前は、runtime.unlock(h);に無条件でruntime.memclrが呼び出されていました。この変更により、時間のかかるmemclr操作がヒープミューテックスの保護下から外され、ミューテックスの保持時間が大幅に短縮されます。

性能への影響

  • 並行性の向上: ヒープミューテックスの保持時間が短縮されることで、複数のゴルーチンが同時にメモリ割り当てを試みる際のロック競合が減少します。これにより、特にマルチコアプロセッサ環境でのメモリ割り当てのスループットが向上します。
  • 不要なゼロ初期化の回避: zeroedフラグと、メモリが既にゼロであるかどうかのチェックにより、本当に必要な場合にのみゼロ初期化が行われるようになります。これにより、CPUサイクルとメモリ帯域の無駄な消費が削減されます。
  • 巨大なメモリブロックの最適化: この変更は特に巨大なメモリブロックの割り当てにおいて顕著な効果を発揮します。なぜなら、ゼロ初期化のコストはメモリブロックのサイズに比例するため、大きなブロックほどその影響が大きくなるからです。

このコミットは、Goランタイムのメモリ管理における細かな最適化ですが、システム全体のパフォーマンス、特に高負荷なアプリケーションにおけるメモリ割り当ての効率に大きな影響を与える可能性があります。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/runtime/malloc.goc

runtime·mallocgc関数内でruntime·MHeap_Allocを呼び出す箇所にzeroed引数が追加されました。

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -60,7 +60,7 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 		npages = size >> PageShift;
 		if((size & PageMask) != 0)
 			npages++;
-		s = runtime·MHeap_Alloc(&runtime·mheap, npages, 0, 1);
+		s = runtime·MHeap_Alloc(&runtime·mheap, npages, 0, 1, zeroed);
 		if(s == nil)
 			runtime·throw("out of memory");
 		size = npages<<PageShift;

src/pkg/runtime/malloc.h

runtime·MHeap_Alloc関数のプロトタイプ宣言にzeroed引数が追加されました。

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -380,7 +380,7 @@ struct MHeap
 extern MHeap runtime·mheap;
 
 void	runtime·MHeap_Init(MHeap *h, void *(*allocator)(uintptr));
-MSpan*	runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct);
+MSpan*	runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed);
 void	runtime·MHeap_Free(MHeap *h, MSpan *s, int32 acct);
 MSpan*	runtime·MHeap_Lookup(MHeap *h, void *v);
 MSpan*	runtime·MHeap_LookupMaybe(MHeap *h, void *v);

src/pkg/runtime/mcentral.c

MCentral_Grow関数内でruntime·MHeap_Allocを呼び出す箇所にzeroed引数として1が追加されました。

--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -207,7 +207,7 @@ MCentral_Grow(MCentral *c)
 
 	runtime·unlock(c);
 	runtime·MGetSizeClassInfo(c->sizeclass, &size, &npages, &n);
-	s = runtime·MHeap_Alloc(&runtime·mheap, npages, c->sizeclass, 0);
+	s = runtime·MHeap_Alloc(&runtime·mheap, npages, c->sizeclass, 0, 1);
 	if(s == nil) {
 		// TODO(rsc): Log out of memory
 		runtime·lock(c);

src/pkg/runtime/mheap.c

runtime·MHeap_Alloc関数の実装が変更されました。

  • 関数のシグネチャにzeroed引数が追加。
  • ヒープロック(runtime·unlock(h);)のに、zeroedtrueかつメモリが既にゼロでない場合にのみruntime·memclrを呼び出す条件付きゼロ初期化ロジックが追加。
  • ヒープロックのにあった無条件のruntime·memclr呼び出しが削除。
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -66,7 +66,7 @@ runtime·MHeap_Init(MHeap *h, void *(*alloc)(uintptr))
 // Allocate a new span of npage pages from the heap
 // and record its size class in the HeapMap and HeapMapCache.
 MSpan*
-runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct)
+runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed)
 {
 	MSpan *s;
 
@@ -81,9 +81,6 @@ runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct)
 		}
 	}
 	runtime·unlock(h);
+	if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)
+		runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
 	return s;
 }
 
@@ -138,9 +136,6 @@ HaveSpan:
 		MHeap_FreeLocked(h, t);
 	}
 
-	if(*(uintptr*)(s->start<<PageShift) != 0)
-		runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
-
 	// Record span info, because gc needs to be
 	// able to map interior pointer to containing span.
 	s->sizeclass = sizeclass;

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/runtime/mheap.c内のruntime·MHeap_Alloc関数に集約されています。

  1. runtime·MHeap_Alloc関数のシグネチャ変更: MSpan* runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed) 新たにint32 zeroedという引数が追加されました。これは、このメモリ割り当てにおいてゼロ初期化が必要かどうかを示すフラグです。このフラグの導入により、呼び出し元(runtime·mallocgcMCentral_Growなど)がゼロ初期化の要否を制御できるようになりました。

  2. ゼロ初期化ロジックの移動と条件化: 変更前は、runtime·MHeap_Allocの内部で、ヒープミューテックス(hに対するロック)がまだ保持されている状態で、無条件にruntime·memclrが呼び出されていました。

    // 変更前の削除されたコード
    if(*(uintptr*)(s->start<<PageShift) != 0)
        runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
    

    このコードは、runtime·unlock(h);の前に存在していました。つまり、ゼロ初期化処理がヒープロックを保持したまま実行されていたため、その間、他のゴルーチンからのメモリ割り当て要求がブロックされていました。

    変更後は、このゼロ初期化ロジックがruntime·unlock(h);に移動され、さらに以下の条件が追加されました。

    if(s != nil && *(uintptr*)(s->start<<PageShift) != 0 && zeroed)
        runtime·memclr((byte*)(s->start<<PageShift), s->npages<<PageShift);
    
    • runtime·unlock(h);: まずヒープミューテックスを解放します。これにより、runtime·memclrが実行されている間も、他のゴルーチンがヒープにアクセスできるようになり、並行性が向上します。
    • s != nil: MSpanが正常に割り当てられたことを確認します。
    • *(uintptr*)(s->start<<PageShift) != 0: これは重要な最適化です。Goのガベージコレクタは、メモリを解放する際に、そのメモリをゼロクリアすることがあります。もし、割り当てられたMSpanの先頭が既にゼロでない(つまり、以前のデータが残っている)場合にのみruntime·memclrを実行します。これにより、既にゼロクリアされているメモリに対して不要なゼロ初期化処理を行うことを回避し、パフォーマンスを向上させます。
    • zeroed: 新しく導入されたフラグです。このフラグがtrueの場合にのみゼロ初期化が実行されます。これにより、呼び出し元がゼロ初期化を不要と判断した場合(例えば、すぐに上書きされることが確実な場合)に、その処理をスキップできるようになります。

この変更により、Goランタイムは、メモリ割り当ての並行性を高めつつ、本当に必要な場合にのみゼロ初期化を行うという、より効率的なメモリ管理を実現しています。特に、巨大なメモリブロックの割り当てにおいて、この最適化は顕著な性能改善をもたらします。

関連リンク

参考にした情報源リンク

  • GoのChange List (CL) 6094050: https://golang.org/cl/6094050
  • Goのソースコード(特にsrc/runtime/malloc.go, src/runtime/mheap.go, src/runtime/mcentral.goなど)
  • Goにおけるメモリのゼロ初期化に関するWeb検索結果:
    • geeksforgeeks.org
    • stackoverflow.com
    • github.io
    • datadoghq.com
    • medium.com
    • dev.to
    • betterprogramming.pub