[インデックス 13801] ファイルの概要
このコミットは、Go言語の sync/atomic
パッケージにおける重要な改善を含んでいます。主な変更点は以下の通りです。
AddT
、LoadT
、StoreT
関数に対するパッケージドキュメントの追加。これにより、これらのアトミック操作の目的と使用法が明確化されました。CompareAndSwapT
およびAddT
関数の最初の引数名をval
からaddr
へと変更。これは、LoadT
およびStoreT
関数との命名の一貫性を保つためのものです。この変更は、Goのソースコードだけでなく、各アーキテクチャ(x86, AMD64, ARM)のアセンブリ言語実装にも反映されています。- テストコード (
atomic_test.go
) における引数名の変更。
これらの変更は、sync/atomic
パッケージの可読性と保守性を向上させ、開発者がアトミック操作をより正確に理解し、利用できるようにすることを目的としています。
コミット
commit e39072d65fd3aeb1db0744226c27abc1fa02e047
Author: Nigel Tao <nigeltao@golang.org>
Date: Wed Sep 12 10:36:22 2012 +1000
sync/atomic: add package doc for AddT, LoadT and StoreT.
Rename the first argument of CompareAndSwapT and AddT s/val/addr/
for consistency with LoadT and StoreT.
R=rsc, r, dvyukov
CC=golang-dev
https://golang.org/cl/6494112
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e39072d65fd3aeb1db0744226c27abc1fa02e047
元コミット内容
sync/atomic
パッケージに AddT
、LoadT
、StoreT
のパッケージドキュメントを追加しました。
CompareAndSwapT
および AddT
の最初の引数名を、LoadT
および StoreT
との一貫性のために val
から addr
に変更しました。
変更の背景
このコミットの背景には、Go言語の sync/atomic
パッケージの使いやすさと一貫性の向上という明確な意図があります。
-
ドキュメントの不足:
sync/atomic
パッケージは、並行処理において非常に重要なアトミック操作を提供しますが、AddT
、LoadT
、StoreT
といった基本的な操作に対する公式なパッケージドキュメントが不足していました。これにより、開発者がこれらの関数の正確な振る舞いや意図を理解する上で障壁がありました。ドキュメントの追加は、パッケージの利用を促進し、誤用を防ぐ上で不可欠です。 -
引数名の一貫性の欠如:
CompareAndSwapT
およびAddT
関数では、操作対象のアドレスを示す引数名がval
となっていました。これに対し、LoadT
およびStoreT
関数では同じ目的の引数名がaddr
となっており、命名規則に一貫性がありませんでした。このような不一致は、コードの可読性を損ない、開発者がAPIを学習する際の混乱を招く可能性があります。引数名をaddr
に統一することで、API全体の整合性が高まり、より直感的で理解しやすいインターフェースが提供されます。
これらの変更は、Go言語の標準ライブラリが常に高品質で、使いやすく、十分にドキュメント化されているべきであるという設計哲学に基づいています。特に、並行処理のような複雑な領域では、明確なドキュメントと一貫したAPI設計が、バグの削減と開発効率の向上に直結します。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. Go言語の sync/atomic
パッケージ
sync/atomic
パッケージは、Go言語において低レベルなアトミック操作を提供する標準ライブラリです。アトミック操作とは、複数のゴルーチン(Goの軽量スレッド)が同時に共有データにアクセスする際に、その操作が不可分(中断されない)であることを保証するものです。これにより、競合状態(race condition)を防ぎ、データの整合性を保つことができます。
主なアトミック操作には以下のようなものがあります。
- Compare-And-Swap (CAS): 特定のメモリ位置の値が期待する値と一致する場合にのみ、その値を新しい値に更新します。これは、ロックを使用せずにスレッドセーフなデータ構造を構築するための基本的なプリミティブです。
CompareAndSwapInt32
,CompareAndSwapInt64
,CompareAndSwapUint32
,CompareAndSwapUint64
,CompareAndSwapUintptr
,CompareAndSwapPointer
など。
- Add: 特定のメモリ位置の値に指定された値をアトミックに加算します。
AddInt32
,AddInt64
,AddUint32
,AddUint64
,AddUintptr
など。
- Load: 特定のメモリ位置の値をアトミックに読み込みます。
LoadInt32
,LoadInt64
,LoadUint32
,LoadUint64
,LoadUintptr
,LoadPointer
など。
- Store: 特定のメモリ位置に値をアトミックに書き込みます。
StoreInt32
,StoreInt64
,StoreUint32
,StoreUint64
,StoreUintptr
,StorePointer
など。
これらの関数は、通常、ミューテックス(sync.Mutex
)のようなロック機構よりも高速であり、特定のシナリオでパフォーマンス上の利点をもたらします。
2. アセンブリ言語とGoの内部実装
Go言語の sync/atomic
パッケージの関数は、Goのコードで直接実装されているわけではなく、各CPUアーキテクチャに特化したアセンブリ言語で実装されています。これは、アトミック操作がCPUの特定の命令セットに依存するためです。
.s
ファイル: Goのソースツリーでは、アセンブリ言語で書かれたファイルは.s
拡張子を持ちます。例えば、asm_386.s
は32ビットx86アーキテクチャ用、asm_amd64.s
は64ビットx86アーキテクチャ用、asm_arm.s
はARMアーキテクチャ用のアセンブリコードを含んでいます。TEXT
ディレクティブ: Goのアセンブリでは、TEXT
ディレクティブがGo関数に対応するアセンブリルーチンを定義します。例えば、TEXT ·CompareAndSwapUint32(SB),7,$0
はCompareAndSwapUint32
関数を定義しています。- レジスタとメモリ操作: アセンブリコードでは、CPUのレジスタ(例:
AX
,BP
,R1
,R2
など)やメモリ(例:0(BP)
,4(FP)
など)を直接操作します。FP
(Frame Pointer): 関数呼び出しのスタックフレームのベースを指します。valptr+0(FP)
のように、引数やローカル変数はフレームポインタからのオフセットでアクセスされます。SB
(Static Base): グローバルシンボルや外部シンボルを指します。
- アトミック命令:
- x86/AMD64:
LOCK
プレフィックス: 続く命令がアトミックに実行されることを保証します。これは、マルチプロセッサ環境でキャッシュコヒーレンシを維持するために重要です。CMPXCHG
(Compare and Exchange): CAS操作を実装するための命令です。アキュムレータ(AX
/EAX
/RAX
)の値とメモリの値を比較し、一致すればメモリを新しい値で更新します。XADD
(Exchange and Add): メモリの値をレジスタの値で加算し、元のメモリの値をレジスタに格納します。これもアトミックな操作です。
- ARM:
LDREX
(Load-Exclusive) /STREX
(Store-Exclusive): ARMv6以降で導入された排他ロード/ストア命令のペアで、CASやアトミックな加算などの操作を実装するために使用されます。LDREX
で値を読み込み、STREX
で書き込みを試みます。STREX
が成功した場合(他のプロセッサがそのメモリ位置に書き込んでいない場合)は成功コードを返します。
- x86/AMD64:
3. unsafe.Pointer
Goの unsafe
パッケージは、型安全性をバイパスしてポインタを直接操作することを可能にします。unsafe.Pointer
は、任意の型のポインタを保持できる汎用ポインタ型です。アトミック操作では、特定の型に依存しない汎用的なメモリ操作が必要となるため、unsafe.Pointer
が頻繁に利用されます。ただし、unsafe
パッケージの使用は、Goの型システムが提供する安全性を損なうため、非常に慎重に行う必要があります。
技術的詳細
このコミットの技術的な詳細は、主に以下の2つの側面に焦点を当てています。
1. 引数名の val
から addr
への変更
この変更は、sync/atomic
パッケージのGo言語の関数シグネチャと、それに対応するアセンブリ言語の実装の両方に影響を与えます。
Go言語の関数シグネチャの変更 (doc.go
):
変更前:
func CompareAndSwapInt32(val *int32, old, new int32) (swapped bool)
func AddInt32(val *int32, delta int32) (new int32)
変更後:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func AddInt32(addr *int32, delta int32) (new int32)
この変更は、関数のセマンティクス(意味)には影響を与えませんが、引数が「値そのもの」ではなく「値が格納されているメモリアドレス」を指すことをより明確に示します。これは、LoadT
や StoreT
関数が既に addr
という引数名を使用していたため、パッケージ全体での一貫性を高めます。
アセンブリ言語の実装の変更 (asm_386.s
, asm_amd64.s
, asm_arm.s
, asm_linux_arm.s
):
Goの関数がアセンブリ言語で実装されているため、Goの関数シグネチャの引数名変更は、アセンブリコード内の引数へのアクセス方法にも影響します。Goのコンパイラは、Goの関数引数をスタックフレーム上の特定のオフセットに配置します。アセンブリコードでは、これらの引数に 引数名+オフセット(FP)
の形式でアクセスします。
例えば、32ビットx86アーキテクチャの asm_386.s
では、CompareAndSwapUint32
関数の最初の引数にアクセスする際に、valptr+0(FP)
から addr+0(FP)
へと変更されています。
変更前:
TEXT ·CompareAndSwapUint32(SB),7,$0
MOVL valptr+0(FP), BP
変更後:
TEXT ·CompareAndSwapUint32(SB),7,$0
MOVL addr+0(FP), BP
同様の変更が、AddT
および LoadT
/StoreT
の関連するアセンブリ関数にも適用されています。この変更は、アセンブリコードの可読性を向上させ、Goの関数シグネチャとの対応関係をより明確にします。
テストコードの変更 (atomic_test.go
):
テスト関数内の引数名も、Goの関数シグネチャの変更に合わせて val
から addr
に変更されています。例えば、hammerAddInt32
関数のシグネチャが uval *uint32
から uaddr *uint32
に変更され、関数内部の変数名も val
から addr
に変更されています。
変更前:
func hammerAddInt32(uval *uint32, count int) {
val := (*int32)(unsafe.Pointer(uval))
for i := 0; i < count; i++ {
AddInt32(val, 1)
}
}
変更後:
func hammerAddInt32(uaddr *uint32, count int) {
addr := (*int32)(unsafe.Pointer(uaddr))
for i := 0; i < count; i++ {
AddInt32(addr, 1)
}
}
これは、コードベース全体での一貫性を保つためのクリーンアップ作業です。
2. AddT
, LoadT
, StoreT
のパッケージドキュメント追加
src/pkg/sync/atomic/doc.go
ファイルに、AddT
、LoadT
、StoreT
関数のアトミックな振る舞いを説明するドキュメントが追加されました。
追加されたドキュメントは、これらの操作がそれぞれ以下のアトミックな等価操作であることを明確に示しています。
- Add操作:
*addr += delta
とreturn *addr
のアトミックな等価操作。 - Load操作:
return *addr
のアトミックな等価操作。 - Store操作:
*addr = val
のアトミックな等価操作。
これらの説明は、開発者がこれらの関数の目的と、それがどのように並行処理の安全性を保証するかを理解する上で非常に役立ちます。特に、アトミック操作が単なる読み書きや加算ではなく、不可分な操作であることを強調しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。
-
src/pkg/sync/atomic/asm_386.s
:CompareAndSwapUint32
,CompareAndSwapUint64
,AddUint32
,AddUint64
,LoadUint32
,LoadUint64
,StoreUint32
,StoreUint64
の各関数で、引数valptr
またはaddrptr
をaddr
に変更。- 例:
MOVL valptr+0(FP), BP
がMOVL addr+0(FP), BP
に変更。
-
src/pkg/sync/atomic/asm_amd64.s
:CompareAndSwapUint32
,CompareAndSwapUint64
,AddUint32
,AddUint64
,LoadUint32
,LoadUint64
,LoadUintptr
,LoadPointer
,StoreUint32
,StoreUint64
,StoreUintptr
,StorePointer
の各関数で、引数valptr
またはaddrptr
をaddr
に変更。- 例:
MOVQ valptr+0(FP), BP
がMOVQ addr+0(FP), BP
に変更。
-
src/pkg/sync/atomic/asm_arm.s
:armCompareAndSwapUint32
,armCompareAndSwapUint64
,armAddUint32
,armAddUint64
,armLoadUint64
,armStoreUint64
の各関数で、引数valptr
またはaddrptr
をaddr
に変更。- 例:
MOVW valptr+0(FP), R1
がMOVW addr+0(FP), R1
に変更。
-
src/pkg/sync/atomic/asm_linux_arm.s
:CompareAndSwapUint32
,AddUint32
,kernelCAS64<>
,generalCAS64<>
,LoadUint32
,StoreUint32
の各関数で、引数valptr
またはaddrptr
をaddr
に変更。- 例:
MOVW valptr+0(FP), R2
がMOVW addr+0(FP), R2
に変更。
-
src/pkg/sync/atomic/atomic_test.go
:hammerAddInt32
,hammerAddUint32
,hammerAddUintptr32
,hammerCompareAndSwapInt32
,hammerCompareAndSwapUint32
,hammerCompareAndSwapUintptr32
,hammerCompareAndSwapPointer32
,hammerAddInt64
,hammerAddUint64
,hammerAddUintptr64
,hammerCompareAndSwapInt64
,hammerCompareAndSwapUint64
,hammerCompareAndSwapUintptr64
,hammerCompareAndSwapPointer64
,hammerStoreLoadInt32
,hammerStoreLoadUint32
,hammerStoreLoadInt64
,hammerStoreLoadUint64
,hammerStoreLoadUintptr
,hammerStoreLoadPointer
の各テスト関数で、引数名uval
をuaddr
に、内部変数名val
をaddr
に変更。
-
src/pkg/sync/atomic/doc.go
:CompareAndSwapInt32
,CompareAndSwapInt64
,CompareAndSwapUint32
,CompareAndSwapUint64
,CompareAndSwapUintptr
,CompareAndSwapPointer
,AddInt32
,AddUint32
,AddInt64
,AddUint64
,AddUintptr
の各関数の引数名val
をaddr
に変更。AddT
、LoadT
、StoreT
のアトミック操作に関する新しいパッケージドキュメントを追加。
コアとなるコードの解説
1. 引数名の変更 (val
-> addr
)
この変更は、Goの sync/atomic
パッケージのAPIと、それを実装するアセンブリコードの両方で行われました。
-
Goの関数シグネチャ:
CompareAndSwapT
およびAddT
ファミリーの関数において、最初の引数名がval
からaddr
に変更されました。これは、これらの関数が操作する対象が「値そのもの」ではなく、「値が格納されているメモリアドレス」であることをより正確に表現するためです。LoadT
およびStoreT
ファミリーの関数は既にaddr
を使用していたため、この変更によりパッケージ全体での命名規則の一貫性が確立されました。これにより、開発者は引数の役割をより直感的に理解できるようになります。 -
アセンブリコード: Goの関数シグネチャの変更に伴い、各アーキテクチャ(x86, AMD64, ARM)のアセンブリ言語実装も更新されました。アセンブリコードでは、Goの関数引数はスタックフレーム上のオフセットとしてアクセスされます。例えば、
MOVL valptr+0(FP), BP
のような命令は、スタックフレームのFP
からvalptr
のオフセットにある値をBP
レジスタにロードしていました。この変更により、MOVL addr+0(FP), BP
のように、アセンブリコード内でも引数名がaddr
に統一されました。これは、アセンブリコードの可読性を向上させ、Goの関数定義との対応関係を明確にするためのものです。機能的な変更はなく、あくまで命名の一貫性を保つためのリファクタリングです。 -
テストコード:
atomic_test.go
内のテスト関数も、Goの関数シグネチャの変更に合わせて引数名と内部変数名がval
からaddr
に変更されました。これにより、テストコードも最新のAPI定義に準拠し、コードベース全体の一貫性が保たれています。
2. パッケージドキュメントの追加
src/pkg/sync/atomic/doc.go
に追加されたドキュメントは、AddT
、LoadT
、StoreT
の各アトミック操作の振る舞いを明確に説明しています。
-
AddT
のドキュメント:// The add operation, implemented by the AddT functions, is the atomic // equivalent of: // // *addr += delta // return *addr
これは、
AddT
関数が、指定されたアドレス*addr
の値にdelta
をアトミックに加算し、その結果の新しい値を返すことを示しています。重要なのは「アトミック」であるという点で、この操作が他のゴルーチンによって中断されることなく、単一の不可分なステップとして実行されることを保証します。 -
LoadT
およびStoreT
のドキュメント:// The load and store operations, implemented by the LoadT and StoreT // functions, are the atomic equivalents of "return *addr" and // "*addr = val".
LoadT
関数は、指定されたアドレス*addr
の値をアトミックに読み込み、StoreT
関数は、指定されたアドレス*addr
にval
をアトミックに書き込むことを説明しています。これらの操作もアトミックであり、並行アクセス時のデータの破損を防ぎます。
これらのドキュメントは、sync/atomic
パッケージの利用者が、各関数の正確なセマンティクスを理解し、並行処理のコードをより安全かつ効率的に記述するのに役立ちます。特に、アトミック操作が単なる通常の読み書きや加算とは異なり、CPUレベルでの特別な保証を伴うことを明確にしています。
関連リンク
- Go CL 6494112: https://golang.org/cl/6494112
参考にした情報源リンク
- Go言語の
sync/atomic
パッケージ公式ドキュメント: https://pkg.go.dev/sync/atomic - Goのアセンブリ言語について: https://go.dev/doc/asm
- x86アセンブリ命令 (CMPXCHG, XADD, LOCK):
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals
- ARMアセンブリ命令 (LDREX, STREX):
- ARM Architecture Reference Manual
- 並行処理とアトミック操作に関する一般的な情報源 (例: Wikipedia, コンピュータサイエンスの教科書)
unsafe.Pointer
の使用に関するGoのドキュメント: https://pkg.go.dev/unsafe