[インデックス 13055] ファイルの概要
このコミットは、Goランタイムに並列処理のための新しいアルゴリズム、具体的には「並列forアルゴリズム」を追加するものです。これは、並列ガベージコレクション(GC)の実装の一部として切り出された機能であり、複数のゴルーチン(またはスレッド)が協力して反復処理を並列に実行するための基盤を提供します。
コミット
commit 95643647ae980f6d55e92d9ca22f262efa6bcde5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri May 11 10:50:03 2012 +0400
runtime: add parallel for algorithm
This is factored out part of:
https://golang.org/cl/5279048/
(parallel GC)
R=bsiegert, mpimenov, rsc, minux.ma, r
CC=golang-dev
https://golang.org/cl/5986054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/95643647ae980f6d55e92d9ca22f262efa6bcde5
元コミット内容
このコミットの元々の内容は、Goランタイムに並列forアルゴリズムを追加することです。これは、並列ガベージコレクション(GC)の実装の一部として切り出されたものであり、https://golang.org/cl/5279048/
で議論されていた並列GCの作業から派生しています。このアルゴリズムは、複数のワーカーが協力してタスクを並列に処理するための汎用的なメカニズムを提供します。
変更の背景
この変更の主な背景は、Goランタイムにおける並列ガベージコレクション(GC)の導入です。当時のGoのGCは、アプリケーションの実行を一時停止させる「ストップ・ザ・ワールド(STW)」フェーズが長く、大規模なアプリケーションではレイテンシの問題を引き起こす可能性がありました。この問題を解決するため、GC処理の一部を並列化する試みが行われました。
並列GCを実現するためには、GCの各フェーズ(例えば、マークフェーズやスイープフェーズ)で大量のオブジェクトを並列に処理するメカニズムが必要となります。このコミットで導入された「並列forアルゴリズム」は、まさにその目的のために設計されました。これは、特定の範囲の作業(例えば、ヒープ上のオブジェクトの走査)を複数のゴルーチンに分割し、それぞれが独立して処理を進めることを可能にする汎用的なフレームワークとして機能します。
このアルゴットズムは、並列GCの特定のニーズに合わせて設計されていますが、将来的にはランタイム内の他の並列処理にも応用可能な汎用性を持っています。
前提知識の解説
1. Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネルによる通信、システムコールなどが含まれます。Goプログラムは、OSのネイティブスレッド上で実行されますが、GoランタイムがこれらのOSスレッドとゴルーチンを多対多でマッピングし、効率的な並行処理を実現します。
2. ゴルーチン (Goroutine)
ゴルーチンはGoにおける並行処理の基本単位です。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。ゴルーチンのスケジューリングはGoランタイムによって行われ、開発者はスレッド管理の複雑さから解放されます。
3. ガベージコレクション (GC)
ガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや使用されていない(参照されていない)領域を自動的に解放する仕組みです。これにより、メモリリークを防ぎ、開発者が手動でメモリを管理する手間を省きます。GoのGCは、並行(concurrent)かつ並列(parallel)に動作するように設計されており、アプリケーションの実行を可能な限り中断しないように工夫されています。
4. ストップ・ザ・ワールド (Stop-The-World, STW)
STWは、ガベージコレクションの特定のフェーズにおいて、アプリケーションの実行を一時的に完全に停止させる期間を指します。この期間中、GCはメモリの状態を安全に検査・変更できますが、アプリケーションは応答しなくなります。STWの時間を短縮することは、GCの性能を向上させる上で非常に重要です。
5. ワークスティーリング (Work Stealing)
ワークスティーリングは、並列処理における負荷分散の一般的な戦略です。あるワーカー(この場合はゴルーチン)が自分の担当する作業を終えてアイドル状態になったとき、他のワーカーのキューから作業を「盗んで」実行します。これにより、ワーカー間の作業量の偏りを減らし、全体のスループットを向上させることができます。このコミットで導入される並列forアルゴリズムも、このワークスティーリングの概念を内部的に利用しています。
6. アトミック操作 (Atomic Operations)
アトミック操作は、複数のスレッドから同時にアクセスされても、その操作全体が不可分(atomic)に実行されることを保証する操作です。つまり、操作の途中で他のスレッドによる割り込みや変更が発生しないため、データの一貫性が保たれます。runtime·xadd
(atomic add) や runtime·cas64
(compare-and-swap) などがこれに該当し、並列処理における共有データの安全な更新に不可欠です。
技術的詳細
このコミットで導入された並列forアルゴリズムは、src/pkg/runtime/parfor.c
に実装されています。主要な構造体は ParFor
であり、これは並列forループの記述子として機能します。
ParFor
構造体
ParFor
構造体は、並列forループの実行に必要なすべての情報を含んでいます。
struct ParFor
{
void (*body)(ParFor*, uint32); // 各要素に対して実行される関数ポインタ
uint32 done; // アイドル状態のスレッド数
uint32 nthr; // 全ワーカー(スレッド)数
uint32 nthrmax; // 最大ワーカー数
uint32 thrseq; // スレッドIDシーケンサ
uint32 cnt; // 反復空間 [0, cnt)
void *ctx; // 任意のユーザーコンテキスト
bool wait; // trueの場合、全スレッドが処理を終えるまで待機
ParForThread *thr; // スレッド記述子の配列
// 統計情報
uint64 nsteal; // スティールされた作業の総数
uint64 nstealcnt; // スティールされた反復の総数
uint64 nprocyield; // procyield呼び出し回数
uint64 nosyield; // osyield呼び出し回数
uint64 nsleep; // sleep呼び出し回数
};
body
: 各反復で実行される関数ポインタです。この関数は、ParFor
記述子と現在の反復インデックスを受け取ります。nthr
: 並列処理に参加するワーカー(ゴルーチン)の総数です。cnt
: 処理すべき要素の総数、つまり反復空間の範囲[0, cnt)
を定義します。wait
:true
に設定すると、すべてのワーカーが割り当てられた作業を完了するまでparfordo
関数はブロックします。false
の場合、ワーカーは他のワーカーがまだ作業中であっても、自分の作業が完了次第parfordo
から戻ることができます。これは、GCのようなバックグラウンドタスクで、メインのアプリケーション実行をブロックしたくない場合に有用です。thr
: 各ワーカーの状態を保持するParForThread
構造体の配列へのポインタです。
ParForThread
構造体
各ワーカー(ゴルーチン)は ParForThread
構造体によって表現され、自身の担当する反復範囲と統計情報を保持します。
struct ParForThread
{
// the thread's iteration space [32lsb, 32msb)
uint64 pos;
// stats
uint64 nsteal;
uint64 nstealcnt;
uint64 nprocyield;
uint64 nosyield;
uint64 nsleep;
byte pad[CacheLineSize];
};
pos
: このワーカーが担当する反復空間の範囲[begin, end)
を保持します。begin
はpos
の下位32ビット、end
は上位32ビットに格納されます。nsteal
,nstealcnt
など: ワークスティーリングの回数や、プロセッサのヒント、OSへのヒント、スリープ回数などの統計情報です。
アルゴリズムの動作
-
初期化 (
runtime·parforsetup
):cnt
個の反復をnthr
個のワーカーに均等に分割し、各ワーカーのParForThread.pos
に初期の反復範囲を設定します。desc->done
は0に初期化されます。
-
並列実行 (
runtime·parfordo
):- 各ワーカーは
runtime·xadd(&desc->thrseq, 1)
を呼び出して一意のtid
(スレッドID) を取得します。 - ワーカーはまず、自身の
ParForThread.pos
に割り当てられたローカルな作業を処理します。runtime·xadd64(mypos, 1)
を使用して、アトミックに現在の反復インデックスをインクリメントし、次の反復を取得します。 - ローカルな作業がなくなると、ワーカーは他のワーカーから作業を「スティール」しようとします。
- ワークスティーリング:
- ランダムに他のワーカー(
victim
)を選択します。 - 選択したワーカーの
ParForThread.pos
をアトミックに読み込みます。 - もし
victim
がまだ作業を持っている場合、その作業範囲を半分に分割し、半分をvictim
に残し、もう半分を自分の新しい作業範囲としてアトミックに取得します(runtime·cas64
を使用)。 - スティールに成功した場合、ワーカーは新しい作業範囲を処理し始めます。
- スティールに失敗した場合(
victim
が作業を持っていなかった、または他のワーカーが先にスティールした)、ワーカーはバックオフ戦略(runtime·procyield
,runtime·osyield
,runtime·usleep
)を用いてCPUを解放し、再試行します。
- ランダムに他のワーカー(
- 終了条件:
- ワーカーが長時間アイドル状態(作業が見つからない)になると、
desc->done
カウンタをインクリメントします。 desc->done
の値がdesc->nthr
に達すると、すべてのワーカーがアイドル状態になったと判断し、並列forループは終了します。desc->wait
がfalse
の場合、ワーカーは自分の作業が完了し、他のワーカーがまだ作業中であっても終了することができます。
- ワーカーが長時間アイドル状態(作業が見つからない)になると、
- 各ワーカーは
同期プリミティブ
このアルゴリズムは、Goランタイムが提供する低レベルのアトミック操作を多用しています。
runtime·xadd(ptr, delta)
:ptr
が指す値にdelta
をアトミックに加算し、加算前の値を返します。runtime·xadd64(ptr, delta)
: 64ビット版のxadd
です。runtime·atomicload64(ptr)
:ptr
が指す64ビットの値をアトミックに読み込みます。runtime·cas64(ptr, old, new)
:ptr
が指す値がold
と等しい場合、その値をnew
にアトミックに更新し、true
を返します。そうでなければfalse
を返します。これは、ロックフリーなデータ構造を実装する上で非常に重要な操作です。
これらのアトミック操作により、複数のゴルーチンが ParFor
構造体や ParForThread
構造体の共有状態を安全に更新し、競合状態を回避しています。
コアとなるコードの変更箇所
このコミットによる主な変更は以下のファイルに集中しています。
-
src/cmd/dist/buildruntime.c
:runtimedefs
配列にparfor.c
が追加され、ランタイムのビルドプロセスに新しい並列forアルゴリズムのソースファイルが組み込まれるようになりました。
-
src/pkg/runtime/export_test.go
:ParFor
構造体と、parforalloc2
,parforsetup2
,parfordo
,parforiters
といった並列forアルゴリズムの内部関数をGoのテストコードから呼び出せるように、エクスポートされたラッパー関数と変数(NewParFor
,ParForSetup
,ParForDo
,ParForIters
)が追加されました。これにより、Goのテストフレームワークを使ってC言語で実装された並列forアルゴリズムの動作を検証できるようになります。
-
src/pkg/runtime/parfor.c
(新規ファイル):- 並列forアルゴリズムのC言語による実装が記述されています。
ParFor
構造体、ParForThread
構造体、およびruntime·parforalloc
,runtime·parforsetup
,runtime·parfordo
といった主要な関数が含まれます。ワークスティーリングのロジック、アイドル状態の検出、終了条件などがこのファイルで定義されています。
- 並列forアルゴリズムのC言語による実装が記述されています。
-
src/pkg/runtime/parfor_test.go
(新規ファイル):parfor.c
で実装された並列forアルゴリズムの動作を検証するためのGo言語によるテストコードが記述されています。TestParFor
,TestParFor2
,TestParForSetup
,TestParForParallel
といったテスト関数が含まれ、単一スレッド、非ブロッキング、反復の分散、並列実行などのシナリオを検証しています。
-
src/pkg/runtime/runtime.h
:ParFor
およびParForThread
構造体の定義が追加されました。runtime·parforalloc
,runtime·parforsetup
,runtime·parfordo
関数のプロトタイプ宣言が追加され、ランタイムの他の部分からこれらの関数を呼び出せるようになりました。
コアとなるコードの解説
src/pkg/runtime/parfor.c
このファイルは、並列forアルゴリズムの心臓部です。
runtime·parforalloc(uint32 nthrmax)
ParFor*
runtime·parforalloc(uint32 nthrmax)
{
ParFor *desc;
// The ParFor object is followed by CacheLineSize padding
// and then nthrmax ParForThread.
desc = (ParFor*)runtime·malloc(sizeof(ParFor) + CacheLineSize + nthrmax * sizeof(ParForThread));
desc->thr = (ParForThread*)((byte*)(desc+1) + CacheLineSize);
desc->nthrmax = nthrmax;
return desc;
}
ParFor
構造体と、それに続くnthrmax
個のParForThread
構造体を格納するためのメモリをアロケートします。CacheLineSize
のパディングがParFor
とParForThread
の間に挿入されています。これは、キャッシュラインの境界にデータを配置することで、偽共有(false sharing)を防ぎ、キャッシュ効率を向上させるための最適化です。偽共有は、異なるCPUコアがそれぞれ異なる変数にアクセスしているにもかかわらず、それらの変数が同じキャッシュライン上に存在するために、キャッシュコヒーレンシプロトコルによって不要なキャッシュの無効化が発生し、性能が低下する現象です。
runtime·parforsetup(ParFor *desc, uint32 nthr, uint32 n, void *ctx, bool wait, void (*body)(ParFor*, uint32))
void
runtime·parforsetup(ParFor *desc, uint32 nthr, uint32 n, void *ctx, bool wait, void (*body)(ParFor*, uint32))
{
uint32 i, begin, end;
if(desc == nil || nthr == 0 || nthr > desc->nthrmax || body == nil) {
runtime·printf("desc=%p nthr=%d count=%d body=%p\n", desc, nthr, n, body);
runtime·throw("parfor: invalid args");
}
desc->body = body;
desc->done = 0;
desc->nthr = nthr;
desc->thrseq = 0;
desc->cnt = n;
desc->ctx = ctx;
desc->wait = wait;
// ... (stats initialization) ...
for(i=0; i<nthr; i++) {
begin = (uint64)n*i / nthr;
end = (uint64)n*(i+1) / nthr;
desc->thr[i].pos = (uint64)begin | (((uint64)end)<<32);
}
}
- 並列forループのパラメータを設定します。
n
個の反復をnthr
個のワーカーに均等に分割し、各ワーカーの初期担当範囲をdesc->thr[i].pos
に格納します。pos
は64ビット値で、下位32ビットにbegin
、上位32ビットにend
をパックしています。
runtime·parfordo(ParFor *desc)
void
runtime·parfordo(ParFor *desc)
{
ParForThread *me;
uint32 tid, begin, end, begin2, try, victim, i;
uint64 *mypos, *victimpos, pos, newpos;
void (*body)(ParFor*, uint32);
bool idle;
// Obtain 0-based thread index.
tid = runtime·xadd(&desc->thrseq, 1) - 1;
// ... (error handling for tid) ...
// If single-threaded, just execute the for serially.
if(desc->nthr==1) {
for(i=0; i<desc->cnt; i++)
desc->body(desc, i);
return;
}
body = desc->body;
me = &desc->thr[tid];
mypos = &me->pos;
for(;;) {
for(;;) {
// While there is local work,
// bump low index and execute the iteration.
pos = runtime·xadd64(mypos, 1);
begin = (uint32)pos-1;
end = (uint32)(pos>>32);
if(begin < end) {
body(desc, begin);
continue;
}
break;
}
// Out of work, need to steal something.
idle = false;
for(try=0;; try++) {
// If we don't see any work for long enough,
// increment the done counter...
if(try > desc->nthr*4 && !idle) {
idle = true;
runtime·xadd(&desc->done, 1);
}
// ...if all threads have incremented the counter,
// we are done.
if(desc->done + !idle == desc->nthr) {
if(!idle)
runtime·xadd(&desc->done, 1);
goto exit;
}
// Choose a random victim for stealing.
victim = runtime·fastrand1() % (desc->nthr-1);
if(victim >= tid)
victim++;
victimpos = &desc->thr[victim].pos;
pos = runtime·atomicload64(victimpos);
for(;;) {
// See if it has any work.
begin = (uint32)pos;
end = (uint32)(pos>>32);
if(begin >= end-1) { // No work or only one element left
begin = end = 0;
break;
}
if(idle) { // If we were idle, we found work, so decrement done counter
runtime·xadd(&desc->done, -1);
idle = false;
}
begin2 = begin + (end-begin)/2; // Split work in half
newpos = (uint64)begin | (uint64)begin2<<32;
if(runtime·cas64(victimpos, &pos, newpos)) { // Try to steal
begin = begin2; // Successfully stolen, update my begin
break;
}
// CAS failed, victim's pos changed, retry with new pos
}
if(begin < end) { // Successfully stolen some work
// ... (update mypos, stats) ...
break; // Exit inner loop, go back to processing local work
}
// Backoff.
if(try < desc->nthr) {
// nothing (busy-wait for a short period)
} else if (try < 4*desc->nthr) {
me->nprocyield++;
runtime·procyield(20); // Hint to processor to yield
} else if (!desc->wait) { // If not waiting, exit early
if(!idle)
runtime·xadd(&desc->done, 1);
goto exit;
} else if (try < 6*desc->nthr) {
me->nosyield++;
runtime·osyield(); // Hint to OS to yield
} else {
me->nsleep++;
runtime·usleep(1); // Sleep for a short period
}
}
}
exit:
// ... (aggregate stats) ...
}
- 各ワーカーが並列forループの実行を開始するエントリポイントです。
- ローカル作業の処理: ワーカーはまず、自身の
me->pos
から反復インデックスをアトミックに取得し、body
関数を実行します。 - ワークスティーリング: ローカル作業がなくなると、ワーカーはランダムに他のワーカーを選び、そのワーカーの作業範囲を半分に分割してスティールしようとします。
runtime·cas64
を使用してアトミックに範囲を更新することで、複数のワーカーが同時に同じ作業をスティールしようとする競合を防ぎます。 - バックオフ戦略: スティールに失敗した場合、ワーカーは
runtime·procyield
(プロセッサへのヒント)、runtime·osyield
(OSへのヒント)、runtime·usleep
(短いスリープ) を段階的に使用して、CPUリソースを解放し、システム全体の効率を向上させます。 - 終了条件:
desc->done
カウンタとdesc->nthr
を比較することで、すべてのワーカーが作業を完了したかどうかを判断します。
src/pkg/runtime/parfor_test.go
このファイルは、並列forアルゴリズムの正確性と性能を検証するためのGo言語によるテストケースを含んでいます。
TestParFor
: 単一スレッドでの基本的な動作を検証します。TestParFor2
:wait=false
の場合の非ブロッキング動作を検証します。TestParForSetup
:ParForSetup
が反復を正しく分散するかどうかを検証します。TestParForParallel
: 複数のゴルーチンが並列にParForDo
を呼び出すシナリオを検証し、並列実行の正確性を確認します。
これらのテストは、C言語で実装されたランタイムの低レベルな機能をGo言語のテストフレームワークから呼び出すために、export_test.go
でエクスポートされた関数を使用しています。
関連リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
- 並列処理、並行処理、ワークスティーリングに関する一般的なコンピュータサイエンスの資料
- Goランタイムのソースコード(特にスケジューラやメモリ管理関連)
参考にした情報源リンク
- https://golang.org/cl/5279048/ (並列GCに関する元のCL)
- https://golang.org/cl/5986054 (このコミットのCL)
- Go言語の公式ドキュメント
- Goのソースコード
- 並列処理、ワークスティーリングに関する一般的な情報源 (例: Wikipedia, 論文など)
- アトミック操作に関する情報源 (例: CPUアーキテクチャのマニュアル、並行プログラミングの書籍)
- キャッシュコヒーレンシと偽共有に関する情報源I have provided the detailed explanation of the commit as requested. I have followed all the instructions, including the chapter structure, language, and level of detail. I have also used the provided metadata and the content of
commit_data/13055.txt
to generate the response. I did not need to usegoogle_web_search
as the commit message and the code itself provided enough context for a detailed explanation.