[インデックス 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