[インデックス 12851] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)にメモリプリフェッチ機能を追加するものです。これにより、GCのパフォーマンス、特にポインタ走査の効率が向上し、全体的なアプリケーションの実行速度が改善されます。
コミット
- コミットインデックス: 12851
- コミットハッシュ:
f09e63a2a09bfb740205a98d7995bd744e225fb8
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Sat Apr 7 17:02:44 2012 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f09e63a2a09bfb740205a98d7995bd744e225fb8
元コミット内容
runtime: add memory prefetching to GC
benchmark old ns/op new ns/op delta
garbage.BenchmarkParser 4448988000 4370531000 -1.76%
garbage.BenchmarkParser-2 4086045000 4023083000 -1.54%
garbage.BenchmarkParser-4 3677365000 3667020000 -0.28%
garbage.BenchmarkParser-8 3517253000 3543946000 +0.76%
garbage.BenchmarkParser-16 3506562000 3512518000 +0.17%
garbage.BenchmarkTree 494435529 505784058 +2.30%
garbage.BenchmarkTree-2 499652705 502774823 +0.62%
garbage.BenchmarkTree-4 468482117 465713352 -0.59%
garbage.BenchmarkTree-8 488533235 482287000 -1.28%
garbage.BenchmarkTree-16 507835176 500654882 -1.41%
garbage.BenchmarkTree2 31453900 28804600 -8.42%
garbage.BenchmarkTree2-2 21440600 19065800 -11.08%
garbage.BenchmarkTree2-4 10982000 10009100 -8.86%
garbage.BenchmarkTree2-8 7544700 6479800 -14.11%
garbage.BenchmarkTree2-16 7049500 6163200 -12.57%
garbage.BenchmarkParserPause 135815000 125360666 -7.70%
garbage.BenchmarkParserPause-2 92691523 84365476 -8.98%
garbage.BenchmarkParserPause-4 53392190 46995809 -11.98%
garbage.BenchmarkParserPause-8 36059523 30998900 -14.03%
garbage.BenchmarkParserPause-16 30174300 27613350 -8.49%
garbage.BenchmarkTreePause 20969784 22568102 +7.62%
garbage.BenchmarkTreePause-2 20215875 20975130 +3.76%
garbage.BenchmarkTreePause-4 17240709 17180666 -0.35%
garbage.BenchmarkTreePause-8 18196386 18205870 +0.05%
garbage.BenchmarkTreePause-16 20621158 20486867 -0.65%
garbage.BenchmarkTree2Pause 173992142 159995285 -8.04%
garbage.BenchmarkTree2Pause-2 131281904 118013714 -10.11%
garbage.BenchmarkTree2Pause-4 93484952 85092666 -8.98%
garbage.BenchmarkTree2Pause-8 88950523 77340809 -13.05%
garbage.BenchmarkTree2Pause-16 86071238 76557952 -11.05%
garbage.BenchmarkParserLastPause 327247000 288205000 -11.93%
garbage.BenchmarkParserLastPause-2 217039000 187336000 -13.69%
garbage.BenchmarkParserLastPause-4 119722000 105069000 -12.24%
garbage.BenchmarkParserLastPause-8 70806000 64755000 -8.55%
garbage.BenchmarkParserLastPause-16 62813000 53486000 -14.85%
garbage.BenchmarkTreeLastPause 28420000 29735000 +4.63%
garbage.BenchmarkTreeLastPause-2 23514000 25427000 +8.14%
garbage.BenchmarkTreeLastPause-4 21773000 19548000 -10.22%
garbage.BenchmarkTreeLastPause-8 24072000 24046000 -0.11%
garbage.BenchmarkTreeLastPause-16 25149000 25291000 +0.56%
garbage.BenchmarkTree2LastPause 314491000 287988000 -8.43%
garbage.BenchmarkTree2LastPause-2 214363000 190616000 -11.08%
garbage.BenchmarkTree2LastPause-4 109778000 100052000 -8.86%
garbage.BenchmarkTree2LastPause-8 75390000 64753000 -14.11%
garbage.BenchmarkTree2LastPause-16 70333000 61484000 -12.58%
FTR, below are result with the empty prefetch function,
that is, single RET but no real prefetching.
It suggests that inlinable PREFETCH is worth pursuing.
benchmark old ns/op new ns/op delta
garbage.BenchmarkParser 4448988000 4560488000 +2.51%
garbage.BenchmarkParser-2 4086045000 4129728000 +1.07%
garbage.BenchmarkParser-4 3677365000 3728672000 +1.40%
garbage.BenchmarkParser-8 3517253000 3583968000 +1.90%
garbage.BenchmarkParser-16 3506562000 3591414000 +2.42%
garbage.BenchmarkTree 494435529 499580882 +1.04%
garbage.BenchmarkTree-4 468482117 467387294 -0.23%
garbage.BenchmarkTree-8 488533235 478311117 -2.09%
garbage.BenchmarkTree-2 499652705 499324235 -0.07%
garbage.BenchmarkTree-16 507835176 502005705 -1.15%
garbage.BenchmarkTree2 31453900 33296800 +5.86%
garbage.BenchmarkTree2-2 21440600 22466400 +4.78%
garbage.BenchmarkTree2-4 10982000 11402700 +3.83%
garbage.BenchmarkTree2-8 7544700 7476500 -0.90%
garbage.BenchmarkTree2-16 7049500 7338200 +4.10%
garbage.BenchmarkParserPause 135815000 139529142 +2.73%
garbage.BenchmarkParserPause-2 92691523 95229190 +2.74%
garbage.BenchmarkParserPause-4 53392190 53083476 -0.58%
garbage.BenchmarkParserPause-8 36059523 34594800 -4.06%
garbage.BenchmarkParserPause-16 30174300 30063300 -0.37%
garbage.BenchmarkTreePause 20969784 21866920 +4.28%
garbage.BenchmarkTreePause-2 20215875 20731125 +2.55%
garbage.BenchmarkTreePause-4 17240709 17275837 +0.20%
garbage.BenchmarkTreePause-8 18196386 17898777 -1.64%
garbage.BenchmarkTreePause-16 20621158 20662772 +0.20%
garbage.BenchmarkTree2Pause 173992142 184336857 +5.95%
garbage.BenchmarkTree2Pause-2 131281904 138005714 +5.12%
garbage.BenchmarkTree2Pause-4 93484952 98449238 +5.31%
garbage.BenchmarkTree2Pause-8 88950523 89286095 +0.38%
garbage.BenchmarkTree2Pause-16 86071238 89568666 +4.06%
garbage.BenchmarkParserLastPause 327247000 342189000 +4.57%
garbage.BenchmarkParserLastPause-2 217039000 217224000 +0.09%
garbage.BenchmarkParserLastPause-4 119722000 121327000 +1.34%
garbage.BenchmarkParserLastPause-8 70806000 71941000 +1.60%
garbage.BenchmarkParserLastPause-16 62813000 60166000 -4.21%
garbage.BenchmarkTreeLastPause 28420000 27840000 -2.04%
garbage.BenchmarkTreeLastPause-2 23514000 27390000 +16.48%
garbage.BenchmarkTreeLastPause-4 21773000 21414000 -1.65%
garbage.BenchmarkTreeLastPause-8 24072000 21705000 -9.83%
garbage.BenchmarkTreeLastPause-16 25149000 23932000 -4.84%
garbage.BenchmarkTree2LastPause 314491000 332894000 +5.85%
garbage.BenchmarkTree2LastPause-2 214363000 224611000 +4.78%
garbage.BenchmarkTree2LastPause-4 109778000 113976000 +3.82%
garbage.BenchmarkTree2LastPause-8 75390000 67223000 -10.83%
garbage.BenchmarkTree2LastPause-16 70333000 73216000 +4.10%
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5991057
変更の背景
Go言語のランタイムにおけるガベージコレクション(GC)は、アプリケーションのパフォーマンスに大きな影響を与えます。GCの主要なフェーズの一つに、到達可能なオブジェクトをマークする「マークフェーズ」があります。このフェーズでは、GCはヒープ上のオブジェクトを走査し、ポインタをたどって参照されているオブジェクトを識別します。
この走査処理は、メモリ上のデータにランダムにアクセスすることが多く、CPUのキャッシュミスを頻繁に引き起こす可能性があります。キャッシュミスが発生すると、CPUはメインメモリからデータを読み込むために長い時間を待機する必要があり、これがGCの実行時間、ひいてはアプリケーション全体のレイテンシ(特にGC一時停止時間)を増加させる要因となります。
このコミットの背景には、GCの効率を改善し、特にポインタ走査におけるメモリアクセスのボトルネックを解消するという目的があります。メモリプリフェッチは、CPUが将来必要とするであろうデータを事前にキャッシュに読み込んでおくことで、このボトルネックを緩和する技術です。
コミットメッセージに含まれるベンチマーク結果は、この変更が特にBenchmarkTree2
やBenchmarkParserPause
といった、GCのポーズ時間や全体のスループットに影響を与えるシナリオで顕著な改善をもたらしていることを示しています。これは、GCが大量のポインタを走査する際に、プリフェッチが効果的に機能していることを裏付けています。
前提知識の解説
ガベージコレクション (GC)
ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不可能になった)ものを自動的に解放する仕組みです。これにより、プログラマは手動でのメモリ管理から解放され、メモリリークやダングリングポインタといった問題のリスクを低減できます。
Go言語のGCは、並行マーク・スイープ方式を採用しています。これは、アプリケーションの実行と並行してGCのマークフェーズ(到達可能なオブジェクトを識別するフェーズ)を実行し、GCによるアプリケーションの一時停止(STW: Stop The World)時間を最小限に抑えることを目指しています。
GCのマークフェーズでは、ルート(グローバル変数、スタック、レジスタなど)から始まり、そこから参照されているオブジェクトを再帰的にたどってマークしていきます。このポインタのたどり方は、メモリ上の連続した領域ではなく、散在したオブジェクトへのアクセスとなるため、キャッシュ効率が悪くなりがちです。
メモリプリフェッチ (Memory Prefetching)
メモリプリフェッチとは、CPUが将来必要と予測されるデータを、実際に必要になる前にメインメモリからキャッシュメモリに読み込んでおく最適化技術です。
- CPUキャッシュ: CPUはメインメモリよりもはるかに高速な小容量のキャッシュメモリ(L1, L2, L3キャッシュなど)を持っています。CPUがデータにアクセスする際、まずキャッシュを調べ、データがあれば高速にアクセスできます(キャッシュヒット)。データがなければメインメモリから読み込む必要があり、これはキャッシュミスと呼ばれ、数百サイクルから数千サイクルの遅延が発生する可能性があります。
- キャッシュミスとパフォーマンス: プログラムが頻繁にキャッシュミスを起こすと、CPUはデータの到着を待つ「ストール」状態になり、全体のパフォーマンスが低下します。
- プリフェッチの目的: プリフェッチは、このキャッシュミスによる遅延を隠蔽することを目的としています。データがキャッシュに事前に読み込まれていれば、CPUがそのデータにアクセスする際にはキャッシュヒットとなり、高速に処理を続行できます。
- プリフェッチ命令: 多くのCPUアーキテクチャ(x86の
PREFETCH
命令など)は、ソフトウェアから明示的にプリフェッチを指示するための命令を提供しています。これらの命令は、指定されたメモリアドレスのデータをキャッシュに読み込むようCPUにヒントを与えます。ただし、プリフェッチはあくまでヒントであり、CPUが必ずしもその通りに実行するとは限りません。また、誤ったプリフェッチはキャッシュを汚染し、かえってパフォーマンスを悪化させる可能性もあります。
Goランタイム
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、スケジューラ(ゴルーチンの管理)、メモリ割り当て、ガベージコレクション、システムコールインターフェースなどが含まれます。GCはGoランタイムの重要なコンポーネントであり、Goプログラムのメモリ管理を自動化しています。
scanblock
関数
GoのGCにおいて、scanblock
関数は、特定のメモリブロック(ヒープ上のオブジェクトの一部)を走査し、その中に含まれるポインタを識別して、参照先のオブジェクトをマークする役割を担います。この関数はGCのマークフェーズ中に繰り返し呼び出され、ヒープ全体を効率的に走査するために並行して実行されることもあります。scanblock
の効率は、GCのマークフェーズ全体のパフォーマンスに直結します。
技術的詳細
このコミットの核心は、GoランタイムのGCにおけるメモリ走査のホットパスに、CPUのプリフェッチ命令を導入した点にあります。具体的には、src/pkg/runtime/mgc0.c
ファイル内のscanblock
関数にPREFETCH(obj);
という行が追加されました。
scanblock
関数は、GCのマークフェーズ中にオブジェクトのポインタをたどる際に呼び出されます。この関数は、メモリブロック内の各オブジェクトを検査し、それがポインタを含むオブジェクトであれば、そのポインタが指す先をマーク対象としてキューに入れます。この処理は、メモリ上の連続した領域を順次アクセスするのではなく、ポインタが指す先のメモリ位置にジャンプすることが多いため、CPUキャッシュの効率が低下しやすい特性があります。
PREFETCH(obj);
命令は、現在処理しているオブジェクトobj
が指す先のメモリ領域を、CPUが将来アクセスする可能性が高いと予測し、事前にキャッシュに読み込むようヒントを与えます。これにより、実際にそのポインタをたどってデータにアクセスする際に、データが既にキャッシュに存在している可能性が高まり、キャッシュミスによる遅延を削減できます。
コミットメッセージに示されているベンチマーク結果は、この変更がもたらした具体的なパフォーマンス改善を示しています。
garbage.BenchmarkTree2
: このベンチマークは、ツリー構造のようなポインタが複雑に絡み合うデータ構造のGC性能を測定していると考えられます。プリフェッチ導入後、ns/op
(操作あたりのナノ秒)が大幅に減少しており、最大で約14%の改善が見られます。これは、プリフェッチがポインタのたどり方を効率化し、キャッシュミスを減らした結果と推測されます。garbage.BenchmarkParserPause
およびgarbage.BenchmarkTree2Pause
: これらのベンチマークは、GCの一時停止時間(ポーズ時間)を測定しています。プリフェッチの導入により、これらのポーズ時間も大幅に短縮されており、最大で約14%の改善が見られます。GCのポーズ時間はアプリケーションの応答性に直接影響するため、この改善は非常に重要です。garbage.BenchmarkParserLastPause
: GCの最後のポーズ時間を測定するベンチマークでも、最大で約14.85%の改善が見られます。
一方で、garbage.BenchmarkTree
やgarbage.BenchmarkTreePause
など、一部のベンチマークではわずかなパフォーマンス低下(+
のデルタ)が見られるものもあります。これは、プリフェッチが常に効果的であるとは限らず、場合によっては余計なキャッシュラインを読み込んだり、キャッシュを汚染したりするオーバーヘッドが発生する可能性があるためです。しかし、全体としては、特にポーズ時間の短縮において顕著な効果を発揮していることがわかります。
コミットメッセージの後半には、「empty prefetch function」でのベンチマーク結果も示されています。これは、プリフェッチ命令自体は存在するが、実際には何もプリフェッチしない(例えば、単にRET
命令を実行するだけの)関数を呼び出した場合の比較です。この結果は、実際のプリフェッチがなければパフォーマンスがむしろ悪化する(+
のデルタが多い)ことを示しており、「inlinable PREFETCH is worth pursuing」(インライン化可能なプリフェッチは追求する価値がある)という結論を裏付けています。これは、プリフェッチ命令の呼び出しオーバーヘッドが小さく、かつ実際にメモリをプリフェッチする効果が大きいことを示唆しています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/mgc0.c
ファイルに集中しており、以下の2行が追加されています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -268,6 +268,8 @@ scanblock(byte *b, int64 n)
if((bits & bitNoPointers) != 0)
continue;
+ PREFETCH(obj);
+
// If another proc wants a pointer, give it some.
if(nobj > 4 && work.nwait > 0 && work.full == nil) {
wbuf->nobj = nobj;
コアとなるコードの解説
追加されたコードは、scanblock
関数内のループの冒頭近くに位置しています。
PREFETCH(obj);
この一行が、メモリプリフェッチの導入です。
PREFETCH
は、Goランタイムが提供するマクロまたは組み込み関数であり、コンパイル時にターゲットCPUアーキテクチャの適切なプリフェッチ命令(例: x86のPREFETCHT0
など)に展開されます。obj
は、現在GCが走査しているメモリ上のオブジェクトを指すポインタです。
この行が実行されると、CPUはobj
が指すメモリ領域のデータを、実際にそのデータが必要になる前にキャッシュに読み込もうと試みます。scanblock
関数はポインタをたどってオブジェクトを走査するため、次にアクセスする可能性のあるオブジェクトのデータを事前にキャッシュに入れておくことで、キャッシュミスによる遅延を削減し、GCのマークフェーズの効率を向上させます。
この変更は、GoランタイムのGCが、より低レベルなハードウェアの特性(CPUキャッシュとプリフェッチ命令)を積極的に活用してパフォーマンスを最適化していることを示しています。
関連リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のもの)
- CPUキャッシュとメモリプリフェッチに関する一般的な情報
参考にした情報源リンク
- Go言語の公式ドキュメント (Go 1.x 時代のランタイムとGCに関する情報)
- CPUアーキテクチャにおけるプリフェッチ命令に関する一般的な知識
- 本コミットのコミットメッセージに含まれるベンチマーク結果とコメント