[インデックス 18824] ファイルの概要
このコミットは、Goランタイムのメモリ管理に関連するmarknogc
関数からアトミックなCAS (Compare-And-Swap) ループを削除する変更です。これは、Goランタイムの内部的なメモリ管理の進化、特にスパン(メモリ領域)の管理方法の変更に伴うものです。
コミット
commit 3877f1d9c80f57beb8a8dde778f6239598fd4a58
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Mar 11 17:35:49 2014 +0400
runtime: remove atomic CAS loop from marknogc
Spans are now private to threads, and the loop
is removed from all other functions.
Remove it from marknogc for consistency.
LGTM=khr, rsc
R=golang-codereviews, bradfitz, khr
CC=golang-codereviews, khr, rsc
https://golang.org/cl/72520043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3877f1d9c80f57beb8a8dde778f6239598fd4a58
元コミット内容
runtime: remove atomic CAS loop from marknogc
Spans are now private to threads, and the loop
is removed from all other functions.
Remove it from marknogc for consistency.
変更の背景
このコミットの主な背景は、Goランタイムのメモリ管理における内部的な設計変更です。コミットメッセージにある「Spans are now private to threads」という記述が重要です。
Goランタイムのメモリ管理では、ヒープメモリは「スパン(span)」と呼ばれる連続したメモリページ群に分割されて管理されます。以前のGoのバージョンでは、これらのスパンは複数のゴルーチンやOSスレッド間で共有される可能性があり、スパンの状態を変更する際には競合状態を防ぐためにアトミック操作(特にCompare-And-Swap: CAS)を用いたループが必要でした。
しかし、このコミットが行われた時期(2014年3月)には、Goランタイムのメモリ管理戦略が進化し、スパンが特定のOSスレッド(またはGoのスケジューラにおけるP: Processor)にプライベートに割り当てられるようになりました。これにより、スパンの状態を変更する際に他のスレッドとの競合を考慮する必要がなくなり、アトミックなCASループが不要になったため、marknogc
関数からもこの冗長なループが削除されました。これは、コードの簡素化とパフォーマンスの向上が目的です。
また、コミットメッセージには「the loop is removed from all other functions」とあり、他の関連する関数からも同様のCASループが既に削除されていたことが示唆されています。marknogc
からの削除は、この一貫性を保つための措置でもあります。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
-
Goのメモリ管理(ヒープとスパン):
- Goプログラムが動的に確保するメモリ(ヒープ)は、
mheap
というグローバルな構造体によって管理されます。 mheap
は、メモリを「アリーナ(arena)」と呼ばれる大きな連続したブロックに分割します。mheap.arena_start
は、このメモリ領域の開始アドレスを示します。- アリーナはさらに8KBのページに分割され、これらのページは「スパン(mspan)」と呼ばれる単位で管理されます。スパンは、特定のサイズのオブジェクトを格納するために使用されます。
- 各スパンには、そのスパン内のメモリブロックの状態(割り当て済みか、空きかなど)を示すビットマップが関連付けられています。
- Goプログラムが動的に確保するメモリ(ヒープ)は、
-
marknogc
関数:marknogc
は、Goランタイム内部で使用される関数で、特定のメモリ領域をガベージコレクション(GC)の対象外としてマークするために使われます。これは、GCが誤ってランタイムの重要なデータ構造を回収してしまわないようにするために必要です。- 関数名から「mark no gc」と推測され、GCのマークフェーズにおいて、GC対象外のメモリを識別するためのビットを設定する役割を担っていたと考えられます。
-
アトミック操作とCAS (Compare-And-Swap):
- 並行プログラミングにおいて、複数のゴルーチンやOSスレッドが共有データに同時にアクセスする場合、競合状態(race condition)が発生する可能性があります。
- アトミック操作は、不可分な操作であり、途中で他の操作に割り込まれることなく完了することが保証されます。
- CASはアトミック操作の一種で、メモリ位置の現在の値が期待される値と一致する場合にのみ、そのメモリ位置の値を新しい値に更新します。一致しない場合は更新を行わず、現在の値を返します。
- CASループは、共有データに対する更新を試みる際に、CAS操作が成功するまで繰り返し試行するパターンです。これはロックフリーなデータ構造を実装する際によく用いられます。
-
GOMAXPROCS
:GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数または関数です。これは、GoスケジューラがゴルーチンをOSスレッドにマッピングする方法に影響を与えます。- この値が1の場合、Goランタイムは単一のOSスレッドで実行されるため、共有データへのアクセスに関する競合は発生しません。そのため、CASループのようなアトミック操作は不要になります。
-
runtime.throw
:runtime.throw
は、Goランタイム内部で致命的かつ回復不可能なエラーが発生した場合に呼び出される関数です。これは、ユーザーがpanic
を呼び出すのとは異なり、プログラムの実行を即座に終了させます。
-
bitAllocated
とbitBlockBoundary
:- これらは、Goランタイムのメモリ管理におけるビットマップの一部として使用される定数です。
bitAllocated
は、メモリブロックが割り当て済みであることを示すビットです。bitBlockBoundary
は、メモリブロックの境界を示すビットです。これらのビットは、GCがメモリをスキャンし、オブジェクトの開始と終了を識別するために使用されます。
技術的詳細
このコミットの技術的な核心は、marknogc
関数におけるメモリビットマップの更新方法の変更です。
変更前は、marknogc
関数は以下のロジックでメモリビットマップを更新していました。
v
(マーク対象のポインタ)から、mheap.arena_start
を基準としたワードオフセットoff
を計算します。- このオフセットと
wordsPerBitmapWord
(ビットマップの1ワードあたりのビット数)を使用して、対応するビットマップワードb
と、そのワード内のビット位置shift
を特定します。 - CASループ:
*b
(現在のビットマップワードの値)をobits
として読み取ります。obits
のshift
位置にあるビットがbitAllocated
でない場合、runtime.throw
を呼び出して致命的なエラーを発生させます。これは、マークしようとしているメモリが既に割り当て済みでないという予期せぬ状態を示します。obits
からbitAllocated
ビットをクリアし、代わりにbitBlockBoundary
ビットを設定した新しい値bits
を計算します。runtime.gomaxprocs == 1
の場合(単一スレッド実行)、直接*b = bits
で更新し、ループを抜けます。runtime.gomaxprocs > 1
の場合(複数スレッド実行)、runtime.casp((void**)b, (void*)obits, (void*)bits)
を使用してアトミックに更新を試みます。CASが成功すればループを抜け、失敗すれば(他のスレッドが*b
を更新したため)ループを続行し、再試行します。
変更後は、このCASループ全体が削除され、以下のシンプルな直接代入に置き換えられました。
*b = (*b & ~(bitAllocated<<shift)) | bitBlockBoundary<<shift;
この変更は、スパンがスレッドにプライベートになったため、marknogc
が呼び出されるコンテキストでは、*b
に対する競合がもはや発生しないという前提に基づいています。つまり、marknogc
が操作するメモリ領域は、その時点で他のゴルーチンやOSスレッドによって同時に変更される可能性がないため、アトミックなCAS操作が不要になったということです。これにより、オーバーヘッドが削減され、コードが簡素化されました。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
ファイルのruntime·marknogc
関数が変更されています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2614,26 +2614,12 @@ runfinq(void)
void
runtime·marknogc(void *v)
{
- uintptr *b, obits, bits, off, shift;
+ uintptr *b, off, shift;
off = (uintptr*)v - (uintptr*)runtime·mheap.arena_start; // word offset
b = (uintptr*)runtime·mheap.arena_start - off/wordsPerBitmapWord - 1;
shift = off % wordsPerBitmapWord;
--
- for(;;) {
-- obits = *b;
-- if((obits>>shift & bitMask) != bitAllocated)
-- runtime·throw("bad initial state for marknogc");
-- bits = (obits & ~(bitAllocated<<shift)) | bitBlockBoundary<<shift;
-- if(runtime·gomaxprocs == 1) {
-- *b = bits;
-- break;
-- } else {
-- // more than one goroutine is potentially running: use atomic op
-- if(runtime·casp((void**)b, (void*)obits, (void*)bits))\
-- break;
-- }
-- }
-+ *b = (*b & ~(bitAllocated<<shift)) | bitBlockBoundary<<shift;
}
void
コアとなるコードの解説
変更されたruntime·marknogc
関数の主要な部分は以下の通りです。
void
runtime·marknogc(void *v)
{
uintptr *b, off, shift; // 変数宣言から obits, bits が削除された
off = (uintptr*)v - (uintptr*)runtime·mheap.arena_start; // word offset
b = (uintptr*)runtime·mheap.arena_start - off/wordsPerBitmapWord - 1;
shift = off % wordsPerBitmapWord;
// 以前のCASループ全体がこの1行に置き換えられた
*b = (*b & ~(bitAllocated<<shift)) | bitBlockBoundary<<shift;
}
-
変数宣言の変更:
- 変更前は
uintptr *b, obits, bits, off, shift;
と宣言されていましたが、obits
とbits
が不要になったため、変更後はuintptr *b, off, shift;
となっています。これは、CASループ内で一時的に使用されていたこれらの変数が、直接代入によって不要になったことを示しています。
- 変更前は
-
オフセットとビットマップ位置の計算:
off = (uintptr*)v - (uintptr*)runtime·mheap.arena_start;
v
はマーク対象のメモリブロックへのポインタです。runtime·mheap.arena_start
はGoヒープの開始アドレスです。- この行は、
v
がarena_start
からどれだけ離れているか(ワード単位のオフセット)を計算します。
b = (uintptr*)runtime·mheap.arena_start - off/wordsPerBitmapWord - 1;
- 計算されたオフセット
off
と、ビットマップの構造(wordsPerBitmapWord
)に基づいて、v
に対応するビットマップワードのポインタb
を計算します。Goのメモリ管理では、ビットマップはヒープの先頭(またはその近く)に配置されることが多いため、arena_start
から逆方向にオフセットを計算しています。
- 計算されたオフセット
shift = off % wordsPerBitmapWord;
b
が指すビットマップワード内で、どのビットを操作すべきかを示すシフト量shift
を計算します。
-
ビットマップの直接更新:
*b = (*b & ~(bitAllocated<<shift)) | bitBlockBoundary<<shift;
- これが変更の核心です。以前のCASループの代わりに、この1行でビットマップが更新されます。
bitAllocated<<shift
:bitAllocated
定数をshift
分左にシフトし、操作対象のビット位置に合わせます。~(bitAllocated<<shift)
: そのビット位置のbitAllocated
ビットを反転させます。これにより、元の*b
からbitAllocated
ビットがクリアされます。*b & ~(bitAllocated<<shift)
:*b
の現在の値から、bitAllocated
ビットをクリアします。bitBlockBoundary<<shift
:bitBlockBoundary
定数をshift
分左にシフトし、操作対象のビット位置に合わせます。| bitBlockBoundary<<shift
:bitAllocated
ビットがクリアされた値に、bitBlockBoundary
ビットを設定します。- 結果として、この操作は、指定されたメモリブロックのビットマップエントリにおいて、「割り当て済み」を示すビットをクリアし、「ブロック境界」を示すビットを設定します。これは、
marknogc
がGC対象外としてマークする際に、そのメモリブロックの状態を適切に更新するためのものです。
この変更は、Goランタイムのメモリ管理がより洗練され、スパンがスレッドプライベートになったことで、特定のコンテキストでのアトミック操作の必要性がなくなったことを明確に示しています。これにより、コードの複雑性が減り、実行効率が向上します。
関連リンク
参考にした情報源リンク
- Go runtime marknogc purpose: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEaK1pV33dpU9iqAuhZDrqsdMqzcob3VYhc9GUht8-3KPyWQChDjJmkb570uFuWw_JlH2jrwpfxOz-PL2pes9p1FnywpoK-uHVnO-mgF3hRX1lZys3bCBqbJxqnk6JS6uFQJMPUdQ==
- Go runtime atomic CAS loop memory management: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHUFrhpY18MDinMmw6Y4WFcMlMAfbmAVtSSP0QtksfLUFjXChJNGbfcEfd-9ZTIWIzIHgwU5EtYJEuT445M2bRNdOj9zrNQSD5KEQArrB_y_llGcA_QQjJqJqjDbkddCQ_-qpb_LSdRapiolA==
- Go runtime spans private to threads 2014: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHBmBnGdiG_PqqxM-r2DuUQ0uLsKgKJmc7Vzzo1UyOabwJHJuJ4vTykXFFsuh1U7YjGJbAwCGEegPRXSOnvLUL2RLHfvZmxeaZY-TMGtNUiBDH8oEvUh2WgyCKBRIjp8c4aZGk2N-GLOrCbpbAyHxwrBN0C8TMqcOEkroeJqnAoRfAnMXRByTex0Tm6uIzTJ4hLNgzL6e-EHM28daTjEFcgvFKSAzvCB7N2TbbvvEGlD2zsckvG7B
- Go runtime gomaxprocs memory allocation: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGJRFkS0l3yqhcDgacY1dYpMtDNvUDigaSk9igpjCL79aX9PWCoQhr8O7wZbLkVEyqbU2iWPsmVvbzf3FbsAELnbnaBa_zP1n_HMwDZYEJiRW8I2OycERcbg-K42UUfXGPDD7L7
- Go runtime mheap.arena_start: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH6Vjm2cHDknva7aIzLHaOYRbiLZkfUa7W1olBugSTuo1f3k6NCQESTgnaoSGLXtaPagj_IfYt8o3A6D7HLue8c3m0hTIrYMDJdfk6CrwmVcCRZNiBX63FzjSen5hfAz4FxNycMVSjlw1wdEna1eMmlxV5_c9QzfbY=
- Go runtime bitAllocated bitBlockBoundary: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFwAY8x2DdVxmCYpAnT8SOfV3MZKSVrDbn-PBA2hoYX8G8ESm42HFB-khK1z86O4aGnwdC5qhlysVcsgLt6NLnu3UxJejXWe2QjoYo-1D085rkP5CEPAr75CzwMbFz8_4lvyVaP3S0YySF77jjRNhBgbbSrVN1DyOMiePie_gfaNX1xfMi-Kah3PD5Qveru
- Go runtime casp function: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGW6y5UcpWs-Epy_HcNPILdB8Eku8Zfcm05NBqdmScatmPfqa8kPrUtJHoCFVw9XbQMsU_IhzLQkC6Ioc9ke9hdKngOAIfLXpoxuBLT51OPZR35FSA8RTltIcuSRUC6FwJSvJbDKV2r6EaakGyG1Xx_hO7Okzkpx3qhA3gZbR45_SqFxlVrsQ==
- Go runtime throw function: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGwMpaU9XHOR053u0jnCOT2RvmACMq0rAn4WQlIj7YdpsKIVgbttIS-2aW7iW8aOILvSLXU7MNW0d96I4VMezAq8quLpeKYrKMkIp-HBmf-ZTC2j-0uRtW76XgI3ZiZgyHMxS2a_wCrXbzZoOoAEDv5lkyRNFZdM0X17hn41vlb-ZNtxA==
- Go 1.2 garbage collection changes: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQENH0raoS1Qvf_8QVFfXRs3beG-tZX2Qb-UMKfB0DGSXb63LIrGtOs4koJRk3dUlrYkqs_eZ_qISmQJK0L8-bl2owT-nWzFMM-CGN3lOZJ81b6iIA==