[インデックス 17225] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージにおけるARM Linuxアーキテクチャ向けのswap
操作のバグ修正に関するものです。具体的には、src/pkg/sync/atomic/64bit_arm.go
とsrc/pkg/sync/atomic/asm_linux_arm.s
の2つのファイルが変更されています。これらのファイルは、ARMプロセッサ上で64ビットのアトミック操作を効率的かつ正確に実行するためのGoランタイムの低レベルな実装を扱っています。
コミット
commit 883530c019f06d557f82707d35f7ee363ff12637
Author: Russ Cox <rsc@golang.org>
Date: Wed Aug 14 00:50:47 2013 -0400
sync/atomic: fix new swap on arm linux
TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12920043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/883530c019f06d557f82707d35f7ee363ff12637
元コミット内容
sync/atomic: fix new swap on arm linux
TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12920043
変更の背景
このコミットは、Go言語のsync/atomic
パッケージにおけるARM Linux環境でのswap
操作に存在するバグを修正するために行われました。sync/atomic
パッケージは、複数のゴルーチン(Goの軽量スレッド)が共有データに同時にアクセスする際に、データ競合を防ぎ、正しく同期された操作を保証するためのプリミティブを提供します。
特に、swap
(交換)操作は、メモリ位置の値を新しい値とアトミックに交換し、元の値を返すという重要な機能です。このコミットメッセージにある「new swap」という記述から、この修正が行われる少し前に、ARM Linux向けのアトミックなswap
操作の実装が新しく導入されたか、あるいは既存の実装が変更されたことが示唆されます。その新しい実装に、特定の条件下で誤動作を引き起こすバグが含まれていたため、この修正が必要となりました。
アトミック操作は、並行プログラミングにおいて非常に重要であり、その正確性はシステムの安定性とデータの整合性に直結します。そのため、このようなバグは速やかに特定され、修正される必要がありました。
前提知識の解説
Go言語のsync/atomic
パッケージ
sync/atomic
パッケージは、Go言語で並行処理を行う際に、共有変数へのアトミックな操作(不可分な操作)を提供する標準ライブラリです。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、その操作が中断されることなく、単一の不可分なステップとして完了することを保証します。これにより、データ競合(data race)を防ぎ、プログラムの正確性を保ちます。
このパッケージは、主に以下の種類のアトミック操作を提供します。
- Add: 指定された値にアトミックに加算する。
- Load: 指定されたメモリ位置からアトミックに値を読み込む。
- Store: 指定されたメモリ位置にアトミックに値を書き込む。
- Swap: 指定されたメモリ位置の値を新しい値とアトミックに交換し、元の値を返す。
- CompareAndSwap (CAS): 指定されたメモリ位置の値が期待する値と一致する場合にのみ、新しい値にアトミックに交換する。成功した場合は
true
、失敗した場合はfalse
を返す。
これらの操作は、ミューテックス(sync.Mutex
)のようなロック機構を使用するよりも、特定のシナリオではより効率的です。
アトミック操作(特にSwap
とCompareAndSwap
)
-
Swap (交換):
Swap(addr, new)
は、addr
が指すメモリ位置の現在の値をnew
に設定し、そのメモリ位置にあった元の値を返します。この操作全体がアトミックに行われるため、他のゴルーチンが同時にaddr
にアクセスしても、中途半端な状態が観測されることはありません。 -
CompareAndSwap (CAS) (比較と交換):
CompareAndSwap(addr, old, new)
は、addr
が指すメモリ位置の現在の値がold
(期待する値)と等しい場合にのみ、その値をnew
に設定します。この操作もアトミックに行われます。値が交換された場合はtrue
を返し、そうでない場合はfalse
を返します。CASは、ロックフリーなデータ構造やアルゴリズムを実装する際の基本的な構成要素です。多くの複雑なアトミック操作は、CASをループ内で繰り返し試行することで実装されます。
ARMアーキテクチャにおけるアトミック操作の特性
ARMアーキテクチャでは、アトミック操作を実装するために、通常、ロード・エクスクルーシブ(LDREX
)とストア・エクスクルーシブ(STREX
)という命令のペアを使用します。
- LDREX (Load-Exclusive): 指定されたメモリ位置から値を読み込み、そのメモリ位置に対する「排他的アクセス」をマークします。
- STREX (Store-Exclusive): 指定されたメモリ位置に値を書き込もうとします。この書き込みは、対応する
LDREX
命令以降、他のプロセッサやコアが同じメモリ位置に書き込みを行っていない場合にのみ成功します。成功した場合は特定のレジスタに0を、失敗した場合は非ゼロの値を返します。
これらの命令を組み合わせて、CASのようなアトミック操作を実装します。例えば、CASはLDREX
で値を読み込み、期待する値と比較し、一致すればSTREX
で新しい値を書き込もうとします。STREX
が失敗した場合は、操作全体を再試行します。
Goのアセンブリ
Go言語は、パフォーマンスが重要な部分や、特定のハードウェア機能にアクセスする必要がある場合に、Goアセンブリ言語(Plan 9アセンブリに似た構文を持つ)を使用してコードを記述することをサポートしています。src/pkg/sync/atomic/asm_linux_arm.s
のようなファイルは、Goのランタイムや標準ライブラリの低レベルな部分で、Goのコードから直接呼び出されるアトミック操作などのプリミティブを実装するために使用されます。
Goアセンブリでは、レジスタの利用、メモリへのアクセス、関数呼び出しなどが直接記述されます。このコミットでは、ARMプロセッサのレジスタ(R0, R1, R2, R4など)が直接操作されており、アトミック操作の内部的な挙動を理解するためには、これらのレジスタの役割と命令のセマンティクスを理解する必要があります。
技術的詳細
このコミットの技術的な核心は、ARM Linux環境におけるswapUint64
関数の実装、特にその内部でCompareAndSwapUint64
(CAS)を使用する際のレジスタの取り扱いに関するバグの修正です。
swapUint64
は、addr
が指すuint64
の値をnew
にアトミックに交換し、元の値をold
として返すことを目的としています。一般的なSwap
操作は、CASをループ内で使用して実装されることが多いです。つまり、「現在の値を読み込み、その値がまだ変わっていなければ新しい値に交換する」という試行を、成功するまで繰り返します。
問題は、src/pkg/sync/atomic/asm_linux_arm.s
内のアセンブリコード、特にcas
(CompareAndSwap)関数の呼び出しとレジスタの使用方法にありました。元のコードでは、swaploop1
内でMOVW 0(R2), R0
によってメモリから値を読み込み、その値をR0
レジスタに格納していました。その後、BL cas<>(SB)
によってcas
関数が呼び出されます。
ここで重要なのは、ARMのABI(Application Binary Interface)やGoのアセンブリ規約において、関数呼び出し(BL
命令)によってR0
レジスタの内容が変更される可能性があるという点です。特に、cas
関数がその結果や一時的な値をR0
に書き込む場合、cas
呼び出し前のR0
に格納されていた「古い値」(old
)が上書きされてしまいます。
元のコードでは、cas
呼び出し後にMOVW R0, old+8(FP)
として、cas
呼び出し後のR0
の値をold
として返そうとしていました。しかし、cas
がR0
を「破壊」する(cas smashes R0
というコメントが示唆するように)ため、このR0
には期待する「古い値」ではなく、cas
の戻り値や副作用による値が入ってしまっていました。これにより、swapUint64
が正しく元の値を返さないというバグが発生していました。
修正は、cas
呼び出しによってR0
が破壊される前に、R0
の値を別のレジスタ(R4
)に退避させることで行われました。これにより、cas
呼び出し後も、退避しておいた正しい「古い値」をold
として返すことができるようになりました。
コアとなるコードの変更箇所
src/pkg/sync/atomic/64bit_arm.go
--- a/src/pkg/sync/atomic/64bit_arm.go
+++ b/src/pkg/sync/atomic/64bit_arm.go
@@ -37,7 +37,7 @@ func addUint64(val *uint64, delta uint64) (new uint64) {
func swapUint64(addr *uint64, new uint64) (old uint64) {
for {
- old := *addr
+ old = *addr
if CompareAndSwapUint64(addr, old, new) {
break
}
src/pkg/sync/atomic/asm_linux_arm.s
--- a/src/pkg/sync/atomic/asm_linux_arm.s
+++ b/src/pkg/sync/atomic/asm_linux_arm.s
@@ -88,9 +88,10 @@ TEXT ·SwapUint32(SB),NOSPLIT,$0-12
MOVW new+4(FP), R1
swaploop1:
MOVW 0(R2), R0
+ MOVW R0, R4 // cas smashes R0
BL cas<>(SB)
BCC swaploop1
- MOVW R0, old+8(FP)
+ MOVW R4, old+8(FP)
RET
TEXT ·SwapUintptr(SB),NOSPLIT,$0
コアとなるコードの解説
src/pkg/sync/atomic/64bit_arm.go
の変更
このファイルでは、swapUint64
関数の内部でold
変数の宣言方法が変更されています。
- 変更前:
old := *addr
- 変更後:
old = *addr
この変更は、old
変数がループの各イテレーションで再宣言されるのではなく、関数のスコープで一度宣言され、ループ内で値が再代入されるようにしています。Go言語では、:=
は短い変数宣言演算子であり、変数を宣言し、初期値を代入します。=
は代入演算子です。この変更自体は、直接的なバグ修正というよりは、コードのスタイルや、コンパイラが生成するアセンブリコードのわずかな最適化に関連している可能性があります。しかし、本質的なバグはアセンブリコード側にありました。
src/pkg/sync/atomic/asm_linux_arm.s
の変更
このファイルは、ARM Linux向けのアトミック操作の低レベルなアセンブリ実装を含んでいます。特にTEXT ·SwapUint32(SB)
セクション(これはSwapUint64
の内部で呼び出される可能性のあるcas
の実装に関連しているか、あるいは同様のロジックがSwapUint64
のアセンブリにも適用されることを示唆しています)に重要な変更があります。
-
変更前:
swaploop1: MOVW 0(R2), R0 BL cas<>(SB) BCC swaploop1 MOVW R0, old+8(FP)
-
変更後:
swaploop1: MOVW 0(R2), R0 MOVW R0, R4 // cas smashes R0 BL cas<>(SB) BCC swaploop1 MOVW R4, old+8(FP)
この変更の核心は以下の2行です。
-
MOVW R0, R4 // cas smashes R0
MOVW 0(R2), R0
でメモリから読み込んだ値(交換前の値、つまり返すべきold
の値)がR0
レジスタに格納されます。- その直後に、この
R0
の値をR4
レジスタにコピーしています。 - コメント
// cas smashes R0
が非常に重要です。これは、次に呼び出されるcas
関数(CompareAndSwap
の実装)が、R0
レジスタの内容を破壊する(上書きする)ことを示しています。cas
関数は、その結果(成功/失敗)やその他の内部的な目的のためにR0
を使用する可能性があります。この行は、cas
呼び出しによってR0
が上書きされる前に、元の値をR4
に「退避」させています。
-
MOVW R4, old+8(FP)
BL cas<>(SB)
でcas
関数が呼び出され、アトミックな比較と交換が試行されます。BCC swaploop1
は、cas
が失敗した場合(キャリーフラグがクリアされている場合)にループを再試行します。cas
が成功し、ループを抜けた後、old
として返す値は、cas
呼び出しによって破壊されたR0
ではなく、事前にR4
に退避しておいた正しい元の値です。この行は、R4
に保存されていた値を、関数の戻り値であるold
(スタックフレーム上のold+8(FP)
の位置)に格納しています。
このアセンブリレベルの修正により、swapUint64
関数が、アトミックな交換操作の成功後に、正しく交換前の値を返すことが保証されるようになりました。これは、並行プログラミングにおけるアトミック操作の正確性を維持するために不可欠な修正です。
関連リンク
- Go CL (Code Review) 12920043: https://golang.org/cl/12920043
参考にした情報源リンク
- Go言語の
sync/atomic
パッケージのドキュメント: https://pkg.go.dev/sync/atomic - ARMアーキテクチャのリファレンスマニュアル(LDREX/STREX命令に関する情報)
- Goアセンブリに関するドキュメントやチュートリアル(例: Goの公式ドキュメントの「Go Assembly Language」セクション)
- Go言語のソースコード(特に
src/runtime
やsrc/sync/atomic
ディレクトリ) - Go言語のIssueトラッカーやメーリングリスト(関連するバグ報告や議論がある場合)