[インデックス 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つの主要な問題がありました。
- MCache破棄時の統計情報損失: Goランタイムは、スレッドローカルなメモリキャッシュであるMCacheを使用して、小さなオブジェクトの割り当てを高速化しています。しかし、MCacheが破棄される際に、そのMCacheが保持していたサイズクラスごとの割り当て統計(
by_size
)が適切にグローバルな統計にマージされず、失われてしまうというバグが存在していました。これにより、メモリ統計の正確性が損なわれていました。 - 不必要な統計情報の頻繁な更新:
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つの主要な技術的変更を導入しています。
-
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.c
のcachestats
関数から、このlocal_by_size
の集約ロジックを削除しました。これは、runtime·purgecachedstats
がMCacheの統計をグローバルにマージする唯一の責任を持つべきであるという設計思想に基づいています。
- 以前は、MCacheが破棄される際に、そのMCacheが保持していた
-
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.c
のMHeap_Alloc
とMHeap_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_Alloc
とruntime·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が破棄されたり、統計が定期的にフラッシュされる際に、サイズクラスごとの正確な統計が失われることがなくなりました。nmalloc
とnfree
がそれぞれグローバルな統計に加算され、MCacheのローカルカウンタはゼロにリセットされます。
-
src/pkg/runtime/malloc.h
の変更:MCache
構造体のフィールドの順序が変更されました。next_sample
フィールドがlocal_cachealloc
の前に移動されています。これは、CPUのキャッシュ効率を考慮した最適化です。next_sample
とlocal_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 Code Review: https://golang.org/cl/9207047
参考にした情報源リンク
- 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.)