[インデックス 15591] ファイルの概要
このコミットは、Go言語のランタイムに64ビットのアトミックな交換操作(xchg64
)を追加するものです。特にネットワークポーラーでの利用を想定しており、並行処理におけるデータの一貫性と効率性を向上させることを目的としています。
コミット
commit add334986778a09dce559f4d8b7ee69534abd47d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Mar 5 09:46:52 2013 +0200
runtime: add atomic xchg64
It will be handy for network poller.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7429048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/add334986778a09dce559f4d8b7ee69534abd47d
元コミット内容
runtime: add atomic xchg64
It will be handy for network poller.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7429048
変更の背景
このコミットの主な背景は、Goランタイムにおけるネットワークポーラーの効率化と堅牢性の向上です。ネットワークポーラーは、I/O操作(ネットワーク通信など)が完了するのを待つゴルーチンを管理するGoランタイムの重要なコンポーネントです。多数のネットワーク接続を効率的に処理するためには、共有データ構造へのアクセスをアトミックに(不可分に)行う必要があります。
既存のアトミック操作では、64ビット値の交換(Exchange)を直接サポートするものが不足していた可能性があります。xchg
(Exchange)操作は、メモリ位置の値を新しい値と交換し、元の値を返すという一連の操作をアトミックに実行します。これは、ロックを使用せずに共有状態を更新する際に非常に有用です。
特に、ネットワークポーラーのような高性能なシステムでは、ロックの取得と解放に伴うオーバーヘッドを最小限に抑えることが重要です。アトミック操作は、このようなシナリオでロックフリーなデータ構造を実装するための基盤を提供し、並行処理のパフォーマンスを向上させます。
このコミットは、xchg64
という64ビットのアトミック交換操作を導入することで、ネットワークポーラーがより効率的かつ安全に内部状態を更新できるようにすることを目的としています。
前提知識の解説
1. Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、システムコールインターフェースなどが含まれます。ランタイムは、GoプログラムがOSとどのように対話するか、そして並行処理がどのように実現されるかを定義する低レベルのコードで構成されています。
2. アトミック操作 (Atomic Operations)
アトミック操作とは、複数のCPU命令から構成される一連の操作が、他のスレッドやプロセスの干渉なしに、単一の不可分な操作として実行されることを保証するものです。これにより、並行処理環境におけるデータ競合(Data Race)を防ぎ、共有データの整合性を保つことができます。
- 不可分性: 操作の途中で他のスレッドが割り込むことができないことを意味します。
- メモリバリア/フェンス: アトミック操作は通常、メモリバリアを含み、コンパイラやCPUによる命令の並べ替えを防ぎ、メモリ操作の順序を保証します。
一般的なアトミック操作には以下のようなものがあります。
- Compare-and-Swap (CAS): 特定のメモリ位置の値が期待する値と一致する場合にのみ、その値を新しい値に更新します。
- Fetch-and-Add: メモリ位置の値を読み取り、それに値を加算し、元の値を返します。
- Exchange (XCHG): メモリ位置の値を新しい値と交換し、元の値を返します。
3. xchg
命令 (Exchange Instruction)
xchg
は、x86アーキテクチャのCPU命令で、2つのオペランドの値を交換します。メモリとレジスタ、または2つのレジスタ間で値を交換できます。特に、メモリオペランドに対してxchg
を使用する場合、CPUは自動的にロックをかけ、その操作がアトミックであることを保証します。これにより、複数のCPUコアが同時に同じメモリ位置にアクセスしようとしても、データ競合が発生しません。
XCHGL
: 32ビット値を交換します。XCHGQ
: 64ビット値を交換します(AMD64アーキテクチャ)。
4. ネットワークポーラー (Network Poller)
Goのネットワークポーラーは、ノンブロッキングI/Oを効率的に管理するためのGoランタイムのコンポーネントです。GoのネットワーキングAPI(net
パッケージなど)は、ブロッキングI/Oのように見えますが、実際にはランタイムが内部的にノンブロッキングI/Oとイベント通知メカニズム(Linuxのepoll、macOSのkqueue、WindowsのIOCPなど)を使用して、多数の同時接続を効率的に処理しています。
ネットワークポーラーは、I/O操作が完了するのを待っているゴルーチンを一時停止し、I/Oイベントが発生したときにそれらを再開する役割を担います。このメカニズムにより、GoはC10K問題(単一サーバーで1万以上の同時接続を処理する問題)を効率的に解決できます。
ネットワークポーラーの内部では、多数のファイルディスクリプタ(ソケットなど)の状態を管理し、I/Oイベントの発生を監視する必要があります。この状態管理には、共有データ構造へのアトミックなアクセスが不可欠です。
5. volatile
キーワード (C言語における)
C言語におけるvolatile
キーワードは、コンパイラに対して、変数がプログラムの外部(例えば、ハードウェア、他のスレッド、シグナルハンドラなど)によって変更される可能性があることを指示します。これにより、コンパイラは最適化(例えば、レジスタへのキャッシュや不要な読み書きの削除)を抑制し、常にメモリから値を読み書きするようにします。並行処理において、共有変数へのアクセスが常に最新の値で行われることを保証するために重要です。
技術的詳細
このコミットは、Goランタイムにruntime·xchg64
という新しいアトミック操作を追加します。これは、64ビットの値をアトミックに交換するための関数です。
AMD64アーキテクチャ (asm_amd64.s
)
AMD64アーキテクチャでは、XCHGQ
命令が64ビットのアトミック交換を直接サポートしています。この命令は、メモリ位置の値をレジスタの値と交換し、その操作がアトミックであることをCPUが保証します。
src/pkg/runtime/asm_amd64.s
に追加されたアセンブリコードは、runtime·xchg64
関数を実装しています。
TEXT runtime·xchg64(SB), 7, $0
MOVQ 8(SP), BX // addr (ポインタ) を BX レジスタにロード
MOVQ 16(SP), AX // v (新しい値) を AX レジスタにロード
XCHGQ AX, 0(BX) // 0(BX) (addrが指すメモリ位置) の値と AX の値を交換
RET // AX には元の値が格納されているので、それが戻り値となる
8(SP)
と16(SP)
は、スタックフレーム上の引数(addr
とv
)にアクセスしています。XCHGQ AX, 0(BX)
が実際の64ビットアトミック交換操作を実行します。0(BX)
はBX
レジスタが指すメモリ位置を意味し、AX
レジスタの値と交換されます。交換後、AX
レジスタには交換前のメモリ位置の値が格納されており、これが関数の戻り値となります。
i386アーキテクチャ (atomic_386.c
)
i386(32ビット)アーキテクチャでは、64ビット値を直接アトミックに交換する単一の命令は存在しません。そのため、runtime·cas64
(Compare-and-Swap 64ビット)をループ内で使用してxchg64
をエミュレートしています。
uint64
runtime·xchg64(uint64 volatile* addr, uint64 v)
{
uint64 old;
old = *addr; // 現在の値を読み込む
while(!runtime·cas64(addr, &old, v)) { // CASが成功するまでループ
// nothing
}
return old; // CASが成功した時点での元の値(old)を返す
}
この実装では、まずaddr
が指す現在の値old
を読み込みます。次に、runtime·cas64
を使用して、addr
の値がold
と一致する場合にのみv
に更新しようとします。もし他のスレッドがaddr
の値を変更した場合、runtime·cas64
は失敗し、ループが繰り返されます。このループは、runtime·cas64
が成功するまで、つまりaddr
の値をアトミックにv
に更新できるまで続きます。
ARMアーキテクチャ (atomic_arm.c
)
ARMアーキテクチャでも、i386と同様に、64ビットのアトミック交換を直接サポートする単一の命令がない場合があります(特に古いARMv6以前のアーキテクチャ)。このコミットでは、ミューテックスのようなロック機構を使用してxchg64
を実装しています。
uint64
runtime·xchg64(uint64 volatile *addr, uint64 v)
{
uint64 res;
runtime·lock(LOCK(addr)); // addr に関連付けられたロックを取得
res = *addr; // 現在の値を読み込む
*addr = v; // 新しい値を書き込む
runtime·unlock(LOCK(addr)); // ロックを解放
return res; // 元の値を返す
}
この実装は、addr
が指すメモリ位置に対する排他的アクセスを保証するために、runtime·lock
とruntime·unlock
を使用しています。これは、アトミック操作をソフトウェア的に実現する一般的な方法ですが、ハードウェアによるアトミック命令に比べてオーバーヘッドが大きくなる可能性があります。
ランタイムテスト (runtime.c
)
src/pkg/runtime/runtime.c
のTestAtomic64
関数に、新しく追加されたruntime·xchg64
のテストケースが追加されています。
if(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
runtime·throw("xchg64 failed");
if(runtime·atomicload64(&z64) != (3ull<<40)+3)
runtime·throw("xchg64 failed");
このテストは、runtime·xchg64
が期待通りに値を交換し、交換後の値が正しく設定されていることを検証します。
ヘッダーファイル (runtime.h
)
src/pkg/runtime/runtime.h
にruntime·xchg64
関数のプロトタイプが追加されています。
uint64 runtime·xchg64(uint64 volatile*, uint64);
これにより、他のランタイムコードからruntime·xchg64
関数を呼び出すことができるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の5つのファイルに分散しています。
src/pkg/runtime/asm_amd64.s
: AMD64アーキテクチャ向けのアセンブリ実装。XCHGQ
命令を直接使用してruntime·xchg64
を実装しています。src/pkg/runtime/atomic_386.c
: i386アーキテクチャ向けC言語実装。runtime·cas64
をループ内で使用してruntime·xchg64
をエミュレートしています。src/pkg/runtime/atomic_arm.c
: ARMアーキテクチャ向けC言語実装。runtime·lock
とruntime·unlock
を使用してruntime·xchg64
を実装しています。src/pkg/runtime/runtime.c
:TestAtomic64
関数にruntime·xchg64
のテストケースを追加。src/pkg/runtime/runtime.h
:runtime·xchg64
関数のプロトタイプ宣言を追加。
これらの変更により、Goランタイムは異なるアーキテクチャ上で64ビットのアトミック交換操作を統一されたインターフェースで提供できるようになります。
コアとなるコードの解説
src/pkg/runtime/asm_amd64.s
--- a/src/pkg/runtime/asm_amd64.s
+++ b/src/pkg/runtime/asm_amd64.s
@@ -442,6 +442,12 @@ TEXT runtime·xchg(SB), 7, $0
XCHGL AX, 0(BX)
RET
+TEXT runtime·xchg64(SB), 7, $0
+\tMOVQ\t8(SP), BX
+\tMOVQ\t16(SP), AX
+\tXCHGQ\tAX, 0(BX)
+\tRET
+\
TEXT runtime·procyield(SB),7,$0
MOVL\t8(SP), AX
again:
このアセンブリコードは、AMD64アーキテクチャにおけるruntime·xchg64
関数の実装です。
TEXT runtime·xchg64(SB), 7, $0
:runtime·xchg64
という名前の関数を定義しています。SB
はStatic Baseレジスタ、7
はスタックフレームのサイズ、$0
は引数のサイズを示します。MOVQ 8(SP), BX
: スタックポインタSP
から8バイトオフセットの位置にある値(最初の引数addr
)をBX
レジスタに移動します。MOVQ 16(SP), AX
: スタックポインタSP
から16バイトオフセットの位置にある値(2番目の引数v
)をAX
レジスタに移動します。XCHGQ AX, 0(BX)
:BX
レジスタが指すメモリ位置(addr
が指す場所)の64ビット値とAX
レジスタの64ビット値をアトミックに交換します。交換後、AX
レジスタには交換前のメモリ位置の値が格納されます。RET
: 関数から戻ります。AX
レジスタに格納された値が関数の戻り値となります。
src/pkg/runtime/atomic_386.c
--- a/src/pkg/runtime/atomic_386.c
+++ b/src/pkg/runtime/atomic_386.c
@@ -30,3 +30,16 @@ runtime·xadd64(uint64 volatile* addr, int64 v)
}\n\treturn old+v;\n}\n+\n+#pragma textflag 7
+uint64
+runtime·xchg64(uint64 volatile* addr, uint64 v)
+{\n+\tuint64 old;\n+\n+\told = *addr;\n+\twhile(!runtime·cas64(addr, &old, v)) {\n+\t\t// nothing\n+\t}\n+\treturn old;\n+}\n```
このC言語コードは、i386アーキテクチャにおける`runtime·xchg64`関数の実装です。
* `#pragma textflag 7`: コンパイラに対する指示で、この関数のコードを特定のセクションに配置することを示します。
* `uint64 old;`: 交換前の値を保持するための変数`old`を宣言します。
* `old = *addr;`: `addr`が指す現在の値を`old`に読み込みます。
* `while(!runtime·cas64(addr, &old, v))`: `runtime·cas64`関数を呼び出し、`addr`が指す値が`old`と等しい場合にのみ、その値を`v`に更新しようとします。`runtime·cas64`は成功した場合に`true`を返します。失敗した場合は`false`を返し、ループが続行されます。ループ内で`old`の値は`runtime·cas64`によって自動的に更新され、最新のメモリ値が反映されます。
* `return old;`: `runtime·cas64`が成功した時点での`old`の値(つまり、交換前のメモリ位置の値)を返します。
### `src/pkg/runtime/atomic_arm.c`
```diff
--- a/src/pkg/runtime/atomic_arm.c
+++ b/src/pkg/runtime/atomic_arm.c
@@ -121,6 +121,19 @@ runtime·xadd64(uint64 volatile *addr, int64 delta)
return res;\n}\n \n+#pragma textflag 7
+uint64
+runtime·xchg64(uint64 volatile *addr, uint64 v)
+{\n+\tuint64 res;\n+\n+\truntime·lock(LOCK(addr));
+\tres = *addr;\n+\t*addr = v;\n+\truntime·unlock(LOCK(addr));
+\treturn res;\n+}\n+\n #pragma textflag 7
uint64
runtime·atomicload64(uint64 volatile *addr)
このC言語コードは、ARMアーキテクチャにおけるruntime·xchg64
関数の実装です。
#pragma textflag 7
: コンパイラに対する指示です。uint64 res;
: 交換前の値を保持するための変数res
を宣言します。runtime·lock(LOCK(addr));
:addr
に関連付けられたロックを取得します。これにより、このクリティカルセクションへの排他的アクセスが保証されます。res = *addr;
: ロックが取得された後、addr
が指す現在の値をres
に読み込みます。*addr = v;
:addr
が指すメモリ位置に新しい値v
を書き込みます。runtime·unlock(LOCK(addr));
: ロックを解放します。return res;
: 交換前の値res
を返します。
src/pkg/runtime/runtime.c
--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -156,6 +156,10 @@ TestAtomic64(void)
runtime·throw("xadd64 failed");
if(runtime·atomicload64(&z64) != (2ull<<40)+2)
runtime·throw("xadd64 failed");
+\tif(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
+\t\truntime·throw("xchg64 failed");
+\tif(runtime·atomicload64(&z64) != (3ull<<40)+3)
+\t\truntime·throw("xchg64 failed");
}\n \n void
このC言語コードは、ランタイムのテスト関数TestAtomic64
にruntime·xchg64
のテストケースを追加しています。
if(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
:z64
という64ビット変数に対してruntime·xchg64
を呼び出し、(3ull<<40)+3
という新しい値と交換します。この時、runtime·xchg64
の戻り値(交換前のz64
の値)が期待値(2ull<<40)+2
と異なる場合、エラーをスローします。if(runtime·atomicload64(&z64) != (3ull<<40)+3)
:runtime·xchg64
の呼び出し後、z64
の現在の値が新しい値(3ull<<40)+3
と等しいことを確認します。異なる場合、エラーをスローします。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -691,6 +691,7 @@ bool\truntime·casp(void**, void*, void*);\n uint32\truntime·xadd(uint32 volatile*, int32);\n uint64\truntime·xadd64(uint64 volatile*, int64);\n uint32\truntime·xchg(uint32 volatile*, uint32);\n+uint64\truntime·xchg64(uint64 volatile*, uint64);\n uint32\truntime·atomicload(uint32 volatile*);\n void\truntime·atomicstore(uint32 volatile*, uint32);\n void\truntime·atomicstore64(uint64 volatile*, uint64);\n```
このヘッダーファイルへの変更は、`runtime·xchg64`関数のプロトタイプ宣言を追加するものです。
* `uint64 runtime·xchg64(uint64 volatile*, uint64);`: `runtime·xchg64`関数が、`uint64 volatile*`型のアドレスと`uint64`型の新しい値を受け取り、`uint64`型の元の値を返すことを宣言しています。これにより、他のC言語ファイルからこの関数を正しく呼び出すことができるようになります。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Goランタイムのソースコード: [https://github.com/golang/go/tree/master/src/runtime](https://github.com/golang/go/tree/master/src/runtime)
* Goの並行処理に関するブログ記事やドキュメント(例: Go Concurrency Patterns)
## 参考にした情報源リンク
* Goのコミット履歴: [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
* Goのコードレビューシステム (Gerrit): [https://go.googlesource.com/go/+/refs/heads/master](https://go.googlesource.com/go/+/refs/heads/master)
* GoのIssue Tracker: [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* アトミック操作に関する一般的な情報源(例: Wikipedia, CPUアーキテクチャのマニュアル)
* Goのネットワークポーラーに関する技術記事や解説
* Goの`sync/atomic`パッケージのドキュメント: [https://pkg.go.dev/sync/atomic](https://pkg.go.dev/sync/atomic) (このコミットはランタイム内部のアトミック操作に関するものですが、Go言語レベルでのアトミック操作の理解に役立ちます)
* Dmitriy Vyukov氏の他のコミットや活動(Goの並行処理とランタイムに多大な貢献をしています)
* Goの`CL`(Change List)リンク: [https://golang.org/cl/7429048](https://golang.org/cl/7429048) (このコミットの元のコードレビューページ)
# [インデックス 15591] ファイルの概要
このコミットは、Go言語のランタイムに64ビットのアトミックな交換操作(`xchg64`)を追加するものです。特にネットワークポーラーでの利用を想定しており、並行処理におけるデータの一貫性と効率性を向上させることを目的としています。
## コミット
commit add334986778a09dce559f4d8b7ee69534abd47d Author: Dmitriy Vyukov dvyukov@google.com Date: Tue Mar 5 09:46:52 2013 +0200
runtime: add atomic xchg64
It will be handy for network poller.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7429048
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/add334986778a09dce559f4d8b7ee69534abd47d](https://github.com/golang/go/commit/add334986778a09dce559f4d8b7ee69534abd47d)
## 元コミット内容
runtime: add atomic xchg64 It will be handy for network poller.
R=golang-dev, rsc CC=golang-dev https://golang.org/cl/7429048
## 変更の背景
このコミットの主な背景は、Goランタイムにおけるネットワークポーラーの効率化と堅牢性の向上です。ネットワークポーラーは、I/O操作(ネットワーク通信など)が完了するのを待つゴルーチンを管理するGoランタイムの重要なコンポーネントです。多数のネットワーク接続を効率的に処理するためには、共有データ構造へのアクセスをアトミックに(不可分に)行う必要があります。
既存のアトミック操作では、64ビット値の交換(Exchange)を直接サポートするものが不足していた可能性があります。`xchg`(Exchange)操作は、メモリ位置の値を新しい値と交換し、元の値を返すという一連の操作をアトミックに実行します。これは、ロックを使用せずに共有状態を更新する際に非常に有用です。
特に、ネットワークポーラーのような高性能なシステムでは、ロックの取得と解放に伴うオーバーヘッドを最小限に抑えることが重要です。アトミック操作は、このようなシナリオでロックフリーなデータ構造を実装するための基盤を提供し、並行処理のパフォーマンスを向上させます。Goランタイムのネットワークポーラー(netpoller)は、ファイルディスクリプタやそれらを待機するゴルーチンに関する共有状態を効率的かつ安全に管理するために、アトミック操作を広範に利用しています。例えば、`pollDesc`構造体内の`atomicInfo`フィールドや、読み書きゴルーチンへのポインタ(`rg`, `wg`)はアトミックにアクセスされます。これらの操作は、`internal/runtime/atomic`パッケージによって提供され、プラットフォーム固有のハードウェア組み込み関数を利用して最適化されており、データ競合を防ぎながら逐次一貫性を保証します。
このコミットは、`xchg64`という64ビットのアトミック交換操作を導入することで、ネットワークポーラーがより効率的かつ安全に内部状態を更新できるようにすることを目的としています。
## 前提知識の解説
### 1. Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、システムコールインターフェースなどが含まれます。ランタイムは、GoプログラムがOSとどのように対話するか、そして並行処理がどのように実現されるかを定義する低レベルのコードで構成されています。
### 2. アトミック操作 (Atomic Operations)
アトミック操作とは、複数のCPU命令から構成される一連の操作が、他のスレッドやプロセスの干渉なしに、単一の不可分な操作として実行されることを保証するものです。これにより、並行処理環境におけるデータ競合(Data Race)を防ぎ、共有データの整合性を保つことができます。
* **不可分性**: 操作の途中で他のスレッドが割り込むことができないことを意味します。
* **メモリバリア/フェンス**: アトミック操作は通常、メモリバリアを含み、コンパイラやCPUによる命令の並べ替えを防ぎ、メモリ操作の順序を保証します。
一般的なアトミック操作には以下のようなものがあります。
* **Compare-and-Swap (CAS)**: 特定のメモリ位置の値が期待する値と一致する場合にのみ、その値を新しい値に更新します。
* **Fetch-and-Add**: メモリ位置の値を読み取り、それに値を加算し、元の値を返します。
* **Exchange (XCHG)**: メモリ位置の値を新しい値と交換し、元の値を返します。
### 3. `xchg` 命令 (Exchange Instruction)
`xchg`は、x86アーキテクチャのCPU命令で、2つのオペランドの値を交換します。メモリとレジスタ、または2つのレジスタ間で値を交換できます。特に、メモリオペランドに対して`xchg`を使用する場合、CPUは自動的にロックをかけ、その操作がアトミックであることを保証します。これにより、複数のCPUコアが同時に同じメモリ位置にアクセスしようとしても、データ競合が発生しません。
* `XCHGL`: 32ビット値を交換します。
* `XCHGQ`: 64ビット値を交換します(AMD64アーキテクチャ)。
### 4. ネットワークポーラー (Network Poller)
Goのネットワークポーラーは、ノンブロッキングI/Oを効率的に管理するためのGoランタイムのコンポーネントです。GoのネットワーキングAPI(`net`パッケージなど)は、ブロッキングI/Oのように見えますが、実際にはランタイムが内部的にノンブロッキングI/Oとイベント通知メカニズム(Linuxのepoll、macOSのkqueue、WindowsのIOCPなど)を使用して、多数の同時接続を効率的に処理しています。
ネットワークポーラーは、I/O操作が完了するのを待っているゴルーチンを一時停止し、I/Oイベントが発生したときにそれらを再開する役割を担います。このメカニズムにより、GoはC10K問題(単一サーバーで1万以上の同時接続を処理する問題)を効率的に解決できます。
ネットワークポーラーの内部では、多数のファイルディスクリプタ(ソケットなど)の状態を管理し、I/Oイベントの発生を監視する必要があります。この状態管理には、共有データ構造へのアトミックなアクセスが不可欠です。
### 5. `volatile` キーワード (C言語における)
C言語における`volatile`キーワードは、コンパイラに対して、変数がプログラムの外部(例えば、ハードウェア、他のスレッド、シグナルハンドラなど)によって変更される可能性があることを指示します。これにより、コンパイラは最適化(例えば、レジスタへのキャッシュや不要な読み書きの削除)を抑制し、常にメモリから値を読み書きするようにします。並行処理において、共有変数へのアクセスが常に最新の値で行われることを保証するために重要です。
## 技術的詳細
このコミットは、Goランタイムに`runtime·xchg64`という新しいアトミック操作を追加します。これは、64ビットの値をアトミックに交換するための関数です。
### AMD64アーキテクチャ (`asm_amd64.s`)
AMD64アーキテクチャでは、`XCHGQ`命令が64ビットのアトミック交換を直接サポートしています。この命令は、メモリ位置の値をレジスタの値と交換し、その操作がアトミックであることをCPUが保証します。
`src/pkg/runtime/asm_amd64.s`に追加されたアセンブリコードは、`runtime·xchg64`関数を実装しています。
```assembly
TEXT runtime·xchg64(SB), 7, $0
MOVQ 8(SP), BX // addr (ポインタ) を BX レジスタにロード
MOVQ 16(SP), AX // v (新しい値) を AX レジスタにロード
XCHGQ AX, 0(BX) // 0(BX) (addrが指すメモリ位置) の値と AX の値を交換
RET // AX には元の値が格納されているので、それが戻り値となる
8(SP)
と16(SP)
は、スタックフレーム上の引数(addr
とv
)にアクセスしています。XCHGQ AX, 0(BX)
が実際の64ビットアトミック交換操作を実行します。0(BX)
はBX
レジスタが指すメモリ位置を意味し、AX
レジスタの値と交換されます。交換後、AX
レジスタには交換前のメモリ位置の値が格納されており、これが関数の戻り値となります。
i386アーキテクチャ (atomic_386.c
)
i386(32ビット)アーキテクチャでは、64ビット値を直接アトミックに交換する単一の命令は存在しません。そのため、runtime·cas64
(Compare-and-Swap 64ビット)をループ内で使用してxchg64
をエミュレートしています。
uint64
runtime·xchg64(uint64 volatile* addr, uint64 v)
{
uint64 old;
old = *addr; // 現在の値を読み込む
while(!runtime·cas64(addr, &old, v)) { // CASが成功するまでループ
// nothing
}
return old; // CASが成功した時点での元の値(old)を返す
}
この実装では、まずaddr
が指す現在の値old
を読み込みます。次に、runtime·cas64
を使用して、addr
の値がold
と一致する場合にのみv
に更新しようとします。もし他のスレッドがaddr
の値を変更した場合、runtime·cas64
は失敗し、ループが繰り返されます。このループは、runtime·cas64
が成功するまで、つまりaddr
の値をアトミックにv
に更新できるまで続きます。
ARMアーキテクチャ (atomic_arm.c
)
ARMアーキテクチャでも、i386と同様に、64ビットのアトミック交換を直接サポートする単一の命令がない場合があります(特に古いARMv6以前のアーキテクチャ)。このコミットでは、ミューテックスのようなロック機構を使用してxchg64
を実装しています。
uint64
runtime·xchg64(uint64 volatile *addr, uint64 v)
{
uint64 res;
runtime·lock(LOCK(addr)); // addr に関連付けられたロックを取得
res = *addr; // 現在の値を読み込む
*addr = v; // 新しい値を書き込む
runtime·unlock(LOCK(addr)); // ロックを解放
return res; // 元の値を返す
}
この実装は、addr
が指すメモリ位置に対する排他的アクセスを保証するために、runtime·lock
とruntime·unlock
を使用しています。これは、アトミック操作をソフトウェア的に実現する一般的な方法ですが、ハードウェアによるアトミック命令に比べてオーバーヘッドが大きくなる可能性があります。
ランタイムテスト (runtime.c
)
src/pkg/runtime/runtime.c
のTestAtomic64
関数に、新しく追加されたruntime·xchg64
のテストケースが追加されています。
if(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
runtime·throw("xchg64 failed");
if(runtime·atomicload64(&z64) != (3ull<<40)+3)
runtime·throw("xchg64 failed");
このテストは、runtime·xchg64
が期待通りに値を交換し、交換後の値が正しく設定されていることを検証します。
ヘッダーファイル (runtime.h
)
src/pkg/runtime/runtime.h
にruntime·xchg64
関数のプロトタイプが追加されています。
uint64 runtime·xchg64(uint64 volatile*, uint64);
これにより、他のランタイムコードからruntime·xchg64
関数を呼び出すことができるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の5つのファイルに分散しています。
src/pkg/runtime/asm_amd64.s
: AMD64アーキテクチャ向けのアセンブリ実装。XCHGQ
命令を直接使用してruntime·xchg64
を実装しています。src/pkg/runtime/atomic_386.c
: i386アーキテクチャ向けC言語実装。runtime·cas64
をループ内で使用してruntime·xchg64
をエミュレートしています。src/pkg/runtime/atomic_arm.c
: ARMアーキテクチャ向けC言語実装。runtime·lock
とruntime·unlock
を使用してruntime·xchg64
を実装しています。src/pkg/runtime/runtime.c
:TestAtomic64
関数にruntime·xchg64
のテストケースを追加。src/pkg/runtime/runtime.h
:runtime·xchg64
関数のプロトタイプ宣言を追加。
これらの変更により、Goランタイムは異なるアーキテクチャ上で64ビットのアトミック交換操作を統一されたインターフェースで提供できるようになります。
コアとなるコードの解説
src/pkg/runtime/asm_amd64.s
--- a/src/pkg/runtime/asm_amd64.s
+++ b/src/pkg/runtime/asm_amd64.s
@@ -442,6 +442,12 @@ TEXT runtime·xchg(SB), 7, $0
XCHGL AX, 0(BX)
RET
+TEXT runtime·xchg64(SB), 7, $0
+\tMOVQ\t8(SP), BX
+\tMOVQ\t16(SP), AX
+\tXCHGQ\tAX, 0(BX)
+\tRET
+\
TEXT runtime·procyield(SB),7,$0
MOVL\t8(SP), AX
again:
このアセンブリコードは、AMD64アーキテクチャにおけるruntime·xchg64
関数の実装です。
TEXT runtime·xchg64(SB), 7, $0
:runtime·xchg64
という名前の関数を定義しています。SB
はStatic Baseレジスタ、7
はスタックフレームのサイズ、$0
は引数のサイズを示します。MOVQ 8(SP), BX
: スタックポインタSP
から8バイトオフセットの位置にある値(最初の引数addr
)をBX
レジスタに移動します。MOVQ 16(SP), AX
: スタックポインタSP
から16バイトオフセットの位置にある値(2番目の引数v
)をAX
レジスタに移動します。XCHGQ AX, 0(BX)
:BX
レジスタが指すメモリ位置(addr
が指す場所)の64ビット値とAX
レジスタの64ビット値をアトミックに交換します。交換後、AX
レジスタには交換前のメモリ位置の値が格納されます。RET
: 関数から戻ります。AX
レジスタに格納された値が関数の戻り値となります。
src/pkg/runtime/atomic_386.c
--- a/src/pkg/runtime/atomic_386.c
+++ b/src/pkg/runtime/atomic_386.c
@@ -30,3 +30,16 @@ runtime·xadd64(uint64 volatile* addr, int64 v)
}\n\treturn old+v;\n}\n+\n+#pragma textflag 7
+uint64
+runtime·xchg64(uint64 volatile* addr, uint64 v)
+{\n+\tuint64 old;\n+\n+\told = *addr;\n+\twhile(!runtime·cas64(addr, &old, v)) {\n+\t\t// nothing\n+\t}\n+\treturn old;\n+}\n```
このC言語コードは、i386アーキテクチャにおける`runtime·xchg64`関数の実装です。
* `#pragma textflag 7`: コンパイラに対する指示で、この関数のコードを特定のセクションに配置することを示します。
* `uint64 old;`: 交換前の値を保持するための変数`old`を宣言します。
* `old = *addr;`: `addr`が指す現在の値を`old`に読み込みます。
* `while(!runtime·cas64(addr, &old, v))`: `runtime·cas64`関数を呼び出し、`addr`が指す値が`old`と等しい場合にのみ、その値を`v`に更新しようとします。`runtime·cas64`は成功した場合に`true`を返します。失敗した場合は`false`を返し、ループが続行されます。ループ内で`old`の値は`runtime·cas64`によって自動的に更新され、最新のメモリ値が反映されます。
* `return old;`: `runtime·cas64`が成功した時点での`old`の値(つまり、交換前のメモリ位置の値)を返します。
### `src/pkg/runtime/atomic_arm.c`
```diff
--- a/src/pkg/runtime/atomic_arm.c
+++ b/src/pkg/runtime/atomic_arm.c
@@ -121,6 +121,19 @@ runtime·xadd64(uint64 volatile *addr, int64 delta)
return res;\n}\n \n+#pragma textflag 7
+uint64
+runtime·xchg64(uint64 volatile *addr, uint64 v)
+{\n+\tuint64 res;\n+\n+\truntime·lock(LOCK(addr));
+\tres = *addr;\n+\t*addr = v;\n+\truntime·unlock(LOCK(addr));
+\treturn res;\n+}\n+\n #pragma textflag 7
uint64
runtime·atomicload64(uint64 volatile *addr)
このC言語コードは、ARMアーキテクチャにおけるruntime·xchg64
関数の実装です。
#pragma textflag 7
: コンパイラに対する指示です。uint64 res;
: 交換前の値を保持するための変数res
を宣言します。runtime·lock(LOCK(addr));
:addr
に関連付けられたロックを取得します。これにより、このクリティカルセクションへの排他的アクセスが保証されます。res = *addr;
: ロックが取得された後、addr
が指す現在の値をres
に読み込みます。*addr = v;
:addr
が指すメモリ位置に新しい値v
を書き込みます。runtime·unlock(LOCK(addr));
: ロックを解放します。return res;
: 交換前の値res
を返します。
src/pkg/runtime/runtime.c
--- a/src/pkg/runtime/runtime.c
+++ b/src/pkg/runtime/runtime.c
@@ -156,6 +156,10 @@ TestAtomic64(void)
runtime·throw("xadd64 failed");
if(runtime·atomicload64(&z64) != (2ull<<40)+2)
runtime·throw("xadd64 failed");
+\tif(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
+\t\truntime·throw("xchg64 failed");
+\tif(runtime·atomicload64(&z64) != (3ull<<40)+3)
+\t\truntime·throw("xchg64 failed");
}\n \n void
このC言語コードは、ランタイムのテスト関数TestAtomic64
にruntime·xchg64
のテストケースを追加しています。
if(runtime·xchg64(&z64, (3ull<<40)+3) != (2ull<<40)+2)
:z64
という64ビット変数に対してruntime·xchg64
を呼び出し、(3ull<<40)+3
という新しい値と交換します。この時、runtime·xchg64
の戻り値(交換前のz64
の値)が期待値(2ull<<40)+2
と異なる場合、エラーをスローします。if(runtime·atomicload64(&z64) != (3ull<<40)+3)
:runtime·xchg64
の呼び出し後、z64
の現在の値が新しい値(3ull<<40)+3
と等しいことを確認します。異なる場合、エラーをスローします。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -691,6 +691,7 @@ bool\truntime·casp(void**, void*, void*);\n uint32\truntime·xadd(uint32 volatile*, int32);\n uint64\truntime·xadd64(uint64 volatile*, int64);\n uint32\truntime·xchg(uint32 volatile*, uint32);\n+uint64\truntime·xchg64(uint64 volatile*, uint64);\n uint32\truntime·atomicload(uint32 volatile*);\n void\truntime·atomicstore(uint32 volatile*, uint32);\n void\truntime·atomicstore64(uint64 volatile*, uint64);\n```
このヘッダーファイルへの変更は、`runtime·xchg64`関数のプロトタイプ宣言を追加するものです。
* `uint64 runtime·xchg64(uint64 volatile*, uint64);`: `runtime·xchg64`関数が、`uint64 volatile*`型のアドレスと`uint64`型の新しい値を受け取り、`uint64`型の元の値を返すことを宣言しています。これにより、他のC言語ファイルからこの関数を正しく呼び出すことができるようになります。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* Goランタイムのソースコード: [https://github.com/golang/go/tree/master/src/runtime](https://github.com/golang/go/tree/master/src/runtime)
* Goの並行処理に関するブログ記事やドキュメント(例: Go Concurrency Patterns)
## 参考にした情報源リンク
* Goのコミット履歴: [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
* Goのコードレビューシステム (Gerrit): [https://go.googlesource.com/go/+/refs/heads/master](https://go.googlesource.com/go/+/refs/heads/master)
* GoのIssue Tracker: [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
* アトミック操作に関する一般的な情報源(例: Wikipedia, CPUアーキテクチャのマニュアル)
* Goのネットワークポーラーに関する技術記事や解説
* Goの`sync/atomic`パッケージのドキュメント: [https://pkg.go.dev/sync/atomic](https://pkg.go.dev/sync/atomic) (このコミットはランタイム内部のアトミック操作に関するものですが、Go言語レベルでのアトミック操作の理解に役立ちます)
* Dmitriy Vyukov氏の他のコミットや活動(Goの並行処理とランタイムに多大な貢献をしています)
* Goの`CL`(Change List)リンク: [https://golang.org/cl/7429048](https://golang.org/cl/7429048) (このコミットの元のコードレビューページ)