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

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

このコミットは、Goランタイムにおけるメモリ割り当て統計(malloc stats)の正確性とパフォーマンスを改善することを目的としています。具体的には、破棄されたMCacheから失われていたサイズクラスごとの統計情報の修正と、mstats.heap_allocの更新タイミングの最適化による速度向上を図っています。

コミット

commit c075d82ccac5eb6fca481efccc798acac00f7dae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 22 22:22:57 2013 +0400

    runtime: fix and speedup malloc stats

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

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

元コミット内容

runtime: fix and speedup malloc stats
Currently per-sizeclass stats are lost for destroyed MCache's. This patch fixes this.
Also, only update mstats.heap_alloc on heap operations, because that's the only
stat that needs to be promptly updated. Everything else needs to be up-to-date only in ReadMemStats().

R=golang-dev, remyoudompheng, dave, iant
CC=golang-dev
https://golang.org/cl/9207047

変更の背景

このコミットが行われた背景には、Goランタイムのメモリ管理における2つの主要な問題がありました。

  1. MCache破棄時の統計情報損失: Goランタイムは、スレッドローカルなメモリキャッシュであるMCacheを使用して、小さなオブジェクトの割り当てを高速化しています。しかし、MCacheが破棄される際に、そのMCacheが保持していたサイズクラスごとの割り当て統計(by_size)が適切にグローバルな統計にマージされず、失われてしまうというバグが存在していました。これにより、メモリ統計の正確性が損なわれていました。
  2. 不必要な統計情報の頻繁な更新: mstats.heap_allocを含む多くのメモリ統計は、ヒープ操作のたびに更新されていました。しかし、mstats.heap_alloc以外の統計は、ReadMemStats()関数が呼び出されたときにのみ最新の状態であれば十分であり、それ以外の頻繁な更新はパフォーマンスのオーバーヘッドとなっていました。特に、mstats.heap_allocは、GCのトリガーなど、即座に正確な値が必要とされる場面があるため、例外的に頻繁な更新が必要とされていました。

これらの問題を解決し、メモリ統計の正確性を確保しつつ、ランタイムのパフォーマンスを向上させることがこのコミットの目的です。

前提知識の解説

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

  • Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、メモリ管理など、低レベルの操作を担当します。
  • MCache (Memory Cache): Goランタイムのメモリ管理において、各M(OSスレッド)に割り当てられるスレッドローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。MCacheは、グローバルなヒープ(MHeap)からメモリブロック(MSpan)を取得し、それをさらに小さなサイズクラスに分割して、アプリケーションからの割り当て要求に応えます。これにより、グローバルなロックの競合を減らし、並行性を高めます。
  • MStats (Memory Statistics): Goランタイムが管理するグローバルなメモリ統計構造体です。ヒープの使用量、割り当てられたオブジェクトの数、ガベージコレクションの統計など、様々なメモリ関連のメトリクスを保持しています。runtime.ReadMemStats()関数を通じて、これらの統計情報をアプリケーションから取得できます。
  • サイズクラス (Size Class): Goランタイムは、メモリ割り当ての効率を高めるために、オブジェクトのサイズに応じて事前に定義された「サイズクラス」を使用します。例えば、8バイト、16バイト、32バイトなどのように、特定のサイズのチャンクでメモリを割り当てます。これにより、メモリの断片化を減らし、割り当てのオーバーヘッドを削減します。by_sizeは、このサイズクラスごとの割り当て統計を保持する配列です。
  • mstats.heap_alloc: MStats構造体の一部で、現在ヒープに割り当てられているバイトの総量を追跡します。この値は、ガベージコレクションのトリガー条件を判断する際などに重要な役割を果たします。
  • ReadMemStats(): runtimeパッケージで提供される関数で、Goランタイムの現在のメモリ統計情報を取得するために使用されます。この関数が呼び出された際に、すべてのメモリ統計が最新の状態に集約されることが期待されます。
  • purgecachedstats(): MCacheのローカルな統計情報をグローバルなmstatsにマージするための内部関数です。MCacheが破棄される際や、定期的に呼び出されて統計情報を同期します。

技術的詳細

このコミットは、Goランタイムのメモリ統計の正確性とパフォーマンスを向上させるために、以下の2つの主要な技術的変更を導入しています。

  1. MCacheのサイズクラス統計の永続化:

    • 以前は、MCacheが破棄される際に、そのMCacheが保持していたlocal_by_size(サイズクラスごとの割り当て/解放統計)がグローバルなmstats.by_sizeに適切に加算されずに失われていました。
    • このコミットでは、runtime·purgecachedstats関数にループを追加し、MCacheのlocal_by_size配列をイテレートして、それぞれのサイズクラスのnmalloc(割り当て数)とnfree(解放数)をグローバルなmstats.by_sizeに加算するように修正しました。これにより、MCacheが破棄されても、そのMCacheで行われた割り当て/解放の統計が失われることなく、正確にグローバル統計に反映されるようになりました。
    • また、src/pkg/runtime/mgc0.ccachestats関数から、このlocal_by_sizeの集約ロジックを削除しました。これは、runtime·purgecachedstatsがMCacheの統計をグローバルにマージする唯一の責任を持つべきであるという設計思想に基づいています。
  2. mstats.heap_allocの更新頻度の最適化:

    • 以前は、mstats.heap_allocを含む多くの統計が、ヒープ操作(割り当てや解放)のたびに更新されていました。
    • このコミットでは、mstats.heap_allocのみがヒープ操作時に即座に更新されるように変更されました。これは、mstats.heap_allocがガベージコレクションのトリガー条件など、即時性が求められる場面で利用されるためです。
    • その他の統計(local_cachealloc以外のMCacheのローカル統計やmstats.by_sizeなど)は、runtime·purgecachedstatsが呼び出されたとき、またはruntime.ReadMemStats()が呼び出されたときにのみ、グローバルなmstatsに集約されるように変更されました。
    • 具体的には、src/pkg/runtime/mheap.cMHeap_AllocMHeap_Free関数内で、runtime·purgecachedstats(m->mcache)の呼び出しを削除し、代わりにmstats.heap_alloc += m->mcache->local_cachealloc; m->mcache->local_cachealloc = 0;という行を追加しました。これにより、heap_allocのみが即座に更新され、他の統計の集約はpurgecachedstatsの他の呼び出し箇所(例えば、MCacheが破棄される時)に任されるようになりました。
    • この変更により、不要な統計更新のオーバーヘッドが削減され、メモリ割り当て/解放のパスが高速化されました。

これらの変更は、Goランタイムのメモリ管理の正確性と効率性を同時に向上させるものであり、特に高負荷な環境でのパフォーマンス改善に寄与します。

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

このコミットによる主要なコード変更は以下のファイルに集中しています。

  • src/pkg/runtime/malloc.goc:

    • runtime·purgecachedstats関数に、c->local_by_sizeの統計をmstats.by_sizeに加算するループが追加されました。
    --- a/src/pkg/runtime/malloc.goc
    +++ b/src/pkg/runtime/malloc.goc
    @@ -293,6 +295,12 @@ runtime·purgecachedstats(MCache *c)
     	c->local_alloc= 0;
     	mstats.total_alloc += c->local_total_alloc;
     	c->local_total_alloc= 0;
    +	for(i=0; i<nelem(c->local_by_size); i++) {
    +		mstats.by_size[i].nmalloc += c->local_by_size[i].nmalloc;
    +		c->local_by_size[i].nmalloc = 0;
    +		mstats.by_size[i].nfree += c->local_by_size[i].nfree;
    +		c->local_by_size[i].nfree = 0;
    +	}
     }
    
  • src/pkg/runtime/malloc.h:

    • MCache構造体内のnext_sampleフィールドの順序が変更されました。これは、キャッシュの局所性を改善し、頻繁にアクセスされるフィールドをまとめて配置するための最適化です。
    --- a/src/pkg/runtime/malloc.h
    +++ b/src/pkg/runtime/malloc.h
    @@ -285,15 +285,18 @@ struct MCacheList
     
     struct MCache
     {
    -	MCacheList list[NumSizeClasses];
    +	// The following members are accessed on every malloc,
    +	// so they are grouped here for better caching.
    +	int32 next_sample;	// trigger heap sample after allocating this many bytes
     	intptr local_cachealloc;	// bytes allocated (or freed) from cache since last lock of heap
    +	// The rest is not accessed on every malloc.
    +	MCacheList list[NumSizeClasses];
     	intptr local_objects;	// objects allocated (or freed) from cache since last lock of heap
     	intptr local_alloc;	// bytes allocated (or freed) since last lock of heap
     	uintptr local_total_alloc;	// bytes allocated (even if freed) since last lock of heap
     	uintptr local_nmalloc;	// number of mallocs since last lock of heap
     	uintptr local_nfree;	// number of frees since last lock of heap
     	uintptr local_nlookup;	// number of pointer lookups since last lock of heap
    -	int32 next_sample;	// trigger heap sample after allocating this many bytes
     	// Statistics about allocation size classes since last lock of heap
     	struct {
     		uintptr nmalloc;
    
  • src/pkg/runtime/mgc0.c:

    • cachestats関数から、local_by_sizeの統計をグローバルにマージするループが削除されました。この処理はruntime·purgecachedstatsに一元化されました。
    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -1863,12 +1863,6 @@ cachestats(GCStats *stats)
     		if(c==nil)
     			continue;
     		runtime·purgecachedstats(c);
    -		for(i=0; i<nelem(c->local_by_size); i++) {
    -			mstats.by_size[i].nmalloc += c->local_by_size[i].nmalloc;
    -			c->local_by_size[i].nmalloc = 0;
    -			mstats.by_size[i].nfree += c->local_by_size[i].nfree;
    -			c->local_by_size[i].nfree = 0;
    -		}
     	}
     	mstats.stacks_inuse = stacks_inuse;
     }
    
  • src/pkg/runtime/mheap.c:

    • runtime·MHeap_Allocruntime·MHeap_Free関数内で、runtime·purgecachedstats(m->mcache)の呼び出しが削除され、代わりにmstats.heap_allocの直接更新が行われるようになりました。
    --- a/src/pkg/runtime/mheap.c
    +++ b/src/pkg/runtime/mheap.c
    @@ -73,7 +73,8 @@ runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32
     	MSpan *s;
     
     	runtime·lock(h);
    -	runtime·purgecachedstats(m->mcache);
    +	mstats.heap_alloc += m->mcache->local_cachealloc;
    +	m->mcache->local_cachealloc = 0;
     	s = MHeap_AllocLocked(h, npage, sizeclass);
     	if(s != nil) {
     		mstats.heap_inuse += npage<<PageShift;
    @@ -296,7 +297,8 @@ void
     runtime·MHeap_Free(MHeap *h, MSpan *s, int32 acct)
     {
     	runtime·lock(h);
    -	runtime·purgecachedstats(m->mcache);
    +	mstats.heap_alloc += m->mcache->local_cachealloc;
    +	m->mcache->local_cachealloc = 0;
     	mstats.heap_inuse -= s->npages<<PageShift;
     	if(acct) {
     		mstats.heap_alloc -= s->npages<<PageShift;
    

コアとなるコードの解説

  • src/pkg/runtime/malloc.goc の変更:

    • runtime·purgecachedstats関数は、MCacheのローカルな統計情報をグローバルなmstatsに集約する役割を担っています。この変更により、forループが追加され、c->local_by_size配列(サイズクラスごとの割り当て/解放数)が完全にmstats.by_sizeにマージされるようになりました。これにより、MCacheが破棄されたり、統計が定期的にフラッシュされる際に、サイズクラスごとの正確な統計が失われることがなくなりました。nmallocnfreeがそれぞれグローバルな統計に加算され、MCacheのローカルカウンタはゼロにリセットされます。
  • src/pkg/runtime/malloc.h の変更:

    • MCache構造体のフィールドの順序が変更されました。next_sampleフィールドがlocal_cacheallocの前に移動されています。これは、CPUのキャッシュ効率を考慮した最適化です。next_samplelocal_cacheallocは、メモリ割り当てのたびに頻繁にアクセスされる可能性のあるフィールドであるため、これらをメモリ上で近くに配置することで、キャッシュミスを減らし、パフォーマンスを向上させることを意図しています。コメントにも「The following members are accessed on every malloc, so they are grouped here for better caching.」と明記されています。
  • src/pkg/runtime/mgc0.c の変更:

    • cachestats関数は、ガベージコレクションの統計を収集する際に呼び出される関数です。以前は、この関数内でlocal_by_sizeの統計をグローバルにマージする処理が含まれていましたが、runtime·purgecachedstats関数がその役割を完全に担うようになったため、この重複するロジックが削除されました。これにより、コードの責務が明確になり、保守性が向上します。
  • src/pkg/runtime/mheap.c の変更:

    • runtime·MHeap_Alloc(メモリ割り当て)とruntime·MHeap_Free(メモリ解放)は、Goランタイムのヒープ操作の核心となる関数です。以前は、これらの関数内でruntime·purgecachedstats(m->mcache)が呼び出され、MCacheのすべてのローカル統計がグローバルにマージされていました。
    • この変更により、runtime·purgecachedstatsの呼び出しが削除され、代わりにmstats.heap_alloc += m->mcache->local_cachealloc; m->mcache->local_cachealloc = 0;という行が追加されました。これは、mstats.heap_allocのみがヒープ操作時に即座に更新されるべきであるという最適化に基づいています。local_cacheallocは、MCacheがヒープから取得した(またはヒープに返した)バイト数を追跡するものであり、heap_allocの更新に直接関連します。他の統計は、ReadMemStats()が呼び出されたときやMCacheが破棄されるときなど、より頻度の低いタイミングで集約されることで、ヒープ操作のオーバーヘッドが削減され、全体的なパフォーマンスが向上します。

これらの変更は、Goランタイムのメモリ管理の内部動作をより正確かつ効率的にするための重要な改善です。

関連リンク

参考にした情報源リンク

  • Go runtime source code (for context and understanding of functions/structs)
  • Go memory management documentation (general concepts)
  • Articles/discussions on Go runtime internals (for deeper understanding of MCache, mstats, etc.)