[インデックス 16749] ファイルの概要
このコミットは、Goランタイムにおける386アーキテクチャ向けのビルド修正に関するものです。具体的には、cas64
(Compare And Swap 64-bit)関数の変更後に発生したビルドエラーを修正しています。
コミット
commit f70a19f085bd4cd67f57e9d99af0a1959ffef4ab
Author: Russ Cox <rsc@golang.org>
Date: Fri Jul 12 00:42:46 2013 -0400
runtime: fix 386 build after cas64 change
Missed this in CL 10909045.
TBR=golang-dev
CC=golang-dev
https://golang.org/cl/10803045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f70a19f085bd4cd67f57e9d99af0a1959ffef4ab
元コミット内容
このコミットは、以前の変更(CL 10909045)によって導入されたcas64
関数のシグネチャ変更に対応するための修正です。元のcas64
関数のシグネチャは、old
引数がポインタであったため、*old
としてその値を参照していました。しかし、CL 10909045でold
引数が値渡しに変更されたにもかかわらず、386アーキテクチャのアセンブリコードがその変更に追従していなかったため、ビルドエラーが発生していました。
変更の背景
Go言語のランタイムは、並行処理を効率的に行うためにアトミック操作を多用します。cas64
(Compare And Swap 64-bit)はそのようなアトミック操作の一つで、マルチスレッド環境での共有データの整合性を保つために不可欠です。
このコミットの背景には、cas64
関数の引数渡し方法の変更があります。以前のコミット(CL 10909045)で、runtime·cas64
関数のold
引数がuint64 *old
(ポインタ)からuint64 old
(値)に変更されました。この変更は、関数の呼び出し規約や内部実装に影響を与えます。
しかし、386アーキテクチャ向けのアセンブリコード(src/pkg/runtime/asm_386.s
)がこの変更に追従していなかったため、cas64
関数が期待する引数の位置と、実際にアセンブリコードが引数を読み込む位置との間に不整合が生じました。これにより、ビルド時にエラーが発生するか、あるいは実行時に不正な動作を引き起こす可能性がありました。
このコミットは、この不整合を解消し、386アーキテクチャ上でのGoランタイムのビルドが正しく行われるようにすることを目的としています。
前提知識の解説
- アトミック操作 (Atomic Operations): マルチスレッド環境において、複数のスレッドから同時にアクセスされる共有データに対して、不可分(atomic)な操作を行うことです。これにより、データの破損や不整合を防ぎます。Compare And Swap (CAS) は、アトミック操作の代表的なもので、特定のメモリ位置の値が期待する値と一致する場合にのみ、新しい値に更新するという操作です。
- Compare And Swap (CAS): CASは3つのオペランドを取ります。メモリ位置(V)、期待する古い値(A)、新しい値(B)です。操作は、Vの現在の値がAと等しい場合にのみ、VをBにアトミックに設定します。そうでない場合は、何も変更せず、Vの現在の値を返します。これにより、ロックを使用せずにスレッドセーフなデータ更新が可能になります。
- 386アーキテクチャ (Intel 80386 Architecture): Intelが開発した32ビットのマイクロプロセッサアーキテクチャです。Go言語は様々なアーキテクチャをサポートしており、386はその一つです。各アーキテクチャにはそれぞれ異なるアセンブリ言語やレジスタの利用規約があります。
- アセンブリ言語 (Assembly Language): コンピュータのプロセッサが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。Goランタイムのようなシステムレベルのコードでは、パフォーマンスの最適化やハードウェアへの直接アクセスが必要な場合にアセンブリ言語が使用されます。
- スタックポインタ (Stack Pointer - SP): プログラムのスタックの現在のトップを指すレジスタです。関数呼び出しの際に引数やローカル変数がスタックにプッシュされ、関数から戻る際にポップされます。引数は通常、スタックポインタからのオフセットでアクセスされます。
- レジスタ (Registers): CPU内部にある高速な記憶領域で、演算やデータ転送の際に一時的にデータを保持します。386アーキテクチャでは、
AX
,DX
,BX
,CX
,SI
,BP
などの汎用レジスタがあります。AX
(Accumulator Register): 演算結果を格納するのに使われることが多い。DX
(Data Register):AX
と組み合わせて64ビット値を扱う際などに使われる。BX
(Base Register): メモリのアドレス指定に使われることが多い。CX
(Count Register): ループカウンタなどに使われることが多い。SI
(Source Index Register): データ転送のソースアドレスに使われることが多い。BP
(Base Pointer Register): スタックフレームのベースアドレスを指す。
CMPXCHG8B
命令: Intel 386以降のプロセッサで利用可能な命令で、64ビットのCAS操作を実行します。EDX:EAX
レジスタペアに期待する値、ECX:EBX
レジスタペアに新しい値を設定し、指定されたメモリ位置の値をEDX:EAX
と比較します。一致すればメモリ位置をECX:EBX
で更新し、そうでなければメモリ位置の値をEDX:EAX
にロードします。
技術的詳細
このコミットの核心は、runtime·cas64
関数のアセンブリ実装における引数のオフセットの修正です。
元のruntime·cas64
関数のC言語でのプロトタイプがbool runtime·cas64(uint64 *val, uint64 *old, uint64 new)
であった場合、スタック上の引数の配置は以下のようになります(一般的な呼び出し規約を仮定):
val
:4(SP)
old
:8(SP)
new
:12(SP)
(下位32ビット),16(SP)
(上位32ビット)
この場合、old
の値はポインタであるため、0(SI)
や4(SI)
(SI
はold
ポインタのアドレス)を介してアクセスされます。
しかし、CL 10909045でプロトタイプがbool runtime·cas64(uint64 *val, uint64 old, uint64 new)
に変更された場合、old
が値渡しになったため、スタック上の引数の配置が変わります。
val
:4(SP)
old
:8(SP)
(下位32ビット),12(SP)
(上位32ビット)new
:16(SP)
(下位32ビット),20(SP)
(上位32ビット)
この変更により、アセンブリコードがold
とnew
の値をスタックから読み込む際のオフセットを調整する必要がありました。
具体的には、src/pkg/runtime/asm_386.s
内のTEXT runtime·cas64(SB)
セクションで、以下の変更が行われました。
-
old
引数の読み込み:- 変更前:
MOVL 8(SP), SI
(ポインタold
のアドレスをSI
にロード) - 変更後:
MOVL 8(SP), AX
(値old
の下位32ビットをAX
にロード) - 変更前:
MOVL 0(SI), AX
(ポインタold
が指す値の下位32ビットをAX
にロード) - 変更後:
MOVL 12(SP), DX
(値old
の上位32ビットをDX
にロード) これにより、AX:DX
レジスタペアにold
の値が正しくロードされるようになります。
- 変更前:
-
new
引数の読み込み:- 変更前:
MOVL 12(SP), BX
(値new
の下位32ビットをBX
にロード) - 変更後:
MOVL 16(SP), BX
(値new
の下位32ビットをBX
にロード) - 変更前:
MOVL 16(SP), CX
(値new
の上位32ビットをCX
にロード) - 変更後:
MOVL 20(SP), CX
(値new
の上位32ビットをCX
にロード) これにより、BX:CX
レジスタペアにnew
の値が正しくロードされるようになります。
- 変更前:
-
cas64_fail
ラベル内の処理:- 変更前は、CAS操作が失敗した場合に
*old = *val
という処理を行うために、AX
とDX
(*val
の現在の値)をSI
が指すアドレス(old
ポインタが指す場所)に書き戻していました。 - 変更後は、
old
が値渡しになったため、この書き戻し処理は不要となり、削除されました。old
は入力値であり、出力として変更されるものではないためです。
- 変更前は、CAS操作が失敗した場合に
また、src/pkg/runtime/atomic_386.c
では、runtime·xchg64
関数内でruntime·cas64
を呼び出す際に、old
引数に*addr
(addr
が指す値)を渡すように修正されています。これは、runtime·cas64
のold
引数が値渡しになったことに対応するものです。
コアとなるコードの変更箇所
src/pkg/runtime/asm_386.s
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -319,30 +319,26 @@ TEXT runtime·cas(SB), 7, $0
MOVL $1, AX
RET
-// bool runtime·cas64(uint64 *val, uint64 *old, uint64 new)
+// bool runtime·cas64(uint64 *val, uint64 old, uint64 new)
// Atomically:
// if(*val == *old){
// *val = new;
// return 1;
// } else {
-// *old = *val
// return 0;
// }
TEXT runtime·cas64(SB), 7, $0
MOVL 4(SP), BP
- MOVL 8(SP), SI
- MOVL 0(SI), AX
- MOVL 4(SI), DX
- MOVL 12(SP), BX
- MOVL 16(SP), CX
+ MOVL 8(SP), AX
+ MOVL 12(SP), DX
+ MOVL 16(SP), BX
+ MOVL 20(SP), CX
LOCK
CMPXCHG8B 0(BP)
JNZ cas64_fail
MOVL $1, AX
RET
cas64_fail:
- MOVL AX, 0(SI)
- MOVL DX, 4(SI)
MOVL $0, AX
RET
src/pkg/runtime/atomic_386.c
--- a/src/pkg/runtime/atomic_386.c
+++ b/src/pkg/runtime/atomic_386.c
@@ -38,7 +38,7 @@ runtime·xchg64(uint64 volatile* addr, uint64 v)
uint64 old;
do
- old = addr;
+ old = *addr;
while(!runtime·cas64(addr, old, v));
return old;
コアとなるコードの解説
src/pkg/runtime/asm_386.s
の変更点
- コメントの更新:
runtime·cas64
関数のシグネチャを示すコメントが、uint64 *old
からuint64 old
へと変更されています。これは、old
引数がポインタではなく値として渡されるようになったことを示しています。 - 引数のオフセット調整:
MOVL 8(SP), SI
がMOVL 8(SP), AX
に変更されました。これは、スタックオフセット8(SP)
からold
引数の下位32ビットをAX
レジスタに直接ロードするように変更されたことを意味します。以前は、8(SP)
にはold
ポインタのアドレスが格納されており、そのアドレスをSI
レジスタにロードしていました。MOVL 0(SI), AX
とMOVL 4(SI), DX
が削除されました。これらは、SI
が指すアドレス(old
ポインタが指す場所)からold
の値(64ビット)を読み込むための命令でした。old
が値渡しになったため、スタックから直接読み込むようになり、これらの間接参照は不要になりました。MOVL 12(SP), BX
がMOVL 16(SP), BX
に、MOVL 16(SP), CX
がMOVL 20(SP), CX
に変更されました。これは、new
引数の下位32ビットと上位32ビットをスタックから読み込む際のオフセットが、それぞれ4バイトずつ増加したことを意味します。これは、old
引数がポインタから値に変わったことで、スタック上の引数の配置がずれたためです。
cas64_fail
ラベル内の処理の削除:MOVL AX, 0(SI)
とMOVL DX, 4(SI)
が削除されました。これらの命令は、CMPXCHG8B
命令が失敗した場合に、現在の*val
の値を*old
に書き戻すためのものでした。しかし、old
が値渡しになったことで、old
は入力値であり、関数内で変更されるべきではないため、この処理は不要になりました。
src/pkg/runtime/atomic_386.c
の変更点
runtime·xchg64
関数内のdo-while
ループ内で、old = addr;
がold = *addr;
に変更されました。- 変更前は、
addr
(uint64 volatile*
型)を直接old
(uint64
型)に代入しようとしていました。これは型が一致しないため、コンパイルエラーになるか、あるいは不正な値が代入される可能性がありました。 - 変更後は、
*addr
とすることで、addr
が指すメモリ位置のuint64
型の値をold
に代入しています。これにより、runtime·cas64
関数に渡されるold
引数が、期待される値(現在の*addr
の値)となるように修正されました。これは、runtime·cas64
のold
引数が値渡しになったことに対応するものです。
- 変更前は、
これらの変更により、386アーキテクチャにおけるcas64
関数のアセンブリ実装と、それを利用するCコードが、新しいcas64
関数のシグネチャに正しく対応し、ビルドおよび実行時の整合性が保たれるようになりました。
関連リンク
- Go言語のランタイムに関するドキュメントやソースコード: https://golang.org/src/runtime/
- Go言語のアトミック操作に関するドキュメント: https://pkg.go.dev/sync/atomic
- Intel 386アーキテクチャのアセンブリ命令セットに関する情報
参考にした情報源リンク
- https://golang.org/cl/10803045 (このコミットのChange List)
- https://golang.org/cl/10909045 (このコミットが参照している、
cas64
変更の元のChange List) - Go言語のソースコード (特に
src/pkg/runtime/asm_386.s
とsrc/pkg/runtime/atomic_386.c
) - Intel 386プロセッサの命令セットリファレンス (特に
CMPXCHG8B
命令に関する情報) - Go言語の呼び出し規約に関する情報 (アセンブリコードの理解に必要)
- アトミック操作とCASに関する一般的なコンピュータサイエンスの知識