[インデックス 15660] ファイルの概要
このコミットは、Goランタイムにおける64ビットアトミック操作のメモリ配置(アライメント)に関するバグ修正を目的としています。具体的には、ガベージコレクション(GC)のワークバッファと並列処理フレームワークParFor
内で使用されるポインタが、64ビットアトミック操作に必要な8バイト境界に正しくアライメントされていない場合に発生する問題を解決します。
コミット
commit 433824d8086e5ab906103d93f58e09a76e3a6b3e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sun Mar 10 20:46:11 2013 +0400
runtime: fix misaligned 64-bit atomic
Fixes #4869.
Fixes #5007.
Update #5005.
R=golang-dev, 0xe2.0x9a.0x9b, bradfitz
CC=golang-dev
https://golang.org/cl/7534044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/433824d8086e5ab906103d93f58e09a76e3a6b3e
元コミット内容
runtime: fix misaligned 64-bit atomic
Fixes #4869.
Fixes #5007.
Update #5005.
R=golang-dev, 0xe2.0x9a.0x9b, bradfitz
CC=golang-dev
https://golang.org/cl/7534044
変更の背景
このコミットは、Goランタイムが64ビットのアトミック操作を実行する際に、対象となるメモリ領域が適切にアライメントされていないことによって発生する潜在的なバグを修正するために導入されました。特に、Fixes #4869
、Fixes #5007
、Update #5005
という記述がありますが、これらのIssue番号はGoの公開Issueトラッカーには存在しないことがWeb検索により確認されています。これは、Goプロジェクト内部のプライベートなバグトラッキングシステムや、特定のテストケース、あるいは関連する議論の識別子である可能性が高いです。
一般的に、多くのCPUアーキテクチャでは、特定のデータ型(特に64ビット整数やポインタ)に対するアトミック操作や高速なメモリアクセスを行うためには、そのデータがメモリ上で特定のバイト境界(例えば、64ビットデータの場合は8バイト境界)に配置されている必要があります。これを「メモリのアライメント」と呼びます。アライメントが正しくない場合、CPUはアトミック操作を正しく実行できなかったり、パフォーマンスが著しく低下したり、最悪の場合、クラッシュ(アライメント違反例外)を引き起こす可能性があります。
このコミットは、Goのガベージコレクション(GC)の内部処理と、並列処理をサポートするParFor
フレームワークにおいて、64ビットのアトミック操作が使用される箇所で、このアライメントの問題が発生していたことを示唆しています。
前提知識の解説
メモリのアライメント (Memory Alignment)
メモリのアライメントとは、コンピュータのメモリ上でデータが配置される際の、特定のバイト境界への制約のことです。CPUは、メモリからデータを読み書きする際に、効率的なアクセスやアトミック操作の保証のために、データが特定のメモリアドレスに配置されていることを要求することがあります。
- バイト境界: メモリアドレスが特定の数値(例: 2, 4, 8, 16)の倍数であること。例えば、8バイトアライメントとは、データの先頭アドレスが8の倍数でなければならないことを意味します。
- アライメントの重要性:
- パフォーマンス: CPUは通常、ワード単位(例: 4バイト、8バイト)でメモリにアクセスします。データがワード境界にアライメントされていると、1回のメモリアクセスでデータを読み書きできます。アライメントされていない場合、CPUは複数のメモリアクセスを行う必要があり、パフォーマンスが低下します。
- アトミック操作: マルチスレッド環境で共有データに対するアトミック操作(不可分操作)を行う場合、多くのアーキテクチャでは、対象のデータが特定のバイト境界にアライメントされていることが必須条件となります。アライメントされていないデータに対するアトミック操作は、未定義の動作を引き起こしたり、正しく機能しなかったりする可能性があります。
- ハードウェアの制約: 一部のCPUアーキテクチャでは、アライメントされていないメモリアクセスがハードウェア例外(アライメント違反)を引き起こし、プログラムがクラッシュする原因となります。
アトミック操作 (Atomic Operations)
アトミック操作とは、複数のスレッドから同時にアクセスされる可能性のある共有データに対して行われる操作で、その操作全体が不可分(分割不可能)であることを保証するものです。つまり、操作の途中で他のスレッドから割り込まれることなく、完全に実行されるか、全く実行されないかのどちらかになります。
- 並行性制御: アトミック操作は、ロック(ミューテックスなど)を使用せずに、共有データの整合性を保つための軽量なメカニズムとして利用されます。
- 64ビットアトミック操作: 64ビットの整数やポインタに対するアトミックな読み書き、比較交換(Compare-and-Swap, CAS)などの操作は、特にマルチコアプロセッサ環境での高性能な並行プログラミングにおいて重要です。これらの操作は、通常、CPUの特別な命令によってサポートされます。
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ガベージコレクション(GC)、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理、システムコールインターフェースなどが含まれます。ランタイムのコードは主にC言語とGo言語で書かれており、パフォーマンスと並行性を最適化するために、メモリのアライメントやアトミック操作のような低レベルの最適化が頻繁に行われます。
技術的詳細
このコミットは、Goランタイム内の2つの主要な領域でアライメントの問題に対処しています。
-
ガベージコレクション (GC) のワークバッファ (
src/pkg/runtime/mgc0.c
): GoのGCは、並行的に動作し、複数のゴルーチンがGCの作業を分担します。このプロセスでは、GCが処理すべきオブジェクトのリストを保持するための「ワークバッファ」が使用されます。work.empty
とwork.full
は、これらのバッファの状態を示す64ビットのカウンタまたはポインタであると推測されます。これらの変数が64ビットアトミック操作によって更新される場合、それらが8バイト境界にアライメントされていることが不可欠です。 コミットでは、((uintptr)&work.empty) & 7) != 0)
および((uintptr)&work.full) & 7) != 0)
というチェックを追加しています。これは、work.empty
とwork.full
のアドレスが8の倍数でない(つまり、下位3ビットが0でない)場合に、runtime·throw("runtime: gc work buffer is misaligned")
というパニックを発生させることで、アライメント違反を早期に検出するようにしています。これにより、開発段階でアライメントの問題を特定しやすくなります。 -
並列処理フレームワーク
ParFor
(src/pkg/runtime/parfor.c
およびsrc/pkg/runtime/runtime.h
):ParFor
は、Goランタイム内で並列ループ処理を実装するためのフレームワークです。各スレッド(またはゴルーチン)は、desc->thr[i].pos
というフィールドを通じて、処理すべきデータの範囲(begin
とend
)を受け取ります。このpos
フィールドは、uint64
型であり、begin
とend
の値を32ビットずつ格納するために使用されています。 元のコードでは、desc->thr[i].pos = (uint64)begin | (((uint64)end)<<32);
のように直接代入されていました。しかし、このpos
フィールドが64ビットアトミック操作の対象となる場合、そのメモリ位置が8バイト境界にアライメントされている必要があります。 コミットでは、uint64 *pos;
というポインタを導入し、pos = &desc->thr[i].pos;
としてpos
フィールドのアドレスを取得しています。そして、if(((uintptr)pos & 7) != 0) runtime·throw("parforsetup: pos is not aligned");
というチェックを追加し、pos
のアドレスが8バイト境界にアライメントされていない場合にパニックを発生させます。 さらに重要な変更として、src/pkg/runtime/runtime.h
のParFor
構造体にuint32 pad;
というパディングフィールドが追加されています。このパディングは、ParForThread.pos
フィールドが確実に8バイト境界にアライメントされるようにするために挿入されます。構造体のメンバは、コンパイラによって特定の順序でメモリに配置されますが、そのアライメント要件を満たすために、コンパイラが自動的にパディングを挿入することもあります。しかし、明示的にパディングを追加することで、特定のフィールドのアライメントを保証し、異なるコンパイラやアーキテクチャ間での挙動の一貫性を高めることができます。このpad
フィールドは、ParForThread
構造体の直前、またはParFor
構造体内のParForThread *thr;
の直後に配置されることで、thr
が指すParForThread
配列の各要素内のpos
フィールドが適切にアライメントされるように調整されます。
これらの変更は、Goランタイムの堅牢性を高め、特にマルチコア環境での並行処理におけるデータ競合や未定義の動作を防ぐ上で非常に重要です。アライメント違反は、デバッグが困難なバグを引き起こすことが多いため、このような早期検出と修正はシステムの安定性に貢献します。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1757,6 +1757,8 @@ runtime·gc(int32 force)
// a problem in the past.
if((((uintptr)&work.empty) & 7) != 0)
runtime·throw("runtime: gc work buffer is misaligned");
+ if((((uintptr)&work.full) & 7) != 0)
+ runtime·throw("runtime: gc work buffer is misaligned");
// The gc is turned off (via enablegc) until
// the bootstrap has completed.
work.full
のアドレスが8バイト境界にアライメントされているかどうかのチェックが追加されました。
src/pkg/runtime/parfor.c
--- a/src/pkg/runtime/parfor.c
+++ b/src/pkg/runtime/parfor.c
@@ -46,6 +46,7 @@ void
runtime·parforsetup(ParFor *desc, uint32 nthr, uint32 n, void *ctx, bool wait, void (*body)(ParFor*, uint32))
{
uint32 i, begin, end;
+ uint64 *pos;
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);
@@ -67,7 +68,10 @@ runtime·parforsetup(ParFor *desc, uint32 nthr, uint32 n, void *ctx, bool wait,
for(i=0; i<nthr; i++) {
begin = (uint64)n*i / nthr;
end = (uint64)n*(i+1) / nthr;
-\t\tdesc->thr[i].pos = (uint64)begin | (((uint64)end)<<32);\n
+\t\tpos = &desc->thr[i].pos;\n
+\t\tif(((uintptr)pos & 7) != 0)\n
+\t\t\truntime·throw("parforsetup: pos is not aligned");\n
+\t\t*pos = (uint64)begin | (((uint64)end)<<32);\n
}\n
}\n
desc->thr[i].pos
への代入前に、そのアドレスが8バイト境界にアライメントされているかどうかのチェックが追加されました。uint64 *pos;
というローカルポインタ変数が導入され、desc->thr[i].pos
のアドレスを保持するために使用されています。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -483,6 +483,7 @@ struct ParFor
bool wait;\t\t\t// if true, wait while all threads finish processing,\n
\t\t\t\t\t// otherwise parfor may return while other threads are still working
ParForThread *thr;\t\t// array of thread descriptors
+\tuint32 pad;\t\t\t// to align ParForThread.pos for 64-bit atomic operations
// stats
uint64 nsteal;
uint64 nstealcnt;
ParFor
構造体にuint32 pad;
というパディングフィールドが追加されました。これは、ParForThread.pos
フィールドが64ビットアトミック操作のために適切にアライメントされることを保証するためのものです。
コアとなるコードの解説
src/pkg/runtime/mgc0.c
の変更
runtime·gc
関数はGoのガベージコレクションの主要なエントリポイントの一つです。この関数内で、GCのワークバッファの状態を管理するwork.empty
とwork.full
という変数が使用されています。これらは、おそらくGCが処理すべきオブジェクトのキューの空き容量と使用済み容量を示す64ビットのカウンタです。
追加されたコード:
if((((uintptr)&work.full) & 7) != 0)
runtime·throw("runtime: gc work buffer is misaligned");
この行は、work.full
変数のメモリアドレスが8バイト境界にアライメントされているかをチェックしています。
&work.full
:work.full
変数のメモリアドレスを取得します。(uintptr)
: アドレスを整数型(ポインタを保持できる十分な大きさの整数型)にキャストします。これにより、アドレスに対してビット演算を行うことができます。& 7
: アドレスを7(バイナリで0b111
)とビットAND演算します。もしアドレスが8の倍数であれば、下位3ビットはすべて0になります。したがって、この演算の結果は0になります。もしアドレスが8の倍数でなければ、結果は0以外の値になります。!= 0
: 結果が0以外の場合、つまりアドレスが8バイト境界にアライメントされていない場合に条件が真となります。runtime·throw("runtime: gc work buffer is misaligned");
: アライメント違反が検出された場合、Goランタイムはパニックを発生させ、プログラムを終了させます。これは、開発者が問題を早期に発見し、修正するためのデバッグ支援メカニズムです。
同様のチェックが既存のwork.empty
に対しても行われており、このコミットでwork.full
にも適用されたことで、GCワークバッファの整合性がより厳密に保証されるようになりました。
src/pkg/runtime/parfor.c
の変更
runtime·parforsetup
関数は、並列処理フレームワークParFor
のセットアップを行います。この関数は、各並列スレッドに処理すべきデータの範囲を割り当てます。
変更前:
desc->thr[i].pos = (uint64)begin | (((uint64)end)<<32);
この行では、begin
とend
という2つの32ビット値を結合して1つの64ビット値を作成し、それをdesc->thr[i].pos
に直接代入していました。pos
フィールドは、各スレッドが処理するデータの開始と終了インデックスを効率的に格納するためのものです。
変更後:
uint64 *pos; // 新しく追加されたポインタ変数
// ...
for(i=0; i<nthr; i++) {
// ...
pos = &desc->thr[i].pos; // posフィールドのアドレスを取得
if(((uintptr)pos & 7) != 0) // アライメントチェック
runtime·throw("parforsetup: pos is not aligned");
*pos = (uint64)begin | (((uint64)end)<<32); // ポインタ経由で値の代入
}
この変更では、desc->thr[i].pos
フィールドへの代入を行う前に、そのアドレスが8バイト境界にアライメントされているかをチェックするようになりました。
uint64 *pos;
:uint64
型へのポインタpos
を宣言します。pos = &desc->thr[i].pos;
:desc->thr[i].pos
のアドレスをpos
ポインタに格納します。if(((uintptr)pos & 7) != 0)
:mgc0.c
と同様に、pos
が指すアドレスが8バイト境界にアライメントされているかをチェックします。アライメントされていない場合はパニックを発生させます。*pos = ...;
: ポインタpos
を介して、begin
とend
を結合した64ビット値をdesc->thr[i].pos
に書き込みます。
この変更により、ParFor
フレームワークが64ビットアトミック操作を安全に実行できることが保証されます。
src/pkg/runtime/runtime.h
の変更
runtime.h
はGoランタイムの内部で使用される構造体や定数の定義を含むヘッダーファイルです。
変更前:
struct ParFor
{
// ...
ParForThread *thr; // array of thread descriptors
// stats
uint64 nsteal;
uint64 nstealcnt;
// ...
};
変更後:
struct ParFor
{
// ...
ParForThread *thr; // array of thread descriptors
uint32 pad; // to align ParForThread.pos for 64-bit atomic operations
// stats
uint64 nsteal;
uint64 nstealcnt;
// ...
};
ParFor
構造体にuint32 pad;
というフィールドが追加されました。
uint32 pad;
: 32ビット(4バイト)のパディングフィールドです。// to align ParForThread.pos for 64-bit atomic operations
: コメントが示すように、このパディングは、ParForThread
構造体内のpos
フィールドが64ビットアトミック操作のために適切に8バイト境界にアライメントされることを保証するために追加されました。
構造体のメンバのメモリ配置は、コンパイラやアーキテクチャによって異なる場合がありますが、明示的なパディングを追加することで、特定のフィールドのアライメント要件を強制し、予期せぬアライメント違反を防ぐことができます。このパディングがParForThread *thr;
の直後に追加されたことで、thr
が指すParForThread
の配列の各要素が、その内部のpos
フィールドが8バイト境界に配置されるように、構造体全体のサイズとアライメントが調整されることになります。
これらの変更は、Goランタイムの低レベルなメモリ管理と並行処理の正確性を向上させるための重要な修正です。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Goのランタイムソースコード: https://github.com/golang/go/tree/master/src/runtime
参考にした情報源リンク
- Goの公開Issueトラッカー (Web検索で関連Issueが見つからなかったため、内部Issueの可能性が高い): https://github.com/golang/go/issues
- メモリのアライメントに関する一般的な情報 (例: Wikipedia, 各種プログラミング言語のドキュメント)
- アトミック操作に関する一般的な情報 (例: Wikipedia, 並行プログラミングの教科書)
- Goのコードレビューシステム (Gerrit): https://go.dev/cl/ (コミットメッセージに記載されている
https://golang.org/cl/7534044
は、このGerritの変更リストへのリンクです。)