[インデックス 13036] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージにおけるLinux/ARMアーキテクチャ向けの64ビットアトミック比較交換(Compare-And-Swap, CAS)操作の修正に関するものです。特に、異なるARMバージョン(ARMv5、ARMv6以降)およびLinuxカーネルの機能(__kuser_cmpxchg64
ヘルパー)に応じて、最適なCAS実装を選択するロジックが導入されています。
コミット
commit 4d724f4c5b31d13f55017266db6e6cc8bd08f541
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat May 5 02:02:36 2012 +0800
sync/atomic: fix 64-bit atomic cas for Linux/ARM
This is a follow-up to CL 5978051.
Use kernel cas64 helper if we can, fallback to LDREXD/STREXD if
we are on ARMv6 or higher, and to lock-emulated cas64 if on ARMv5.
A future CL will fix {Add,Load,Store}{Int,Uint}64 and issue 3331.
R=bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6034048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4d724f4c5b31d13f55017266db6e6cc8bd08f541
元コミット内容
sync/atomic: fix 64-bit atomic cas for Linux/ARM
これはCL 5978051のフォローアップです。
可能であればカーネルのcas64ヘルパーを使用し、ARMv6以降であればLDREXD/STREXDにフォールバックし、ARMv5であればロックエミュレートされたcas64にフォールバックします。
将来のCLで{Add,Load,Store}{Int,Uint}64とissue 3331を修正する予定です。
変更の背景
このコミットの背景には、ARMアーキテクチャにおける64ビットアトミック操作の複雑さがあります。特に、異なるARMバージョン(ARMv5、ARMv6以降)では、64ビット値をアトミックに操作するための命令セットが異なります。
- ARMv5: 64ビットのアトミックCAS命令をネイティブにサポートしていません。そのため、ソフトウェアによるエミュレーション(通常はミューテックスやスピンロックを用いたロックベースの実装)が必要になります。これはパフォーマンスのボトルネックとなる可能性があります。
- ARMv6以降:
LDREXD
(Load-Exclusive Doubleword)とSTREXD
(Store-Exclusive Doubleword)という排他ロード/ストア命令が導入されました。これらは、2つのレジスタ(64ビット値)をアトミックにロードし、条件付きでストアすることを可能にし、ハードウェアレベルでのアトミックCAS操作を効率的に実装できます。 - Linuxカーネルのヘルパー: Linuxカーネルは、ユーザー空間アプリケーションが利用できるヘルパー関数を提供することがあります。この場合、
__kuser_cmpxchg64
というヘルパー関数が、カーネルが提供する64ビットCAS操作を利用するためのメカニズムとして存在します。これは、カーネルがより最適化された、または特権的な方法でアトミック操作を実行できる場合に特に有用です。
以前の実装では、ARM11以降のデバイス(ARMv6K以降に相当)でのみネイティブARM命令を使用するとされていましたが、これはカーネルヘルパーの利用や、より広範なARMv6以降のサポートを考慮していませんでした。このコミットは、これらの異なる実行環境と利用可能な機能を考慮し、最も効率的で堅牢な64ビットCAS実装を動的に選択するためのロジックを導入することで、パフォーマンスと互換性を向上させることを目的としています。
また、コミットメッセージには「CL 5978051のフォローアップ」とあり、これは以前の関連する変更があったことを示唆しています。さらに、「将来のCLで{Add,Load,Store}{Int,Uint}64とissue 3331を修正する予定」とあることから、このコミットは64ビットアトミック操作全般の改善に向けた一連の作業の一部であることがわかります。
前提知識の解説
- アトミック操作(Atomic Operations): マルチスレッド環境において、複数のスレッドから同時にアクセスされる共有データに対して、その操作が不可分(atomic)であることを保証するものです。つまり、あるスレッドがアトミック操作を実行している間、他のスレッドはその操作の途中の状態を観測したり、割り込んだりすることができません。これにより、データ競合(data race)を防ぎ、プログラムの正確性を保証します。
- 比較交換(Compare-And-Swap, CAS): アトミック操作の一種で、共有メモリ上の特定のアドレスにある値が期待する値と一致する場合にのみ、その値を新しい値に更新する操作です。これは多くのロックフリー(lock-free)アルゴリズムの基礎となります。CASは通常、
CAS(address, expected_value, new_value)
のような形式で表現され、操作が成功したかどうかを示すブール値を返します。 - ARMアーキテクチャ: Advanced RISC Machineの略で、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。ARMアーキテクチャには多くのバージョンがあり、それぞれ異なる命令セットや機能拡張を持っています。
- ARMv5: 古いARMアーキテクチャのバージョン。64ビットのアトミック操作を直接サポートする命令がありません。
- ARMv6/ARMv6K: ARMv5に比べて多くの改善が加えられ、特に
LDREX
(Load-Exclusive)とSTREX
(Store-Exclusive)命令が導入されました。これらは、排他モニタと呼ばれるハードウェア機構と連携して、アトミックな読み書きやCAS操作を実装するために使用されます。LDREXD
/STREXD
は64ビット版です。
- Linuxカーネルの
__kuser_cmpxchg64
ヘルパー: Linuxカーネルは、ユーザー空間のプログラムが特定のシステムコールを介さずに、カーネルが提供する最適化されたルーチンを直接呼び出せるように、__kuser_helper_version
というメカニズムを提供しています。__kuser_cmpxchg64
は、このメカニズムを通じて利用できる64ビットCAS操作のヘルパー関数です。これは、カーネルがより効率的な方法でCASを実行できる場合や、特定のハードウェア機能を利用できる場合に有用です。このヘルパーは、Linuxカーネルバージョン3.1以降で利用可能です。 - Go言語の
sync/atomic
パッケージ: Go言語の標準ライブラリの一部で、ミューテックスなどのロック機構を使用せずに、共有変数に対するアトミックな操作(読み込み、書き込み、加算、比較交換など)を提供するパッケージです。これにより、データ競合を避けつつ、高い並行性を実現できます。
技術的詳細
このコミットは、src/pkg/sync/atomic/asm_linux_arm.s
というARMアセンブリ言語で書かれたファイルに変更を加えています。このファイルは、Go言語のsync/atomic
パッケージがLinux/ARM環境で64ビットアトミック操作を実行するための低レベルな実装を提供しています。
変更の核心は、CompareAndSwapInt64
(およびそれを通じて呼び出されるCompareAndSwapUint64
)関数が、実行時のARMアーキテクチャのバージョンとLinuxカーネルの機能に応じて、最適な64ビットCAS実装を動的に選択するようになった点です。
具体的な選択ロジックは以下の通りです。
-
カーネルヘルパーの利用(最優先):
setupAndCallCAS64<>
という新しいアセンブリ関数が導入され、まず__kuser_helper_version
をチェックします。__kuser_helper_version
が5以上の場合(Linuxカーネル3.1以降で__kuser_cmpxchg64
が利用可能)、カーネルが提供する__kuser_cmpxchg64
ヘルパー関数(cas64<>
ラベルで参照)を直接呼び出します。kernelCAS64<>
というラッパー関数が、Goの呼び出し規約からカーネルヘルパーの呼び出し規約に引数を変換し、結果をGoの期待する形式に変換します。- このパスが利用可能な場合、
armCAS64
というグローバル変数にkernelCAS64<>
のアドレスが設定され、以降のCAS呼び出しはこの最適化されたパスを使用します。
-
LDREXD/STREXDの利用(ARMv6以降):
- カーネルヘルパーが利用できない場合、次に
runtime·armArch
(Goランタイムが検出したARMアーキテクチャバージョン)をチェックします。 runtime·armArch
が6以上(ARMv6K以降)の場合、LDREXD
とSTREXD
命令を使用したネイティブなアトミックCAS実装(既存の·armCompareAndSwapUint64
関数)にフォールバックします。- このパスが利用可能な場合、
armCAS64
に·armCompareAndSwapUint64
のアドレスが設定されます。
- カーネルヘルパーが利用できない場合、次に
-
ソフトウェアエミュレーション(ARMv5):
- 上記いずれのパスも利用できない場合(主にARMv5のような古いARMアーキテクチャ)、Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS実装(
runtime·cas64
関数、generalCAS64<>
ラッパーを通じて呼び出される)にフォールバックします。 - このパスが利用される場合、
armCAS64
にgeneralCAS64<>
のアドレスが設定されます。
- 上記いずれのパスも利用できない場合(主にARMv5のような古いARMアーキテクチャ)、Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS実装(
armCAS64
というグローバル変数は、一度最適なCAS実装が決定されると、そのアドレスをキャッシュするために使用されます。これにより、以降のCompareAndSwapInt64
の呼び出しでは、毎回アーキテクチャチェックを行うことなく、直接キャッシュされた関数を呼び出すことができます。
この動的な選択ロジックにより、Goプログラムは実行されるARMデバイスとLinuxカーネルのバージョンに応じて、常に最も効率的な64ビットアトミックCAS操作を利用できるようになります。
コアとなるコードの変更箇所
変更は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
@@ -72,14 +72,63 @@ addloop1:
TEXT ·AddUintptr(SB),7,$0
B ·AddUint32(SB)
-// The kernel provides no 64-bit compare-and-swap,
-// so use native ARM instructions, which will only work on
-// ARM 11 and later devices.
-TEXT ·CompareAndSwapInt64(SB),7,$0
- B ·armCompareAndSwapUint64(SB)
+TEXT cas64<>(SB),7,$0
+ MOVW $0xffff0f60, PC // __kuser_cmpxchg64: Linux-3.1 and above
+
+TEXT kernelCAS64<>(SB),7,$0
+ // int (*__kuser_cmpxchg64_t)(const int64_t *oldval, const int64_t *newval, volatile int64_t *ptr);
+ MOVW valptr+0(FP), R2 // ptr
+ MOVW $4(FP), R0 // oldval
+ MOVW $12(FP), R1 // newval
+ BL cas64<>(SB)
+ MOVW.CS $1, R0 // C is set if the kernel has changed *ptr
+ MOVW.CC $0, R0
+ MOVW R0, 20(FP)
+ RET
+
+TEXT generalCAS64<>(SB),7,$20
+ // bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
+ MOVW valptr+0(FP), R0
+ MOVW R0, 4(R13)
+ MOVW $4(FP), R1 // oldval
+ MOVW R1, 8(R13)
+ MOVW newlo+12(FP), R2
+ MOVW R2, 12(R13)
+ MOVW newhi+16(FP), R3
+ MOVW R3, 16(R13)
+ BL runtime·cas64(SB)
+ MOVW R0, 20(FP)
+ RET
+
+GLOBL armCAS64(SB), $4
+
+TEXT setupAndCallCAS64<>(SB),7,$-4
+ MOVW $0xffff0ffc, R0 // __kuser_helper_version
+ MOVW (R0), R0
+ // __kuser_cmpxchg64 only present if helper version >= 5
+ CMP $5, R0
+ MOVW.CS $kernelCAS64<>(SB), R1
+ MOVW.CS R1, armCAS64(SB)
+ MOVW.CS R1, PC
+ MOVB runtime·armArch(SB), R0
+ // LDREXD, STREXD only present on ARMv6K or higher
+ CMP $6, R0 // TODO(minux): how to differentiate ARMv6 with ARMv6K?
+ MOVW.CS $·armCompareAndSwapUint64(SB), R1
+ MOVW.CS R1, armCAS64(SB)
+ MOVW.CS R1, PC
+ // we are out of luck, can only use runtime's emulated 64-bit cas
+ MOVW $generalCAS64<>(SB), R1
+ MOVW R1, armCAS64(SB)
+ MOVW R1, PC
+
+TEXT ·CompareAndSwapInt64(SB),7,$-4
+ MOVW armCAS64(SB), R0
+ CMP $0, R0
+ MOVW.NE R0, PC
+ B setupAndCallCAS64<>(SB)
TEXT ·CompareAndSwapUint64(SB),7,$0
- B ·armCompareAndSwapUint64(SB)
+ B ·CompareAndSwapInt64(SB)
TEXT ·AddInt64(SB),7,$0
B ·armAddUint64(SB)
コアとなるコードの解説
-
cas64<>(SB)
:- これは、Linuxカーネルが提供する
__kuser_cmpxchg64
ヘルパー関数へのジャンプテーブルエントリです。 MOVW $0xffff0f60, PC
は、__kuser_cmpxchg64
ヘルパー関数の固定アドレス(0xffff0f60
)にプログラムカウンタ(PC)を直接設定することで、その関数を呼び出します。これは、ユーザー空間からカーネルヘルパーを効率的に呼び出すための一般的な手法です。
- これは、Linuxカーネルが提供する
-
kernelCAS64<>(SB)
:- Goの
CompareAndSwapInt64
関数からカーネルヘルパーを呼び出すためのラッパー関数です。 - Goの関数呼び出し規約(引数がスタックフレームのFPレジスタからのオフセットで渡される)から、カーネルヘルパーの期待する引数(レジスタR0, R1, R2)に変換します。
BL cas64<>(SB)
でカーネルヘルパーを呼び出します。- カーネルヘルパーの戻り値(CPSRレジスタのCフラグ)をチェックし、Goの期待するブール値(成功なら1、失敗なら0)に変換して返します。
- Goの
-
generalCAS64<>(SB)
:- Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS関数
runtime·cas64
を呼び出すためのラッパーです。 - これもGoの呼び出し規約から
runtime·cas64
の期待する引数に変換し、結果を返します。
- Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS関数
-
GLOBL armCAS64(SB), $4
:armCAS64
というグローバル変数を宣言しています。この変数は、最適な64ビットCAS実装のアドレスをキャッシュするために使用されます。$4
は変数のサイズ(4バイト、ARMのポインタサイズ)を示します。
-
setupAndCallCAS64<>(SB)
:- この関数は、最初に呼び出されたときに、どの64ビットCAS実装を使用すべきかを決定し、そのアドレスを
armCAS64
に格納します。 - カーネルヘルパーのチェック:
MOVW $0xffff0ffc, R0
で__kuser_helper_version
のアドレスをロードし、その値を読み取ります。CMP $5, R0
でバージョンが5以上かチェックします。MOVW.CS $kernelCAS64<>(SB), R1
とMOVW.CS R1, armCAS64(SB)
は、条件付き実行(Cフラグがセットされている場合、つまりバージョンが5以上の場合)でkernelCAS64<>
のアドレスをarmCAS64
に格納します。MOVW.CS R1, PC
は、条件付きでkernelCAS64<>
にジャンプし、最初のCAS操作を実行します。
- LDREXD/STREXDのチェック:
- カーネルヘルパーが利用できない場合、
MOVB runtime·armArch(SB), R0
でGoランタイムが検出したARMアーキテクチャバージョンを読み取ります。 CMP $6, R0
でバージョンが6以上かチェックします。- 同様に、条件付き実行で
·armCompareAndSwapUint64
のアドレスをarmCAS64
に格納し、ジャンプします。
- カーネルヘルパーが利用できない場合、
- ソフトウェアエミュレーションへのフォールバック:
- 上記いずれの条件も満たさない場合、
generalCAS64<>
のアドレスをarmCAS64
に格納し、ジャンプします。
- 上記いずれの条件も満たさない場合、
- この関数は、最初に呼び出されたときに、どの64ビットCAS実装を使用すべきかを決定し、そのアドレスを
-
·CompareAndSwapInt64(SB)
:- Goの
sync/atomic.CompareAndSwapInt64
関数に対応するエントリポイントです。 MOVW armCAS64(SB), R0
でarmCAS64
にキャッシュされたCAS実装のアドレスを読み取ります。CMP $0, R0
でアドレスが0(初期値)かどうかをチェックします。MOVW.NE R0, PC
は、アドレスが0でない場合(つまり、すでに最適な実装が決定されている場合)に、そのアドレスに直接ジャンプしてCAS操作を実行します。B setupAndCallCAS64<>(SB)
は、アドレスが0の場合(最初の呼び出し時)にsetupAndCallCAS64<>
を呼び出して、最適な実装を決定させます。
- Goの
-
·CompareAndSwapUint64(SB)
:CompareAndSwapInt64
と同様に、CompareAndSwapUint64
もCompareAndSwapInt64
にジャンプするように変更されています。これにより、両方の関数が同じ動的な選択ロジックと実装を共有するようになります。
この変更により、GoはARM環境で64ビットアトミックCAS操作を実行する際に、利用可能な最も効率的なハードウェア機能(カーネルヘルパー、LDREXD/STREXD)を優先的に利用し、それが不可能な場合にのみソフトウェアエミュレーションにフォールバックする、堅牢でパフォーマンスの高いメカニズムを実現しています。
関連リンク
- Go言語の
sync/atomic
パッケージ: https://pkg.go.dev/sync/atomic - ARMアーキテクチャの排他ロード/ストア命令(LDREX/STREX)に関する情報:
- ARMv6 Architecture Reference Manual (LDREX/STREX命令のセクション)
- Stack Overflow: https://stackoverflow.com/questions/11227809/why-is-accessing-a-global-variable-atomic-in-c-c (アトミック操作全般の解説)
- Linuxカーネルの
__kuser_helper_version
と__kuser_cmpxchg64
に関する情報:- Linuxカーネルのソースコード(
arch/arm/kernel/entry-armv.S
やarch/arm/include/asm/kuser.h
など) - 関連するメーリングリストの議論やパッチ
- LWN.net: https://lwn.net/Articles/460620/ (ARMv6/v7のユーザー空間ヘルパーに関する記事)
- Linuxカーネルのソースコード(
参考にした情報源リンク
- Go言語のコミット履歴と関連するCL(Change List):
- https://golang.org/cl/6034048 (このコミットのCL)
- https://golang.org/cl/5978051 (コミットメッセージで言及されている先行CL)
- Go言語のIssueトラッカー:
- https://golang.org/issue/3331 (コミットメッセージで言及されているIssue)
- ARMアーキテクチャリファレンスマニュアル(ARMv5, ARMv6Kなど)
- Linuxカーネルのドキュメントとソースコード
- Go言語の
src/pkg/sync/atomic/asm_linux_arm.s
ファイルの変更前後の内容 - アセンブリ言語とARMアーキテクチャに関する一般的な知識
- アトミック操作と並行プログラミングに関する一般的な知識
- Stack Overflowや技術ブログなどのオンラインリソース# [インデックス 13036] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージにおけるLinux/ARMアーキテクチャ向けの64ビットアトミック比較交換(Compare-And-Swap, CAS)操作の修正に関するものです。特に、異なるARMバージョン(ARMv5、ARMv6以降)およびLinuxカーネルの機能(__kuser_cmpxchg64
ヘルパー)に応じて、最適なCAS実装を選択するロジックが導入されています。
コミット
commit 4d724f4c5b31d13f55017266db6e6cc8bd08f541
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat May 5 02:02:36 2012 +0800
sync/atomic: fix 64-bit atomic cas for Linux/ARM
This is a follow-up to CL 5978051.
Use kernel cas64 helper if we can, fallback to LDREXD/STREXD if
we are on ARMv6 or higher, and to lock-emulated cas64 if on ARMv5.
A future CL will fix {Add,Load,Store}{Int,Uint}64 and issue 3331.
R=bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6034048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4d724f4c5b31d13f55017266db6e6cc8bd08f541
元コミット内容
sync/atomic: fix 64-bit atomic cas for Linux/ARM
これはCL 5978051のフォローアップです。
可能であればカーネルのcas64ヘルパーを使用し、ARMv6以降であればLDREXD/STREXDにフォールバックし、ARMv5であればロックエミュレートされたcas64にフォールバックします。
将来のCLで{Add,Load,Store}{Int,Uint}64とissue 3331を修正する予定です。
変更の背景
このコミットの背景には、ARMアーキテクチャにおける64ビットアトミック操作の複雑さがあります。特に、異なるARMバージョン(ARMv5、ARMv6以降)では、64ビット値をアトミックに操作するための命令セットが異なります。
- ARMv5: 64ビットのアトミックCAS命令をネイティブにサポートしていません。そのため、ソフトウェアによるエミュレーション(通常はミューテックスやスピンロックを用いたロックベースの実装)が必要になります。これはパフォーマンスのボトルネックとなる可能性があります。
- ARMv6以降:
LDREXD
(Load-Exclusive Doubleword)とSTREXD
(Store-Exclusive Doubleword)という排他ロード/ストア命令が導入されました。これらは、2つのレジスタ(64ビット値)をアトミックにロードし、条件付きでストアすることを可能にし、ハードウェアレベルでのアトミックCAS操作を効率的に実装できます。 - Linuxカーネルのヘルパー: Linuxカーネルは、ユーザー空間アプリケーションが利用できるヘルパー関数を提供することがあります。この場合、
__kuser_cmpxchg64
というヘルパー関数が、カーネルが提供する64ビットCAS操作を利用するためのメカニズムとして存在します。これは、カーネルがより最適化された、または特権的な方法でアトミック操作を実行できる場合に特に有用です。
以前の実装では、ARM11以降のデバイス(ARMv6K以降に相当)でのみネイティブARM命令を使用するとされていましたが、これはカーネルヘルパーの利用や、より広範なARMv6以降のサポートを考慮していませんでした。このコミットは、これらの異なる実行環境と利用可能な機能を考慮し、最も効率的で堅牢な64ビットCAS実装を動的に選択するためのロジックを導入することで、パフォーマンスと互換性を向上させることを目的としています。
また、コミットメッセージには「CL 5978051のフォローアップ」とあり、これは以前の関連する変更があったことを示唆しています。さらに、「将来のCLで{Add,Load,Store}{Int,Uint}64とissue 3331を修正する予定」とあることから、このコミットは64ビットアトミック操作全般の改善に向けた一連の作業の一部であることがわかります。
前提知識の解説
- アトミック操作(Atomic Operations): マルチスレッド環境において、複数のスレッドから同時にアクセスされる共有データに対して、その操作が不可分(atomic)であることを保証するものです。つまり、あるスレッドがアトミック操作を実行している間、他のスレッドはその操作の途中の状態を観測したり、割り込んだりすることができません。これにより、データ競合(data race)を防ぎ、プログラムの正確性を保証します。
- 比較交換(Compare-And-Swap, CAS): アトミック操作の一種で、共有メモリ上の特定のアドレスにある値が期待する値と一致する場合にのみ、その値を新しい値に更新する操作です。これは多くのロックフリー(lock-free)アルゴリズムの基礎となります。CASは通常、
CAS(address, expected_value, new_value)
のような形式で表現され、操作が成功したかどうかを示すブール値を返します。 - ARMアーキテクチャ: Advanced RISC Machineの略で、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。ARMアーキテクチャには多くのバージョンがあり、それぞれ異なる命令セットや機能拡張を持っています。
- ARMv5: 古いARMアーキテクチャのバージョン。64ビットのアトミック操作を直接サポートする命令がありません。
- ARMv6/ARMv6K: ARMv5に比べて多くの改善が加えられ、特に
LDREX
(Load-Exclusive)とSTREX
(Store-Exclusive)命令が導入されました。これらは、排他モニタと呼ばれるハードウェア機構と連携して、アトミックな読み書きやCAS操作を実装するために使用されます。LDREXD
/STREXD
は64ビット版です。
- Linuxカーネルの
__kuser_cmpxchg64
ヘルパー: Linuxカーネルは、ユーザー空間のプログラムが特定のシステムコールを介さずに、カーネルが提供する最適化されたルーチンを直接呼び出せるように、__kuser_helper_version
というメカニズムを提供しています。__kuser_cmpxchg64
は、このメカニズムを通じて利用できる64ビットCAS操作のヘルパー関数です。これは、カーネルがより効率的な方法でCASを実行できる場合や、特定のハードウェア機能を利用できる場合に有用です。このヘルパーは、Linuxカーネルバージョン3.1以降で利用可能です。 - Go言語の
sync/atomic
パッケージ: Go言語の標準ライブラリの一部で、ミューテックスなどのロック機構を使用せずに、共有変数に対するアトミックな操作(読み込み、書き込み、加算、比較交換など)を提供するパッケージです。これにより、データ競合を避けつつ、高い並行性を実現できます。
技術的詳細
このコミットは、src/pkg/sync/atomic/asm_linux_arm.s
というARMアセンブリ言語で書かれたファイルに変更を加えています。このファイルは、Go言語のsync/atomic
パッケージがLinux/ARM環境で64ビットアトミック操作を実行するための低レベルな実装を提供しています。
変更の核心は、CompareAndSwapInt64
(およびそれを通じて呼び出されるCompareAndSwapUint64
)関数が、実行時のARMアーキテクチャのバージョンとLinuxカーネルの機能に応じて、最適な64ビットCAS実装を動的に選択するようになった点です。
具体的な選択ロジックは以下の通りです。
-
カーネルヘルパーの利用(最優先):
setupAndCallCAS64<>
という新しいアセンブリ関数が導入され、まず__kuser_helper_version
をチェックします。__kuser_helper_version
が5以上の場合(Linuxカーネル3.1以降で__kuser_cmpxchg64
が利用可能)、カーネルが提供する__kuser_cmpxchg64
ヘルパー関数(cas64<>
ラベルで参照)を直接呼び出します。kernelCAS64<>
というラッパー関数が、Goの呼び出し規約からカーネルヘルパーの呼び出し規約に引数を変換し、結果をGoの期待する形式に変換します。- このパスが利用可能な場合、
armCAS64
というグローバル変数にkernelCAS64<>
のアドレスが設定され、以降のCAS呼び出しはこの最適化されたパスを使用します。
-
LDREXD/STREXDの利用(ARMv6以降):
- カーネルヘルパーが利用できない場合、次に
runtime·armArch
(Goランタイムが検出したARMアーキテクチャバージョン)をチェックします。 runtime·armArch
が6以上(ARMv6K以降)の場合、LDREXD
とSTREXD
命令を使用したネイティブなアトミックCAS実装(既存の·armCompareAndSwapUint64
関数)にフォールバックします。- このパスが利用可能な場合、
armCAS64
に·armCompareAndSwapUint64
のアドレスが設定されます。
- カーネルヘルパーが利用できない場合、次に
-
ソフトウェアエミュレーション(ARMv5):
- 上記いずれのパスも利用できない場合(主にARMv5のような古いARMアーキテクチャ)、Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS実装(
runtime·cas64
関数、generalCAS64<>
ラッパーを通じて呼び出される)にフォールバックします。 - このパスが利用される場合、
armCAS64
にgeneralCAS64<>
のアドレスが設定されます。
- 上記いずれのパスも利用できない場合(主にARMv5のような古いARMアーキテクチャ)、Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS実装(
armCAS64
というグローバル変数は、一度最適なCAS実装が決定されると、そのアドレスをキャッシュするために使用されます。これにより、以降のCompareAndSwapInt64
の呼び出しでは、毎回アーキテクチャチェックを行うことなく、直接キャッシュされた関数を呼び出すことができます。
この動的な選択ロジックにより、Goプログラムは実行されるARMデバイスとLinuxカーネルのバージョンに応じて、常に最も効率的な64ビットアトミックCAS操作を利用できるようになります。
コアとなるコードの変更箇所
変更は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
@@ -72,14 +72,63 @@ addloop1:
TEXT ·AddUintptr(SB),7,$0
B ·AddUint32(SB)
-// The kernel provides no 64-bit compare-and-swap,
-// so use native ARM instructions, which will only work on
-// ARM 11 and later devices.
-TEXT ·CompareAndSwapInt64(SB),7,$0
- B ·armCompareAndSwapUint64(SB)
+TEXT cas64<>(SB),7,$0
+ MOVW $0xffff0f60, PC // __kuser_cmpxchg64: Linux-3.1 and above
+
+TEXT kernelCAS64<>(SB),7,$0
+ // int (*__kuser_cmpxchg64_t)(const int64_t *oldval, const int64_t *newval, volatile int64_t *ptr);
+ MOVW valptr+0(FP), R2 // ptr
+ MOVW $4(FP), R0 // oldval
+ MOVW $12(FP), R1 // newval
+ BL cas64<>(SB)
+ MOVW.CS $1, R0 // C is set if the kernel has changed *ptr
+ MOVW.CC $0, R0
+ MOVW R0, 20(FP)
+ RET
+
+TEXT generalCAS64<>(SB),7,$20
+ // bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
+ MOVW valptr+0(FP), R0
+ MOVW R0, 4(R13)
+ MOVW $4(FP), R1 // oldval
+ MOVW R1, 8(R13)
+ MOVW newlo+12(FP), R2
+ MOVW R2, 12(R13)
+ MOVW newhi+16(FP), R3
+ MOVW R3, 16(R13)
+ BL runtime·cas64(SB)
+ MOVW R0, 20(FP)
+ RET
+
+GLOBL armCAS64(SB), $4
+
+TEXT setupAndCallCAS64<>(SB),7,$-4
+ MOVW $0xffff0ffc, R0 // __kuser_helper_version
+ MOVW (R0), R0
+ // __kuser_cmpxchg64 only present if helper version >= 5
+ CMP $5, R0
+ MOVW.CS $kernelCAS64<>(SB), R1
+ MOVW.CS R1, armCAS64(SB)
+ MOVW.CS R1, PC
+ MOVB runtime·armArch(SB), R0
+ // LDREXD, STREXD only present on ARMv6K or higher
+ CMP $6, R0 // TODO(minux): how to differentiate ARMv6 with ARMv6K?
+ MOVW.CS $·armCompareAndSwapUint64(SB), R1
+ MOVW.CS R1, armCAS64(SB)
+ MOVW.CS R1, PC
+ // we are out of luck, can only use runtime's emulated 64-bit cas
+ MOVW $generalCAS64<>(SB), R1
+ MOVW R1, armCAS64(SB)
+ MOVW R1, PC
+
+TEXT ·CompareAndSwapInt64(SB),7,$-4
+ MOVW armCAS64(SB), R0
+ CMP $0, R0
+ MOVW.NE R0, PC
+ B setupAndCallCAS64<>(SB)
TEXT ·CompareAndSwapUint64(SB),7,$0
- B ·armCompareAndSwapUint64(SB)
+ B ·CompareAndSwapInt64(SB)
TEXT ·AddInt64(SB),7,$0
B ·armAddUint64(SB)
コアとなるコードの解説
-
cas64<>(SB)
:- これは、Linuxカーネルが提供する
__kuser_cmpxchg64
ヘルパー関数へのジャンプテーブルエントリです。 MOVW $0xffff0f60, PC
は、__kuser_cmpxchg64
ヘルパー関数の固定アドレス(0xffff0f60
)にプログラムカウンタ(PC)を直接設定することで、その関数を呼び出します。これは、ユーザー空間からカーネルヘルパーを効率的に呼び出すための一般的な手法です。
- これは、Linuxカーネルが提供する
-
kernelCAS64<>(SB)
:- Goの
CompareAndSwapInt64
関数からカーネルヘルパーを呼び出すためのラッパー関数です。 - Goの関数呼び出し規約(引数がスタックフレームのFPレジスタからのオフセットで渡される)から、カーネルヘルパーの期待する引数(レジスタR0, R1, R2)に変換します。
BL cas64<>(SB)
でカーネルヘルパーを呼び出します。- カーネルヘルパーの戻り値(CPSRレジスタのCフラグ)をチェックし、Goの期待するブール値(成功なら1、失敗なら0)に変換して返します。
- Goの
-
generalCAS64<>(SB)
:- Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS関数
runtime·cas64
を呼び出すためのラッパーです。 - これもGoの呼び出し規約から
runtime·cas64
の期待する引数に変換し、結果を返します。
- Goランタイムが提供するソフトウェアエミュレートされた64ビットCAS関数
-
GLOBL armCAS64(SB), $4
:armCAS64
というグローバル変数を宣言しています。この変数は、最適な64ビットCAS実装のアドレスをキャッシュするために使用されます。$4
は変数のサイズ(4バイト、ARMのポインタサイズ)を示します。
-
setupAndCallCAS64<>(SB)
:- この関数は、最初に呼び出されたときに、どの64ビットCAS実装を使用すべきかを決定し、そのアドレスを
armCAS64
に格納します。 - カーネルヘルパーのチェック:
MOVW $0xffff0ffc, R0
で__kuser_helper_version
のアドレスをロードし、その値を読み取ります。CMP $5, R0
でバージョンが5以上かチェックします。MOVW.CS $kernelCAS64<>(SB), R1
とMOVW.CS R1, armCAS64(SB)
は、条件付き実行(Cフラグがセットされている場合、つまりバージョンが5以上の場合)でkernelCAS64<>
のアドレスをarmCAS64
に格納します。MOVW.CS R1, PC
は、条件付きでkernelCAS64<>
にジャンプし、最初のCAS操作を実行します。
- LDREXD/STREXDのチェック:
- カーネルヘルパーが利用できない場合、
MOVB runtime·armArch(SB), R0
でGoランタイムが検出したARMアーキテクチャバージョンを読み取ります。 CMP $6, R0
でバージョンが6以上かチェックします。- 同様に、条件付き実行で
·armCompareAndSwapUint64
のアドレスをarmCAS64
に格納し、ジャンプします。
- カーネルヘルパーが利用できない場合、
- ソフトウェアエミュレーションへのフォールバック:
- 上記いずれの条件も満たさない場合、
generalCAS64<>
のアドレスをarmCAS64
に格納し、ジャンプします。
- 上記いずれの条件も満たさない場合、
- この関数は、最初に呼び出されたときに、どの64ビットCAS実装を使用すべきかを決定し、そのアドレスを
-
·CompareAndSwapInt64(SB)
:- Goの
sync/atomic.CompareAndSwapInt64
関数に対応するエントリポイントです。 MOVW armCAS64(SB), R0
でarmCAS64
にキャッシュされたCAS実装のアドレスを読み取ります。CMP $0, R0
でアドレスが0(初期値)かどうかをチェックします。MOVW.NE R0, PC
は、アドレスが0でない場合(つまり、すでに最適な実装が決定されている場合)に、そのアドレスに直接ジャンプしてCAS操作を実行します。B setupAndCallCAS64<>(SB)
は、アドレスが0の場合(最初の呼び出し時)にsetupAndCallCAS64<>
を呼び出して、最適な実装を決定させます。
- Goの
-
·CompareAndSwapUint64(SB)
:CompareAndSwapInt64
と同様に、CompareAndSwapUint64
もCompareAndSwapInt64
にジャンプするように変更されています。これにより、両方の関数が同じ動的な選択ロジックと実装を共有するようになります。
この変更により、GoはARM環境で64ビットアトミックCAS操作を実行する際に、利用可能な最も効率的なハードウェア機能(カーネルヘルパー、LDREXD/STREXD)を優先的に利用し、それが不可能な場合にのみソフトウェアエミュレーションにフォールバックする、堅牢でパフォーマンスの高いメカニズムを実現しています。
関連リンク
- Go言語の
sync/atomic
パッケージ: https://pkg.go.dev/sync/atomic - ARMアーキテクチャの排他ロード/ストア命令(LDREX/STREX)に関する情報:
- ARMv6 Architecture Reference Manual (LDREX/STREX命令のセクション)
- Stack Overflow: https://stackoverflow.com/questions/11227809/why-is-accessing-a-global-variable-atomic-in-c-c (アトミック操作全般の解説)
- Linuxカーネルの
__kuser_helper_version
と__kuser_cmpxchg64
に関する情報:- Linuxカーネルのソースコード(
arch/arm/kernel/entry-armv.S
やarch/arm/include/asm/kuser.h
など) - 関連するメーリングリストの議論やパッチ
- LWN.net: https://lwn.net/Articles/460620/ (ARMv6/v7のユーザー空間ヘルパーに関する記事)
- Linuxカーネルのソースコード(
参考にした情報源リンク
- Go言語のコミット履歴と関連するCL(Change List):
- https://golang.org/cl/6034048 (このコミットのCL)
- https://golang.org/cl/5978051 (コミットメッセージで言及されている先行CL)
- Go言語のIssueトラッカー:
- https://golang.org/issue/3331 (コミットメッセージで言及されているIssue)
- ARMアーキテクチャリファレンスマニュアル(ARMv5, ARMv6Kなど)
- Linuxカーネルのドキュメントとソースコード
- Go言語の
src/pkg/sync/atomic/asm_linux_arm.s
ファイルの変更前後の内容 - アセンブリ言語とARMアーキテクチャに関する一般的な知識
- アトミック操作と並行プログラミングに関する一般的な知識
- Stack Overflowや技術ブログなどのオンラインリソース