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

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

このコミットは、Goランタイムのメモリプロファイラ (mprof.goc) におけるメモリ割り当ての仕組みを改善するものです。具体的には、プロファイラが使用するメモリをガベージコレクタ (GC) の管理下から外し、GCが不要なメモリブロックを解放できない問題を解決することを目的としています。また、addrhash 変数を .bss セクションから移動させる変更も含まれています。

コミット

commit 4da6b36fbf2af314f62bc51576c7785a5631c016
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Wed Jan 30 09:01:31 2013 -0800

    runtime: local allocation in mprof.goc
    
    Binary data in mprof.goc may prevent the garbage collector from freeing
    memory blocks. This patch replaces all calls to runtime·mallocgc() with
    calls to an allocator private to mprof.goc, thus making the private
    memory invisible to the garbage collector. The addrhash variable is
    moved outside of the .bss section.
    
    R=golang-dev, dvyukov, rsc, minux.ma
    CC=dave, golang-dev, remyoudompheng
    https://golang.org/cl/7135063

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

https://github.com/golang/go/commit/4da6b36fbf2af314f62bc51576c7785a5631c016

元コミット内容

runtime: local allocation in mprof.goc

Binary data in mprof.goc may prevent the garbage collector from freeing
memory blocks. This patch replaces all calls to runtime·mallocgc() with
calls to an allocator private to mprof.goc, thus making the private
memory invisible to the garbage collector. The addrhash variable is
moved outside of the .bss section.

R=golang-dev, dvyukov, rsc, minux.ma
CC=dave, golang-dev, remyoudompheng
https://golang.org/cl/7135063

変更の背景

Goのランタイムには、プログラムのメモリ使用状況をプロファイルするためのメモリプロファイラ (mprof.goc) が存在します。このプロファイラは、内部でメモリを割り当ててバイナリデータを格納しますが、従来の runtime·mallocgc() を使用した割り当て方法では、プロファイラが使用するメモリがGoのガベージコレクタ (GC) の管理下に置かれていました。

この状況は、以下のような問題を引き起こす可能性がありました。

  1. GCの効率低下: プロファイラが割り当てたメモリがGCの対象となるため、GCはプロファイラがまだ使用しているメモリブロックを解放できないと判断し、不必要にメモリを保持し続ける可能性がありました。これにより、GCの実行頻度が増えたり、GCのサイクルが長くなったりして、アプリケーション全体のパフォーマンスに悪影響を与えることが考えられます。
  2. メモリリークの可能性: プロファイラが内部的に使用するデータ構造がGCによって適切に認識されない場合、GCがそのメモリを「到達可能」と誤って判断し、実際には不要になったメモリが解放されない、という論理的なメモリリークが発生するリスクがありました。
  3. プロファイラの独立性: メモリプロファイラは、それ自体がメモリ使用量を測定するツールであるため、その内部的なメモリ管理がGCの挙動に影響を与えることは望ましくありません。プロファイラ自身のメモリ割り当てがGCの対象となることで、プロファイラの測定結果がGCの挙動によって歪められる可能性も考えられます。

これらの問題を解決するため、mprof.goc が使用するメモリをGCから独立させ、プロファイラ専用のローカルなアロケータを使用するように変更する必要がありました。また、addrhash 変数が .bss セクションに配置されていることが、何らかの理由で問題を引き起こす可能性があったため、これも変更の対象となりました。.bss セクションは通常、初期化されていない静的変数やグローバル変数が配置される場所であり、特定の最適化やメモリ管理の文脈で特別な考慮が必要になることがあります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとメモリ管理に関する基本的な知識が必要です。

  1. Goのガベージコレクタ (GC):

    • Goは自動メモリ管理を採用しており、不要になったメモリを自動的に解放するガベージコレクタを備えています。
    • GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。プログラムが使用している(到達可能な)メモリブロックをマークし、マークされなかった(到達不可能な)メモリブロックを解放します。
    • GCは、ヒープメモリ(runtime·mallocgc などで割り当てられるメモリ)を管理します。
    • GCの目的は、開発者が手動でメモリを解放する手間を省き、メモリリークのリスクを低減することですが、GCの実行自体がアプリケーションのパフォーマンスに影響を与えることがあります。
  2. Goランタイムのメモリ割り当て:

    • Goプログラムがメモリを要求すると、ランタイムはOSからメモリを確保し、それを管理します。
    • runtime·mallocgc(): これはGoランタイム内部で使用される関数で、GCの管理下にあるヒープメモリを割り当てます。この関数で割り当てられたメモリはGCの対象となり、不要になればGCによって解放されます。
    • runtime·SysAlloc(): これはGoランタイムがOSから直接メモリを要求するために使用する低レベルな関数です。runtime·SysAlloc() で割り当てられたメモリは、GoのGCの直接的な管理下にはありません。通常、ランタイムの内部構造や、GCの対象外としたい特定の目的のために使用されます。
  3. メモリプロファイラ (mprof.goc):

    • Goのランタイムには、プログラムのメモリ使用状況(どのコードがどれくらいのメモリを割り当てているか、どのオブジェクトがどれくらいのメモリを消費しているかなど)を収集・分析するためのプロファイリングツールが組み込まれています。
    • mprof.goc は、このメモリプロファイラのC言語部分の実装ファイルです。Goランタイムの多くの部分はC言語で書かれており、Goのコードと連携して動作します。
  4. .bss セクション:

    • コンパイルされたプログラムのメモリレイアウトの一部です。
    • .bss (Block Started by Symbol) セクションは、初期化されていない静的変数やグローバル変数が格納される領域です。これらの変数は、プログラムの実行開始時にゼロで初期化されます。
    • .data セクションは初期化された静的変数やグローバル変数が格納される領域です。
    • .text セクションは実行可能なコードが格納される領域です。
    • 特定の状況下では、.bss セクションに配置された変数の扱いが、メモリ管理やリンカの挙動に影響を与えることがあります。
  5. uintptr:

    • Goにおける uintptr 型は、ポインタを保持するのに十分な大きさの符号なし整数型です。ポ
    • インタ演算や、メモリのアドレスを整数として扱う場合によく使用されます。GCの対象外のメモリを扱う際にも利用されます。

技術的詳細

このコミットの技術的な核心は、mprof.goc 内でのメモリ割り当て戦略の変更にあります。

  1. 専用アロケータの導入:

    • mprof.goc 内に allocate という新しい関数が導入されました。この関数は、メモリプロファイラが内部的に使用するメモリを割り当てるための専用アロケータです。
    • allocate 関数は、runtime·mallocgc() の代わりに、runtime·SysAlloc() を使用してOSから直接メモリを確保します。これにより、allocate によって割り当てられたメモリはGoのGCの管理対象外となります。
    • allocate 関数は、poolpoolfree という静的変数を使用して、確保したメモリを再利用するシンプルなプールアロケータとして機能します。
      • pool: 現在のメモリプールの開始アドレスを指すポインタ。
      • poolfree: 現在のメモリプールに残っているバイト数。
    • Chunk 定数 (32*PageSize) は、一度にOSから確保するメモリブロックのサイズを定義しています。PageSize はシステムのメモリページサイズ(通常4KB)です。
    • allocate 関数は、要求されたサイズが Chunk/2 以上の場合、直接 runtime·SysAlloc を呼び出します。これは、大きな割り当てに対してはプールを介さずに直接OSからメモリを確保する方が効率的であるためです。
    • 小さな割り当ての場合、alloclock という新しいロックを使用して、メモリプールへのアクセスを同期します。これは、複数のゴルーチンが同時にプロファイラを使用する可能性があるため、データ競合を防ぐためです。
    • プールに十分なメモリがない場合、新しい Chunk サイズのメモリブロックを runtime·SysAlloc で確保し、それを新しいプールとして使用します。
  2. runtime·mallocgc() から allocate() への置き換え:

    • mprof.goc 内の既存の runtime·mallocgc() の呼び出しが、すべて新しく導入された allocate() 関数に置き換えられました。
    • これにより、プロファイラが内部的に使用する Bucket 構造体や AddrHash 構造体などのメモリが、GCの管理下から外れ、プロファイラ専用のメモリプールから割り当てられるようになります。
  3. addrhash 変数の移動と初期化:

    • addrhash 変数は、以前は static AddrHash *addrhash[1<<AddrHashBits]; として定義されており、これは .bss セクションに配置される可能性がありました。
    • 変更後、static AddrHash **addrhash; とポインタとして宣言され、runtime·mprofinit() 関数内で allocate を使用して明示的にメモリが割り当てられるようになりました。
    • runtime·mprofinit() 関数は、runtime·schedinit() (スケジューラの初期化関数) の中で呼び出されるようになり、Goランタイムの起動時に addrhash が適切に初期化されることが保証されます。
    • この変更により、addrhash がGCの管理外のメモリに配置され、.bss セクションに関連する潜在的な問題を回避できます。

変更による効果:

  • GCの負荷軽減: メモリプロファイラが使用するメモリがGCの対象外となるため、GCはプロファイラが使用するメモリをスキャンしたり、解放を試みたりする必要がなくなります。これにより、GCの実行効率が向上し、アプリケーション全体のパフォーマンスが改善される可能性があります。
  • メモリリークの防止: プロファイラが使用するメモリがGCから独立することで、GCがプロファイラのメモリを誤って保持し続けることによる論理的なメモリリークのリスクがなくなります。
  • プロファイラの独立性向上: プロファイラ自身のメモリ管理がGCの挙動に左右されなくなるため、より正確で安定したプロファイリング結果が得られるようになります。

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

src/pkg/runtime/mprof.goc

  • 新しい静的変数と定数の追加:
    static Lock proflock, alloclock; // alloclockが追加
    static byte *pool;        // memory allocation pool
    static uintptr poolfree;  // number of bytes left in the pool
    enum {
    	Chunk = 32*PageSize,  // initial size of the pool
    };
    
  • allocate 関数の追加:
    static void*
    allocate(uintptr size)
    {
    	void *v;
    
    	if(size == 0)
    		return nil;
    
    	if(size >= Chunk/2)
    		return runtime·SysAlloc(size);
    
    	runtime·lock(&alloclock);
    	if(size > poolfree) {
    		pool = runtime·SysAlloc(Chunk);
    		poolfree = Chunk;
    	}
    	v = pool;
    	pool += size;
    	poolfree -= size;
    	runtime·unlock(&alloclock);
    	return v;
    }
    
  • runtime·mallocgcallocate への置き換え:
    • stkbucket 関数内:
      -	b = runtime·mallocgc(sizeof *b + nstk*sizeof stk[0], FlagNoProfiling, 0, 1);
      +	b = allocate(sizeof *b + nstk*sizeof stk[0]);
      
    • setaddrbucket 関数内:
      -	ah = runtime·mallocgc(sizeof *ah, FlagNoProfiling, 0, 1);
      +	ah = allocate(sizeof *ah);
      // ...
      -	e = runtime·mallocgc(64*sizeof *e, FlagNoProfiling, 0, 0);
      +	e = allocate(64*sizeof *e);
      
  • addrhash の宣言変更と初期化関数の追加:
    -static AddrHash *addrhash[1<<AddrHashBits];
    +static AddrHash **addrhash;	// points to (AddrHash*)[1<<AddrHashBits]
    // ...
    +void
    +runtime·mprofinit(void)
    +{
    +	addrhash = allocate((1<<AddrHashBits)*sizeof *addrhash);
    +}
    

src/pkg/runtime/proc.c

  • runtime·mprofinit() の呼び出し追加:
    @@ -190,6 +190,7 @@ runtime·schedinit(void)\n 	byte *p;\n \n 	m->nomemprof++;\n+	runtime·mprofinit();\n 	runtime·mallocinit();\n 	mcommoninit(m);\n ```
    
    

src/pkg/runtime/runtime.h

  • runtime·mprofinit() の関数プロトタイプ宣言追加:
    @@ -657,6 +657,7 @@ void	runtime·stackfree(void*, uintptr);\n MCache*	runtime·allocmcache(void);\n void	runtime·freemcache(MCache*);\n void	runtime·mallocinit(void);\n+void	runtime·mprofinit(void);\n bool	runtime·ifaceeq_c(Iface, Iface);\n bool	runtime·efaceeq_c(Eface, Eface);\n uintptr	runtime·ifacehash(Iface, uintptr);\n ```
    
    

コアとなるコードの解説

このコミットの核心は、mprof.goc 内で定義された新しい allocate 関数と、それに関連するメモリプールの実装です。

  1. allocate 関数:

    • この関数は、メモリプロファイラが内部的に使用するすべてのメモリ割り当てを処理するためのゲートウェイとなります。
    • runtime·SysAlloc(size) を直接呼び出すことで、OSから生のメモリを確保します。これにより、Goのガベージコレクタはこのメモリを認識せず、GCの対象外となります。
    • Chunk サイズのメモリプールを管理し、小さな割り当て要求に対しては、このプールからメモリを切り出して提供します。これにより、頻繁な runtime·SysAlloc の呼び出しを避け、システムコールによるオーバーヘッドを削減します。
    • alloclock を使用して、メモリプールへのアクセスを保護し、複数のゴルーチンからの同時アクセスによるデータ破損を防ぎます。
    • poolpoolfree は、このメモリプールの状態を追跡するために使用されます。pool は次に割り当てるメモリの開始位置を指し、poolfree はプールに残っている利用可能なバイト数を示します。
  2. runtime·mallocgc から allocate への置き換え:

    • mprof.goc 内の stkbucket 関数と setaddrbucket 関数は、メモリプロファイラの内部データ構造(BucketAddrHash)を割り当てるために runtime·mallocgc を使用していました。
    • これらの呼び出しを allocate に置き換えることで、プロファイラが使用するメモリがGCの管理下から完全に外れ、プロファイラ自身のメモリ管理ロジックによって制御されるようになります。これにより、GCの干渉を受けずにプロファイラが動作できるようになります。
  3. addrhash の初期化:

    • addrhash は、メモリプロファイラがアドレス情報を効率的にルックアップするために使用するハッシュテーブルのポインタです。
    • 以前は静的配列として宣言されていたため、.bss セクションに配置され、Goランタイムの起動時にゼロ初期化されていました。
    • 新しい実装では、addrhash はポインタのポインタ (AddrHash **addrhash) として宣言され、runtime·mprofinit() 関数内で allocate を使用して、GC管理外のメモリに明示的に割り当てられます。
    • runtime·mprofinit() は、Goランタイムの初期化フェーズである runtime·schedinit() の中で呼び出されるため、プログラムの実行開始前に addrhash が適切に初期化されることが保証されます。この変更は、.bss セクションに関連する潜在的な問題を回避し、プロファイラのメモリ管理をより一貫性のあるものにするためのものです。

これらの変更により、Goのメモリプロファイラは、自身のメモリ割り当てをGCから独立させ、より効率的かつ予測可能な方法で動作するようになります。これは、プロファイラの正確性を向上させ、Goアプリケーション全体のパフォーマンスプロファイリングの信頼性を高める上で重要な改善です。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/doc/
  • Goのガベージコレクタに関する情報: Goの公式ブログやドキュメントでGCの進化や仕組みについて解説されています。
  • Goのランタイムソースコード: https://github.com/golang/go/tree/master/src/runtime
  • Goのメモリプロファイリングに関するドキュメント: https://pkg.go.dev/runtime/pprof (これはGoのユーザーレベルのプロファイリングツールですが、内部的には mprof.goc のようなランタイム機能に依存しています)

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/7135063 は、このGerritの変更リストへのリンクです)
  • Goのランタイムに関する技術記事やブログポスト (例: GoのGCの仕組み、メモリ管理の内部など)
  • C言語のメモリセクション (.bss, .data, .text など) に関する一般的な情報源 (コンパイラやリンカのドキュメントなど)
  • runtime·SysAllocruntime·mallocgc のようなGoランタイム内部関数の挙動に関するGoのソースコードコメントや関連する設計ドキュメント。

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

このコミットは、Goランタイムのメモリプロファイラ (mprof.goc) におけるメモリ割り当ての仕組みを改善するものです。具体的には、プロファイラが使用するメモリをガベージコレクタ (GC) の管理下から外し、GCが不要なメモリブロックを解放できない問題を解決することを目的としています。また、addrhash 変数を .bss セクションから移動させる変更も含まれています。

コミット

commit 4da6b36fbf2af314f62bc51576c7785a5631c016
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Wed Jan 30 09:01:31 2013 -0800

    runtime: local allocation in mprof.goc
    
    Binary data in mprof.goc may prevent the garbage collector from freeing
    memory blocks. This patch replaces all calls to runtime·mallocgc() with
    calls to an allocator private to mprof.goc, thus making the private
    memory invisible to the garbage collector. The addrhash variable is
    moved outside of the .bss section.
    
    R=golang-dev, dvyukov, rsc, minux.ma
    CC=dave, golang-dev, remyoudompheng
    https://golang.org/cl/7135063

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

https://github.com/golang/go/commit/4da6b36fbf2af314f62bc51576c7785a5631c016

元コミット内容

runtime: local allocation in mprof.goc

Binary data in mprof.goc may prevent the garbage collector from freeing
memory blocks. This patch replaces all calls to runtime·mallocgc() with
calls to an allocator private to mprof.goc, thus making the private
memory invisible to the garbage collector. The addrhash variable is
moved outside of the .bss section.

R=golang-dev, dvyukov, rsc, minux.ma
CC=dave, golang-dev, remyoudompheng
https://golang.org/cl/7135063

変更の背景

Goのランタイムには、プログラムのメモリ使用状況をプロファイルするためのメモリプロファイラ (mprof.goc) が存在します。このプロファイラは、内部でメモリを割り当ててバイナリデータを格納しますが、従来の runtime·mallocgc() を使用した割り当て方法では、プロファイラが使用するメモリがGoのガベージコレクタ (GC) の管理下に置かれていました。

この状況は、以下のような問題を引き起こす可能性がありました。

  1. GCの効率低下: プロファイラが割り当てたメモリがGCの対象となるため、GCはプロファイラがまだ使用しているメモリブロックを解放できないと判断し、不必要にメモリを保持し続ける可能性がありました。これにより、GCの実行頻度が増えたり、GCのサイクルが長くなったりして、アプリケーション全体のパフォーマンスに悪影響を与えることが考えられます。
  2. メモリリークの可能性: プロファイラが内部的に使用するデータ構造がGCによって適切に認識されない場合、GCがそのメモリを「到達可能」と誤って判断し、実際には不要になったメモリが解放されない、という論理的なメモリリークが発生するリスクがありました。
  3. プロファイラの独立性: メモリプロファイラは、それ自体がメモリ使用量を測定するツールであるため、その内部的なメモリ管理がGCの挙動に影響を与えることは望ましくありません。プロファイラ自身のメモリ割り当てがGCの対象となることで、プロファイラの測定結果がGCの挙動によって歪められる可能性も考えられます。

これらの問題を解決するため、mprof.goc が使用するメモリをGCから独立させ、プロファイラ専用のローカルなアロケータを使用するように変更する必要がありました。また、addrhash 変数が .bss セクションに配置されていることが、何らかの理由で問題を引き起こす可能性があったため、これも変更の対象となりました。.bss セクションは通常、初期化されていない静的変数やグローバル変数が配置される場所であり、特定の最適化やメモリ管理の文脈で特別な考慮が必要になることがあります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとメモリ管理に関する基本的な知識が必要です。

  1. Goのガベージコレクタ (GC):

    • Goは自動メモリ管理を採用しており、不要になったメモリを自動的に解放するガベージコレクタを備えています。
    • GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。プログラムが使用している(到達可能な)メモリブロックをマークし、マークされなかった(到達不可能な)メモリブロックを解放します。
    • GCは、ヒープメモリ(runtime·mallocgc などで割り当てられるメモリ)を管理します。
    • GCの目的は、開発者が手動でメモリを解放する手間を省き、メモリリークのリスクを低減することですが、GCの実行自体がアプリケーションのパフォーマンスに影響を与えることがあります。
  2. Goランタイムのメモリ割り当て:

    • Goプログラムがメモリを要求すると、ランタイムはOSからメモリを確保し、それを管理します。
    • runtime·mallocgc(): これはGoランタイム内部で使用される関数で、GCの管理下にあるヒープメモリを割り当てます。この関数で割り当てられたメモリはGCの対象となり、不要になればGCによって解放されます。
    • runtime·SysAlloc(): これはGoランタイムがOSから直接メモリを要求するために使用する低レベルな関数です。runtime·SysAlloc() で割り当てられたメモリは、GoのGCの直接的な管理下にはありません。通常、ランタイムの内部構造や、GCの対象外としたい特定の目的のために使用されます。
  3. メモリプロファイラ (mprof.goc):

    • Goのランタイムには、プログラムのメモリ使用状況(どのコードがどれくらいのメモリを割り当てているか、どのオブジェクトがどれくらいのメモリを消費しているかなど)を収集・分析するためのプロファイリングツールが組み込まれています。
    • mprof.goc は、このメモリプロファイラのC言語部分の実装ファイルです。Goランタイムの多くの部分はC言語で書かれており、Goのコードと連携して動作します。
  4. .bss セクション:

    • コンパイルされたプログラムのメモリレイアウトの一部です。
    • .bss (Block Started by Symbol) セクションは、初期化されていない静的変数やグローバル変数が格納される領域です。これらの変数は、プログラムの実行開始時にゼロで初期化されます。
    • .data セクションは初期化された静的変数やグローバル変数が格納される領域です。
    • .text セクションは実行可能なコードが格納される領域です。
    • 特定の状況下では、.bss セクションに配置された変数の扱いが、メモリ管理やリンカの挙動に影響を与えることがあります。
  5. uintptr:

    • Goにおける uintptr 型は、ポインタを保持するのに十分な大きさの符号なし整数型です。ポ
    • インタ演算や、メモリのアドレスを整数として扱う場合によく使用されます。GCの対象外のメモリを扱う際にも利用されます。

技術的詳細

このコミットの技術的な核心は、mprof.goc 内でのメモリ割り当て戦略の変更にあります。

  1. 専用アロケータの導入:

    • mprof.goc 内に allocate という新しい関数が導入されました。この関数は、メモリプロファイラが内部的に使用するメモリを割り当てるための専用アロケータです。
    • allocate 関数は、runtime·mallocgc() の代わりに、runtime·SysAlloc() を使用してOSから直接メモリを確保します。これにより、allocate によって割り当てられたメモリはGoのGCの管理対象外となります。
    • allocate 関数は、poolpoolfree という静的変数を使用して、確保したメモリを再利用するシンプルなプールアロケータとして機能します。
      • pool: 現在のメモリプールの開始アドレスを指すポインタ。
      • poolfree: 現在のメモリプールに残っているバイト数。
    • Chunk 定数 (32*PageSize) は、一度にOSから確保するメモリブロックのサイズを定義しています。PageSize はシステムのメモリページサイズ(通常4KB)です。
    • allocate 関数は、要求されたサイズが Chunk/2 以上の場合、直接 runtime·SysAlloc を呼び出します。これは、大きな割り当てに対してはプールを介さずに直接OSからメモリを確保する方が効率的であるためです。
    • 小さな割り当ての場合、alloclock という新しいロックを使用して、メモリプールへのアクセスを同期します。これは、複数のゴルーチンが同時にプロファイラを使用する可能性があるため、データ競合を防ぐためです。
    • プールに十分なメモリがない場合、新しい Chunk サイズのメモリブロックを runtime·SysAlloc で確保し、それを新しいプールとして使用します。
  2. runtime·mallocgc() から allocate() への置き換え:

    • mprof.goc 内の既存の runtime·mallocgc() の呼び出しが、すべて新しく導入された allocate() 関数に置き換えられました。
    • これにより、プロファイラが内部的に使用する Bucket 構造体や AddrHash 構造体などのメモリが、GCの管理下から外れ、プロファイラ専用のメモリプールから割り当てられるようになります。
  3. addrhash 変数の移動と初期化:

    • addrhash 変数は、以前は static AddrHash *addrhash[1<<AddrHashBits]; として定義されており、これは .bss セクションに配置される可能性がありました。
    • 変更後、static AddrHash **addrhash; とポインタとして宣言され、runtime·mprofinit() 関数内で allocate を使用して明示的にメモリが割り当てられるようになりました。
    • runtime·mprofinit() 関数は、runtime·schedinit() (スケジューラの初期化関数) の中で呼び出されるようになり、Goランタイムの起動時に addrhash が適切に初期化されることが保証されます。
    • この変更により、addrhash がGCの管理外のメモリに配置され、.bss セクションに関連する潜在的な問題を回避できます。

変更による効果:

  • GCの負荷軽減: メモリプロファイラが使用するメモリがGCの対象外となるため、GCはプロファイラが使用するメモリをスキャンしたり、解放を試みたりする必要がなくなります。これにより、GCの実行効率が向上し、アプリケーション全体のパフォーマンスが改善される可能性があります。
  • メモリリークの防止: プロファイラが使用するメモリがGCから独立することで、GCがプロファイラのメモリを誤って保持し続けることによる論理的なメモリリークのリスクがなくなります。
  • プロファイラの独立性向上: プロファイラ自身のメモリ管理がGCの挙動に左右されなくなるため、より正確で安定したプロファイリング結果が得られるようになります。

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

src/pkg/runtime/mprof.goc

  • 新しい静的変数と定数の追加:
    static Lock proflock, alloclock; // alloclockが追加
    static byte *pool;        // memory allocation pool
    static uintptr poolfree;  // number of bytes left in the pool
    enum {
    	Chunk = 32*PageSize,  // initial size of the pool
    };
    
  • allocate 関数の追加:
    static void*
    allocate(uintptr size)
    {
    	void *v;
    
    	if(size == 0)
    		return nil;
    
    	if(size >= Chunk/2)
    		return runtime·SysAlloc(size);
    
    	runtime·lock(&alloclock);
    	if(size > poolfree) {
    		pool = runtime·SysAlloc(Chunk);
    		poolfree = Chunk;
    	}
    	v = pool;
    	pool += size;
    	poolfree -= size;
    	runtime·unlock(&alloclock);
    	return v;
    }
    
  • runtime·mallocgcallocate への置き換え:
    • stkbucket 関数内:
      -	b = runtime·mallocgc(sizeof *b + nstk*sizeof stk[0], FlagNoProfiling, 0, 1);
      +	b = allocate(sizeof *b + nstk*sizeof stk[0]);
      
    • setaddrbucket 関数内:
      -	ah = runtime·mallocgc(sizeof *ah, FlagNoProfiling, 0, 1);
      +	ah = allocate(sizeof *ah);
      // ...
      -	e = runtime·mallocgc(64*sizeof *e, FlagNoProfiling, 0, 0);
      +	e = allocate(64*sizeof *e);
      
  • addrhash の宣言変更と初期化関数の追加:
    -static AddrHash *addrhash[1<<AddrHashBits];
    +static AddrHash **addrhash;	// points to (AddrHash*)[1<<AddrHashBits]
    // ...
    +void
    +runtime·mprofinit(void)
    +{
    +	addrhash = allocate((1<<AddrHashBits)*sizeof *addrhash);
    +}
    

src/pkg/runtime/proc.c

  • runtime·mprofinit() の呼び出し追加:
    @@ -190,6 +190,7 @@ runtime·schedinit(void)\n 	byte *p;\n \n 	m->nomemprof++;\n+	runtime·mprofinit();\n 	runtime·mallocinit();\n 	mcommoninit(m);\n ```
    
    

src/pkg/runtime/runtime.h

  • runtime·mprofinit() の関数プロトタイプ宣言追加:
    @@ -657,6 +657,7 @@ void	runtime·stackfree(void*, uintptr);\n MCache*	runtime·allocmcache(void);\n void	runtime·freemcache(MCache*);\n void	runtime·mallocinit(void);\n+void	runtime·mprofinit(void);\n bool	runtime·ifaceeq_c(Iface, Iface);\n bool	runtime·efaceeq_c(Eface, Eface);\n uintptr	runtime·ifacehash(Iface, uintptr);\n ```
    
    

コアとなるコードの解説

このコミットの核心は、mprof.goc 内で定義された新しい allocate 関数と、それに関連するメモリプールの実装です。

  1. allocate 関数:

    • この関数は、メモリプロファイラが内部的に使用するすべてのメモリ割り当てを処理するためのゲートウェイとなります。
    • runtime·SysAlloc(size) を直接呼び出すことで、OSから生のメモリを確保します。これにより、Goのガベージコレクタはこのメモリを認識せず、GCの対象外となります。
    • Chunk サイズのメモリプールを管理し、小さな割り当て要求に対しては、このプールからメモリを切り出して提供します。これにより、頻繁な runtime·SysAlloc の呼び出しを避け、システムコールによるオーバーヘッドを削減します。
    • alloclock を使用して、メモリプールへのアクセスを保護し、複数のゴルーチンからの同時アクセスによるデータ破損を防ぎます。
    • poolpoolfree は、このメモリプールの状態を追跡するために使用されます。pool は次に割り当てるメモリの開始位置を指し、poolfree はプールに残っている利用可能なバイト数を示します。
  2. runtime·mallocgc から allocate への置き換え:

    • mprof.goc 内の stkbucket 関数と setaddrbucket 関数は、メモリプロファイラの内部データ構造(BucketAddrHash)を割り当てるために runtime·mallocgc を使用していました。
    • これらの呼び出しを allocate に置き換えることで、プロファイラが使用するメモリがGCの管理下から完全に外れ、プロファイラ自身のメモリ管理ロジックによって制御されるようになります。これにより、GCの干渉を受けずにプロファイラが動作できるようになります。
  3. addrhash の初期化:

    • addrhash は、メモリプロファイラがアドレス情報を効率的にルックアップするために使用するハッシュテーブルのポインタです。
    • 以前は静的配列として宣言されていたため、.bss セクションに配置され、Goランタイムの起動時にゼロ初期化されていました。
    • 新しい実装では、addrhash はポインタのポインタ (AddrHash **addrhash) として宣言され、runtime·mprofinit() 関数内で allocate を使用して、GC管理外のメモリに明示的に割り当てられます。
    • runtime·mprofinit() は、Goランタイムの初期化フェーズである runtime·schedinit() の中で呼び出されるため、プログラムの実行開始前に addrhash が適切に初期化されることが保証されます。この変更は、.bss セクションに関連する潜在的な問題を回避し、プロファイラのメモリ管理をより一貫性のあるものにするためのものです。

これらの変更により、Goのメモリプロファイラは、自身のメモリ割り当てをGCから独立させ、より効率的かつ予測可能な方法で動作するようになります。これは、プロファイラの正確性を向上させ、Goアプリケーション全体のパフォーマンスプロファイリングの信頼性を高める上で重要な改善です。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/doc/
  • Goのガベージコレクタに関する情報: Goの公式ブログやドキュメントでGCの進化や仕組みについて解説されています。
  • Goのランタイムソースコード: https://github.com/golang/go/tree/master/src/runtime
  • Goのメモリプロファイリングに関するドキュメント: https://pkg.go.dev/runtime/pprof (これはGoのユーザーレベルのプロファイリングツールですが、内部的には mprof.goc のようなランタイム機能に依存しています)

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/7135063 は、このGerritの変更リストへのリンクです)
  • Goのランタイムに関する技術記事やブログポスト (例: GoのGCの仕組み、メモリ管理の内部など)
  • C言語のメモリセクション (.bss, .data, .text など) に関する一般的な情報源 (コンパイラやリンカのドキュメントなど)
  • runtime·SysAllocruntime·mallocgc のようなGoランタイム内部関数の挙動に関するGoのソースコードコメントや関連する設計ドキュメント。