[インデックス 14161] ファイルの概要
コミット
commit ace9ff4578cb6d3077fbd2c7934b4ac063047145
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Wed Oct 17 14:27:58 2012 +0800
sync/atomic: FreeBSD/ARM support
only supports ARMv6K and newer ARM cores.
R=rsc, dave
CC=golang-dev
https://golang.org/cl/6601064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ace9ff4578cb6d3077fbd2c7934b4ac063047145
元コミット内容
このコミットは、Go言語の sync/atomic
パッケージに FreeBSD/ARM アーキテクチャのサポートを追加するものです。特に、ARMv6K 以降の新しい ARM コアのみをサポートすることが明記されています。
変更内容は以下の通りです。
src/pkg/sync/atomic/64bit_linux_arm.go
がsrc/pkg/sync/atomic/64bit_arm.go
にリネームされました。これは、64ビットアトミック操作の共通実装がLinuxに限定されず、ARM全般に適用されることを示唆しています。src/pkg/sync/atomic/asm_freebsd_arm.s
という新しいアセンブリファイルが追加されました。このファイルには、FreeBSD/ARM 環境におけるアトミック操作の実装が含まれています。
変更の背景
Go言語の sync/atomic
パッケージは、複数のゴルーチン(Goの軽量スレッド)が共有データに安全にアクセスするためのアトミック操作を提供します。アトミック操作は、割り込み不可能な単一の操作として実行されるため、競合状態(race condition)を防ぎ、データの整合性を保証します。
このコミットが作成された2012年当時、Goは様々なプラットフォームへの対応を進めていました。FreeBSDはUNIX系の主要なオペレーティングシステムの一つであり、ARMアーキテクチャはモバイルデバイスや組み込みシステムで広く利用されていました。したがって、FreeBSD上でARMプロセッサを搭載したシステムでGoプログラムが正しく動作するためには、低レベルのアトミック操作がOSとハードウェアの特性に合わせて実装される必要がありました。
特に、コミットメッセージにある「only supports ARMv6K and newer ARM cores」という記述は重要です。これは、古いARMアーキテクチャでは、アトミック操作を効率的かつ安全に実装するためのハードウェアサポートが不足していたことを示唆しています。ARMv6Kは、アトミック操作をサポートするための特定の命令セット拡張(LDREX/STREX命令など)を導入したバージョンであり、これ以降のARMコアでなければ、Goの sync/atomic
パッケージが提供するような堅牢なアトミック操作を効率的に実現できないため、サポート対象が限定されています。
前提知識の解説
1. アトミック操作 (Atomic Operations)
アトミック操作とは、複数のスレッドやプロセスが共有データにアクセスする際に、その操作が途中で中断されることなく、完全に実行されるか、全く実行されないかのいずれかであることを保証する操作です。これにより、データの破損や不整合を防ぎます。
Go言語の sync/atomic
パッケージは、以下のような基本的なアトミック操作を提供します。
- CompareAndSwap (CAS): 特定のメモリ位置の値が期待する値と一致する場合にのみ、新しい値に更新します。これはロックフリーなアルゴリズムを実装する上で非常に重要なプリミティブです。
- Add: メモリ位置の値をアトミックに加算します。
- Load: メモリ位置の値をアトミックに読み込みます。
- Store: メモリ位置に値をアトミックに書き込みます。
2. ARMアーキテクチャとアトミック操作
ARMアーキテクチャは、RISC(Reduced Instruction Set Computer)ベースのプロセッサアーキテクチャで、低消費電力と高性能を両立しているため、スマートフォン、タブレット、組み込みシステムなど、幅広いデバイスで利用されています。
ARMプロセッサにおけるアトミック操作の実装には、主に以下の命令が使用されます。
- LDREX (Load-Exclusive): 指定されたメモリアドレスから値をロードし、そのアドレスを排他モニタに登録します。
- STREX (Store-Exclusive): 指定されたメモリアドレスに値をストアしようとします。ストアが成功するのは、そのアドレスが排他モニタに登録されており、かつそのアドレスに対する他の排他アクセスが検出されていない場合のみです。ストアが成功すると、レジスタに0が返され、失敗すると非ゼロの値が返されます。
これらの命令は、ロード・リンク/ストア・コンディショナル (LL/SC) と呼ばれるアトミックプリミティブのペアを形成し、Compare-and-Swap (CAS) などのより複雑なアトミック操作を実装するために使用されます。LDREXで値を読み込み、STREXで条件付きで書き込むことで、他のプロセッサがそのメモリ位置にアクセスしていないことを確認しながら、アトミックな更新を実現します。
3. ARMv6K
ARMv6Kは、ARMv6アーキテクチャの拡張版であり、特にマルチプロセッサシステムにおける同期プリミティブのサポートを強化しています。LDREX/STREX命令はARMv6Kで導入された重要な機能であり、これにより、ソフトウェアレベルでの複雑なロック機構を必要とせずに、ハードウェアレベルで効率的なアトミック操作が可能になりました。このコミットがARMv6K以降のコアを要求するのは、これらの排他ロード/ストア命令がアトミック操作の実装に不可欠であるためです。
4. FreeBSD
FreeBSDは、UNIX系のオープンソースオペレーティングシステムです。安定性、セキュリティ、パフォーマンスに優れており、サーバー、組み込みシステム、デスクトップなど、様々な用途で利用されています。Go言語がFreeBSD上で動作するためには、OSのシステムコールやハードウェアの特性に合わせた低レベルの実装が必要となります。
技術的詳細
このコミットの主要な技術的詳細は、FreeBSD/ARM環境におけるアトミック操作をアセンブリ言語で実装している点にあります。Goの sync/atomic
パッケージは、プラットフォーム固有の最適化やハードウェアの特性を最大限に活用するために、多くの場合、アセンブリ言語で実装されます。
asm_freebsd_arm.s
ファイルは、Goのアセンブリ言語(Plan 9アセンブラの構文に似ています)で書かれており、以下の主要なアトミック操作のラッパー関数を提供しています。
CompareAndSwapInt32
,CompareAndSwapUint32
,CompareAndSwapUintptr
,CompareAndSwapPointer
AddInt32
,AddUint32
,AddUintptr
CompareAndSwapInt64
,CompareAndSwapUint64
AddInt64
,AddUint64
LoadInt32
,LoadUint32
,LoadInt64
,LoadUint64
,LoadUintptr
,LoadPointer
StoreInt32
,StoreUint32
,StoreInt64
,StoreUint64
,StoreUintptr
,StorePointer
これらの関数は、実際の低レベルのアトミック操作を行う別の内部関数(例: armCompareAndSwapUint32
, armAddUint32
, armCompareAndSwapUint64
, addUint64
, loadUint64
, storeUint64
)にジャンプ(B
命令)しています。これは、共通のロジックを再利用し、コードの重複を避けるための一般的なパターンです。
特に注目すべきは、LoadUint32
および StoreUint32
の実装です。これらはARMの LDREX
と STREX
命令を使用して、アトミックなロードとストアを実現しています。
-
LoadUint32
:MOVW addr+0(FP), R1
: 関数引数addr
(読み込むメモリアドレス) をレジスタR1
にロードします。load32loop:
: ループの開始ラベル。LDREX (R1), R2
:R1
が指すアドレスから値を排他的にロードし、結果をR2
に格納します。このアドレスは排他モニタに登録されます。STREX R2, (R1), R0
:R2
の値をR1
が指すアドレスに排他的にストアしようとします。ストアが成功するとR0
に0が、失敗すると非ゼロが返されます。CMP $0, R0
:STREX
の結果 (R0
) が0かどうかを比較します。BNE load32loop
: 結果が0でなければ(ストアが失敗していれば)、load32loop
に戻って再試行します。これは、他のプロセッサがその間にメモリにアクセスした可能性があるためです。MOVW R2, val+4(FP)
: 成功した場合、ロードした値 (R2
) を関数の戻り値val
に格納します。RET
: 関数から戻ります。
-
StoreUint32
:MOVW addr+0(FP), R1
: 関数引数addr
(書き込むメモリアドレス) をレジスタR1
にロードします。MOVW val+4(FP), R2
: 関数引数val
(書き込む値) をレジスタR2
にロードします。storeloop:
: ループの開始ラベル。LDREX (R1), R4
:R1
が指すアドレスから値を排他的にロードし、結果をR4
に格納します。これは、STREX
が成功するための前提条件として、排他モニタをセットアップするために行われます。ロードした値自体はここでは使用されません。STREX R2, (R1), R0
:R2
の値をR1
が指すアドレスに排他的にストアしようとします。CMP $0, R0
:STREX
の結果 (R0
) が0かどうかを比較します。BNE storeloop
: 結果が0でなければ(ストアが失敗していれば)、storeloop
に戻って再試行します。RET
: 関数から戻ります。
これらの実装は、ARMv6K以降のプロセッサが提供するハードウェアレベルの排他ロード/ストア命令を直接利用することで、効率的かつ正確なアトミック操作を実現しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は以下の2点です。
-
ファイルのリネーム:
src/pkg/sync/atomic/64bit_linux_arm.go
がsrc/pkg/sync/atomic/64bit_arm.go
に変更されました。- これは、64ビットアトミック操作のGo言語側の実装が、Linuxに特化せず、より汎用的なARMプラットフォーム向けになったことを示しています。ファイルの内容自体は変更されていませんが、その適用範囲が広がったことを意味します。
-
新規アセンブリファイルの追加:
src/pkg/sync/atomic/asm_freebsd_arm.s
が新規追加されました。- このファイルは、FreeBSD/ARM環境における32ビットおよび64ビットのアトミック操作(CompareAndSwap, Add, Load, Store)をアセンブリ言語で実装しています。Goの
sync/atomic
パッケージが提供するGo言語のAPIから、これらのアセンブリ関数が呼び出されることで、プラットフォーム固有の効率的なアトミック操作が実現されます。
コアとなるコードの解説
追加された src/pkg/sync/atomic/asm_freebsd_arm.s
ファイルは、Goの sync/atomic
パッケージがFreeBSD/ARM上で動作するために必要な低レベルのアトミック操作を定義しています。
ファイル冒頭のコメント // TODO(minux): this only supports ARMv6K or higher.
は、この実装がARMv6K以降のプロセッサに依存していることを明確に示しています。これは、前述の LDREX
および STREX
命令がARMv6Kで導入されたためです。
コードの構造は、Goのアセンブリ言語の慣習に従っています。
TEXT
ディレクティブは関数の定義を開始します。例えば、TEXT ·CompareAndSwapInt32(SB),7,$0
はCompareAndSwapInt32
というGo関数に対応するアセンブリコードの開始を示します。(SB)
は、シンボルが静的セグメント(Static Base)にあることを示します。,7
は、GoのABI(Application Binary Interface)におけるレジスタ使用規則に関連するフラグです。,$0
や$-4
は、スタックフレームのサイズを示します。
多くの関数は、対応する符号なし整数バージョンや、より汎用的な内部アセンブリ関数にジャンプしています。
TEXT ·CompareAndSwapInt32(SB),7,$0
はB ·CompareAndSwapUint32(SB)
にジャンプし、さらにB ·armCompareAndSwapUint32(SB)
にジャンプします。これは、符号付き/符号なし、ポインタ型などのアトミック操作が、最終的に共通の低レベルアセンブリルーチンを呼び出すように設計されていることを示しています。
特に重要なのは、LoadUint32
と StoreUint32
の実装です。これらは、ARMの排他ロード/ストア命令 (LDREX
, STREX
) を使用して、競合状態を避けるためのループを構築しています。
LoadUint32
の詳細な解説:
TEXT ·LoadUint32(SB),7,$0
MOVW addr+0(FP), R1 // 関数引数 addr (読み込むメモリアドレス) を R1 にロード
load32loop:
LDREX (R1), R2 // R1 が指すアドレスから値を排他的にロードし、R2 に格納。排他モニタに登録。
STREX R2, (R1), R0 // R2 の値を R1 が指すアドレスに排他的にストアしようとする。成功なら R0=0、失敗なら R0!=0。
CMP $0, R0 // STREX の結果 (R0) が0かどうかを比較
BNE load32loop // 0でなければ (ストアが失敗していれば)、load32loop に戻って再試行
MOVW R2, val+4(FP) // 成功した場合、ロードした値 (R2) を関数の戻り値 val に格納
RET // 関数から戻る
この LoadUint32
は、単にメモリから値を読み込むだけでなく、その読み込みがアトミックであることを保証しています。LDREX
で排他ロックを取得し、STREX
でそのロックがまだ有効であることを確認しながら、読み込んだ値を「書き戻す」という一見奇妙な操作を行っています。これは、LDREX
/STREX
ペアが、その間のメモリへの他の書き込みを検出するためのメカニズムとして機能するためです。もし STREX
が失敗した場合、それは他のプロセッサがそのメモリ位置を変更したことを意味するため、ループして再試行することで、常に最新のアトミックな値を取得します。
StoreUint32
の詳細な解説:
TEXT ·StoreUint32(SB),7,$0
MOVW addr+0(FP), R1 // 関数引数 addr (書き込むメモリアドレス) を R1 にロード
MOVW val+4(FP), R2 // 関数引数 val (書き込む値) を R2 にロード
storeloop:
LDREX (R1), R4 // R1 が指すアドレスから値を排他的にロードし、R4 に格納。排他モニタをセットアップ。
STREX R2, (R1), R0 // R2 の値を R1 が指すアドレスに排他的にストアしようとする。
CMP $0, R0 // STREX の結果 (R0) が0かどうかを比較
BNE storeloop // 0でなければ (ストアが失敗していれば)、storeloop に戻って再試行
RET // 関数から戻る
StoreUint32
も同様に LDREX
/STREX
ループを使用しています。ここでは、LDREX
で値を読み込む目的は、その値を実際に使用することではなく、STREX
が成功するための前提条件として排他モニタをセットアップすることです。もし STREX
が失敗した場合、それは他のプロセッサがそのメモリ位置にアクセスしたことを意味するため、ループして再試行することで、アトミックな書き込みを保証します。
これらのアセンブリコードは、Goの sync/atomic
パッケージが、特定のハードウェアアーキテクチャとOSの組み合わせにおいて、最高レベルのパフォーマンスと正確性でアトミック操作を提供するために、いかに低レベルの最適化を行っているかを示しています。
関連リンク
参考にした情報源リンク
- ARM Architecture Reference Manual (LDREX/STREX instructions) (具体的なバージョンは異なる可能性がありますが、LDREX/STREXの概念は共通です)
- Go Assembly Language
- Go sync/atomic package documentation
- FreeBSD Project
- ARMv6K Architecture (Wikipedia)
- Atomic operations - Wikipedia
- Compare-and-swap - Wikipedia