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

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

このコミットは、Goランタイムのメモリ管理部分、特にアロケータの統計情報(malloc stats)に関する改善を目的としています。主な変更点は、統計情報を保持するデータ型をint64からuintptrに変更し、32ビットシステムでの統計情報のオーバーフローを防ぐためのメカニズムを導入したことです。これにより、特定のベンチマークにおいて顕著なパフォーマンス向上が見られます。

コミット

commit 0b09425b5cfda5bd535ec226b9368e396d6d07a7
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Jun 8 17:35:14 2012 -0400

    runtime: use uintptr where possible in malloc stats
    
    linux/arm OMAP4 pandaboard
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkBinaryTree17   68723297000  37026214000  -46.12%
    BenchmarkFannkuch11     34962402000  35958435000   +2.85%
    BenchmarkGobDecode        137298600    124182150   -9.55%
    BenchmarkGobEncode         60717160     60006700   -1.17%
    BenchmarkGzip            5647156000   5550873000   -1.70%
    BenchmarkGunzip          1196350000   1198670000   +0.19%
    BenchmarkJSONEncode       863012800    782898000   -9.28%
    BenchmarkJSONDecode      3312989000   2781800000  -16.03%
    BenchmarkMandelbrot200     45727540     45703120   -0.05%
    BenchmarkParse             74781800     59990840  -19.78%
    BenchmarkRevcomp          140043650    139462300   -0.42%
    BenchmarkTemplate        6467682000   5832153000   -9.83%\n    
    benchmark                  old MB/s     new MB/s  speedup
    BenchmarkGobDecode             5.59         6.18    1.11x
    BenchmarkGobEncode            12.64        12.79    1.01x
    BenchmarkGzip                  3.44         3.50    1.02x
    BenchmarkGunzip               16.22        16.19    1.00x
    BenchmarkJSONEncode            2.25         2.48    1.10x
    BenchmarkJSONDecode            0.59         0.70    1.19x
    BenchmarkParse                 0.77         0.97    1.26x
    BenchmarkRevcomp              18.15        18.23    1.00x
    BenchmarkTemplate              0.30         0.33    1.10x
    
    darwin/386 core duo
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkBinaryTree17   10591616577   9678245733   -8.62%\n    BenchmarkFannkuch11     10758473315  10749303846   -0.09%\n    BenchmarkGobDecode         34379785     34121250   -0.75%\n    BenchmarkGobEncode         23523721     23475750   -0.20%\n    BenchmarkGzip            2486191492   2446539568   -1.59%\n    BenchmarkGunzip           444179328    444250293   +0.02%\n    BenchmarkJSONEncode       221138507    219757826   -0.62%\n    BenchmarkJSONDecode      1056034428   1048975133   -0.67%\n    BenchmarkMandelbrot200     19862516     19868346   +0.03%\n    BenchmarkRevcomp         3742610872   3724821662   -0.48%\n    BenchmarkTemplate         960283112    944791517   -1.61%\n    
    benchmark                  old MB/s     new MB/s  speedup
    BenchmarkGobDecode            22.33        22.49    1.01x
    BenchmarkGobEncode            32.63        32.69    1.00x
    BenchmarkGzip                  7.80         7.93    1.02x
    BenchmarkGunzip               43.69        43.68    1.00x
    BenchmarkJSONEncode            8.77         8.83    1.01x
    BenchmarkJSONDecode            1.84         1.85    1.01x
    BenchmarkRevcomp              67.91        68.24    1.00x
    BenchmarkTemplate              2.02         2.05    1.01x
    
    R=rsc, 0xe2.0x9a.0x9b, mirtchovski
    CC=golang-dev, minux.ma
    https://golang.org/cl/6297047

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

https://github.com/golang/go/commit/0b09425b5cfda5bd535ec226b9368e396d6d07a7

元コミット内容

このコミットの元々の内容は、Goランタイムのメモリ割り当て統計(malloc stats)において、可能な限りuintptr型を使用するように変更することです。これは、特に32ビットアーキテクチャにおいて、統計カウンタのオーバーフローを防ぎ、パフォーマンスを向上させることを目的としています。

コミットメッセージには、linux/arm OMAP4 pandaboarddarwin/386 core duoという異なるアーキテクチャでのベンチマーク結果が示されており、特にBenchmarkBinaryTree17BenchmarkGobDecodeBenchmarkJSONEncodeBenchmarkJSONDecodeBenchmarkParseBenchmarkTemplateなどで大幅な性能改善が見られます。

変更の背景

Goランタイムのメモリ管理システムは、内部的に様々な統計情報を収集しています。これには、割り当てられたバイト数、オブジェクト数、ルックアップ回数などが含まれます。これらの統計情報は、ガベージコレクション(GC)の動作やメモリ使用状況の分析に利用されます。

しかし、これらの統計カウンタがint64型で定義されている場合、特に32ビットシステムでは、カウンタの値が非常に大きくなった際に、その値の処理や比較において非効率が生じる可能性がありました。また、理論的にはint64は64ビットなのでオーバーフローの心配は少ないですが、特定の内部処理や、32ビット環境でのint64の扱い方によっては、意図しない挙動や性能劣化を引き起こす可能性も考えられます。

このコミットの背景には、32ビットアーキテクチャ(例: ARM、x86 32-bit)でのメモリ統計情報の効率的な管理と、それに伴うパフォーマンスの最適化という課題がありました。特に、uintptr型はポインタのサイズに合わせた符号なし整数型であり、32ビットシステムでは32ビット、64ビットシステムでは64ビットのサイズを持ちます。これにより、システムのアドレス空間のサイズに合わせた最適なデータ型で統計情報を扱うことが可能になります。

前提知識の解説

Goランタイムとメモリ管理

Go言語は独自のランタイムを持っており、その中でメモリ管理(アロケーションとガベージコレクション)が行われます。Goのメモリ管理は、ヒープ領域を管理し、オブジェクトの割り当てと解放を効率的に行うことを目的としています。

  • ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goのオブジェクトは通常ヒープに割り当てられます。
  • ガベージコレクション (GC): 不要になったメモリを自動的に解放する仕組みです。GoのGCは並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作します。
  • mheap: Goランタイムのグローバルなヒープ構造体で、システム全体のメモリ管理を司ります。
  • mcache: 各P(プロセッサ、Goスケジューラが管理する論理CPU)に紐付けられたローカルなキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcacheは、グローバルなmheapへのロック競合を減らす役割も果たします。
  • MSpan: ヒープメモリの連続したページ群を表す構造体です。Goのメモリ管理はページ単位で行われます。

uintptr

Go言語におけるuintptrは、ポインタを保持するのに十分な大きさの符号なし整数型です。そのサイズは、実行されているシステムのアーキテクチャに依存します。

  • 32ビットシステム: uintptrは32ビット(4バイト)です。
  • 64ビットシステム: uintptrは64ビット(8バイト)です。

uintptrは、ポインタ演算を行う際や、C言語との相互運用でポインタを整数として扱う必要がある場合によく使用されます。このコミットでは、メモリ統計情報がポインタサイズと密接に関連しているため、uintptrが選択されました。

統計情報のオーバーフロー

コンピュータの数値型には表現できる値の範囲に限りがあります。例えば、32ビット符号なし整数は0から約42億までの値を表現できます。もしカウンタがこの上限を超えると、値がゼロに戻ったり(ラップアラウンド)、予期しない負の値になったりする「オーバーフロー」が発生します。

このコミットでは、特に32ビットシステムにおいて、メモリ割り当てやルックアップの回数が非常に多くなった場合に、統計カウンタがオーバーフローする可能性を考慮しています。int64は64ビットなので、通常はオーバーフローの心配は少ないですが、32ビットシステムでのint64の処理コストや、特定のコンテキストでの型変換の際に問題が生じる可能性がありました。

技術的詳細

このコミットの技術的な核心は、Goランタイムのメモリ管理における統計情報のデータ型をint64からuintptrに変更し、さらに32ビットシステムでのオーバーフロー対策を講じた点にあります。

  1. データ型の変更 (src/pkg/runtime/malloc.h): MCache構造体内の複数の統計カウンタ(size, local_cachealloc, local_objects, local_alloc, local_total_alloc, local_nmalloc, local_nfree, local_nlookup)および、local_by_size内のnmalloc, nfreeint64からuintptrに変更されました。 これにより、これらのカウンタはシステムのポインタサイズに合わせた最適なビット幅を持つようになります。32ビットシステムでは32ビット、64ビットシステムでは64ビットとして扱われます。これは、カウンタの読み書きの効率化に寄与し、特に32ビットシステムでのパフォーマンス向上に繋がったと考えられます。

  2. 32ビットシステムでのオーバーフロー対策 (src/pkg/runtime/malloc.goc): runtime·mallocgc関数とruntime·mlookup関数に、以下の条件分岐が追加されました。

    if (sizeof(void*) == 4 && c->local_total_alloc >= (1<<30)) {
        // purge cache stats to prevent overflow
        runtime·lock(&runtime·mheap);
        runtime·purgecachedstats(m);
        runtime·unlock(&runtime·mheap);
    }
    
    • sizeof(void*) == 4: これは、現在のシステムが32ビットアーキテクチャであるかどうかをチェックする条件です。void*のサイズが4バイト(32ビット)であれば、32ビットシステムと判断されます。
    • c->local_total_alloc >= (1<<30) (または m->mcache->local_nlookup >= (1<<30)): これは、該当する統計カウンタが約1ギガバイト(または10億回)に達したかどうかをチェックする条件です。1<<30は2の30乗、つまり1,073,741,824です。
    • runtime·purgecachedstats(m): 上記の条件が満たされた場合、この関数が呼び出されます。この関数は、現在のM(マシン、OSスレッド)に関連付けられたmcacheの統計情報をクリア(またはリセット)する役割を担います。これにより、32ビットシステムでuintptr型のカウンタがオーバーフローする前に、その値をリセットし、正確な統計情報を維持します。
    • runtime·lock(&runtime·mheap) / runtime·unlock(&runtime·mheap): purgecachedstatsの呼び出しは、グローバルなヒープロック(runtime·mheap)によって保護されています。これは、統計情報のクリア処理中に他のスレッドがヒープにアクセスしてデータ競合が発生するのを防ぐためです。

この変更により、32ビットシステムではuintptrが32ビット幅となるため、カウンタが約2GB(符号なしの場合)に近づく前に、明示的に統計情報をクリアすることで、オーバーフローを回避しつつ、カウンタの精度を保つことができます。64ビットシステムではuintptrが64ビット幅であるため、このオーバーフロー対策の条件は満たされず、purgecachedstatsは呼び出されません。

ベンチマーク結果が示すように、この変更は特にlinux/armのような32ビットアーキテクチャで顕著なパフォーマンス向上をもたらしています。これは、データ型の変更によるメモリアクセスの効率化や、カウンタのオーバーフロー対策が適切に機能した結果と考えられます。

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

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -72,6 +72,14 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 		// setup for mark sweep
 		runtime·markspan(v, 0, 0, true);
 	}
+\
+\tif (sizeof(void*) == 4 && c->local_total_alloc >= (1<<30)) {
+\t\t// purge cache stats to prevent overflow
+\t\truntime·lock(&runtime·mheap);\
+\t\truntime·purgecachedstats(m);\
+\t\truntime·unlock(&runtime·mheap);\
+\t}\
+\
 	if(!(flag & FlagNoGC))
 		runtime·markallocated(v, size, (flag&FlagNoPointers) != 0);
 
@@ -170,6 +178,13 @@ runtime·mlookup(void *v, byte **base, uintptr *size, MSpan **sp)
 	MSpan *s;
 
 	m->mcache->local_nlookup++;
+\tif (sizeof(void*) == 4 && m->mcache->local_nlookup >= (1<<30)) {
+\t\t// purge cache stats to prevent overflow
+\t\truntime·lock(&runtime·mheap);\
+\t\truntime·purgecachedstats(m);\
+\t\truntime·unlock(&runtime·mheap);\
+\t}\
+\
 	s = runtime·MHeap_LookupMaybe(&runtime·mheap, v);
 	if(sp)
 		*sp = s;

src/pkg/runtime/malloc.h

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -273,19 +273,19 @@ struct MCacheList
 struct MCache
 {
 	MCacheList list[NumSizeClasses];
-\tuint64 size;\
-\tint64 local_cachealloc;\t// bytes allocated (or freed) from cache since last lock of heap
-\tint64 local_objects;\t// objects allocated (or freed) from cache since last lock of heap
-\tint64 local_alloc;\t// bytes allocated (or freed) since last lock of heap
-\tint64 local_total_alloc;\t// bytes allocated (even if freed) since last lock of heap
-\tint64 local_nmalloc;\t// number of mallocs since last lock of heap
-\tint64 local_nfree;\t// number of frees since last lock of heap
-\tint64 local_nlookup;\t// number of pointer lookups since last lock of heap
+\tuintptr size;\
+\tuintptr local_cachealloc;\t// bytes allocated (or freed) from cache since last lock of heap
+\tuintptr local_objects;\t// objects allocated (or freed) from cache since last lock of heap
+\tuintptr local_alloc;\t// bytes allocated (or freed) from cache since last lock of heap
+\tuintptr local_total_alloc;\t// bytes allocated (even if freed) since last lock of heap
+\tuintptr local_nmalloc;\t// number of mallocs since last lock of heap
+\tuintptr local_nfree;\t// number of frees since last lock of heap
+\tuintptr local_nlookup;\t// number of pointer lookups since last lock of heap
 	int32 next_sample;\t// trigger heap sample after allocating this many bytes
 	// Statistics about allocation size classes since last lock of heap
 	struct {
-\t\tint64 nmalloc;\
-\t\tint64 nfree;\
+\t\tuintptr nmalloc;\
+\t\tuintptr nfree;\
 	} local_by_size[NumSizeClasses];
 
 };

コアとなるコードの解説

src/pkg/runtime/malloc.h の変更

このファイルでは、Goランタイムのメモリキャッシュ(MCache)構造体内の統計情報フィールドの型が変更されています。

  • uint64からuintptrへの変更:
    • size: キャッシュのサイズ。
    • local_cachealloc: キャッシュから割り当て(または解放)されたバイト数。
    • local_objects: キャッシュから割り当て(または解放)されたオブジェクト数。
    • local_alloc: ヒープロック以降に割り当て(または解放)されたバイト数。
    • local_total_alloc: ヒープロック以降に割り当てられた(解放済みであっても)バイト数。
    • local_nmalloc: ヒープロック以降のmalloc呼び出し回数。
    • local_nfree: ヒープロック以降のfree呼び出し回数。
    • local_nlookup: ヒープロック以降のポインタルックアップ回数。
    • local_by_size内のnmallocnfree: 特定のサイズクラスにおけるmalloc/free回数。

これらの変更により、統計カウンタのサイズがシステムのポインタサイズに最適化され、特に32ビットシステムでのメモリ効率とアクセス速度が向上します。

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

このファイルでは、メモリ割り当てとルックアップの主要な関数に、32ビットシステムでの統計情報オーバーフロー対策が追加されています。

  • runtime·mallocgc関数内: この関数は、ガベージコレクションを考慮したメモリ割り当ての主要なエントリポイントです。割り当て処理の後に、以下のチェックが追加されました。

    if (sizeof(void*) == 4 && c->local_total_alloc >= (1<<30)) {
        runtime·lock(&runtime·mheap);
        runtime·purgecachedstats(m);
        runtime·unlock(&runtime·mheap);
    }
    

    c->local_total_allocは、現在のP(プロセッサ)のローカルキャッシュで割り当てられた総バイト数を追跡します。32ビットシステムでこの値が約1GBに達した場合、runtime·purgecachedstatsが呼び出され、統計情報がクリアされます。

  • runtime·mlookup関数内: この関数は、指定されたアドレスがどのメモリ領域(MSpan)に属するかをルックアップする際に使用されます。ルックアップ処理の後に、以下のチェックが追加されました。

    if (sizeof(void*) == 4 && m->mcache->local_nlookup >= (1<<30)) {
        runtime·lock(&runtime·mheap);
        runtime·purgecachedstats(m);
        runtime·unlock(&runtime·mheap);
    }
    

    m->mcache->local_nlookupは、現在のM(OSスレッド)のローカルキャッシュで実行されたポインタルックアップの回数を追跡します。32ビットシステムでこの値が約10億回に達した場合、同様にruntime·purgecachedstatsが呼び出され、統計情報がクリアされます。

これらの追加されたロジックは、32ビットシステムでのuintptr型のカウンタがオーバーフローする前に、定期的に統計情報をリセットすることで、カウンタの正確性を維持し、潜在的な問題を防ぐためのものです。runtime·lockruntime·unlockは、runtime·mheapというグローバルなヒープロックを使い、統計情報のクリア処理がアトミックに行われることを保証します。

関連リンク

  • Go言語のメモリ管理に関する公式ドキュメントやブログ記事
  • Goのガベージコレクションに関する詳細な解説
  • uintptr型のGo言語仕様

参考にした情報源リンク