[インデックス 17694] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージにおけるARMアーキテクチャ向けのアトミック操作の実装を更新するものです。具体的には、Goランタイム内部の64ビット比較交換(CompareAndSwap, CAS)操作であるruntime.cas64
関数のプロトタイプ(引数の型)が変更されたことに対応しています。この変更により、アセンブリコードと関連するテストコードが修正されています。
コミット
commit 828a4b93765c87a96578c1aaa3b0781d3d4e31be
Author: Russ Cox <rsc@golang.org>
Date: Tue Sep 24 15:54:48 2013 -0400
sync/atomic: adjust for new runtime.cas64 prototype
R=golang-dev, minux.ma, josharian
CC=golang-dev
https://golang.org/cl/13859043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/828a4b93765c87a96578c1aaa3b0781d3d4e31be
元コミット内容
sync/atomic: adjust for new runtime.cas64 prototype
R=golang-dev, minux.ma, josharian
CC=golang-dev
https://golang.org/cl/13859043
変更の背景
Go言語のランタイムは、様々なアーキテクチャ上で効率的な並行処理を実現するために、低レベルのアトミック操作を内部的に提供しています。sync/atomic
パッケージは、これらのランタイムプリミティブをGoのユーザーが利用できるようにラップしています。
このコミットの背景には、runtime.cas64
という64ビット値に対する比較交換操作の内部プロトタイプ(関数の引数リスト)が変更されたことがあります。以前のプロトタイプでは、比較対象の「古い値」をポインタで受け取っていた可能性がありますが、新しいプロトタイプでは値を直接受け取るように変更されました。このような変更は、ランタイムの最適化、特定のアーキテクチャでの呼び出し規約の改善、またはコードの簡素化を目的として行われることがあります。
特にARMのような32ビットアーキテクチャでは、64ビット値を扱う際に特別な考慮が必要です。64ビット値は通常、2つの32ビットレジスタに分割して扱われるため、引数の渡し方がポインタから値に変わると、アセンブリコードレベルでのレジスタの割り当てやメモリからの値のロード方法が大きく変わります。このコミットは、そのランタイムの変更にsync/atomic
パッケージのARMアセンブリ実装が追従するためのものです。
前提知識の解説
アトミック操作 (Atomic Operations)
アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされた場合でも、その操作全体が不可分(atomic)であることを保証する操作です。つまり、操作の途中で他のスレッドから割り込まれることがなく、常に一貫性のある結果が得られます。並行プログラミングにおいて、共有データの一貫性を保つために不可欠な要素です。
比較交換 (Compare-And-Swap, CAS)
CASはアトミック操作の一種で、以下のロジックを不可分に実行します。
- メモリ上の特定のアドレスにある現在の値(
current
)を読み込む。 - 読み込んだ
current
が、期待する値(old
)と一致するかどうかを比較する。 - もし一致すれば、そのアドレスの値を新しい値(
new
)に更新する。 - 操作が成功したかどうか(値が更新されたかどうか)を示すブール値を返す。
CASはロックフリーなデータ構造やアルゴリズムを実装する際の基本的な構成要素です。
Go言語のsync/atomic
パッケージ
Goの標準ライブラリであるsync/atomic
パッケージは、ミューテックスなどのロック機構を使わずに、共有変数に対するアトミックな操作(加算、減算、ロード、ストア、比較交換など)を提供します。これにより、競合状態(race condition)を避けつつ、高い並行性を実現できます。
ARMアーキテクチャと64ビット値
ARMは主にモバイルデバイスや組み込みシステムで広く使われているRISC(Reduced Instruction Set Computer)アーキテクチャです。多くのARMプロセッサは32ビットアーキテクチャですが、64ビットのデータ型(例: uint64
)をサポートしています。32ビットのレジスタしか持たないCPUで64ビット値を扱う場合、通常は2つの32ビットレジスタを組み合わせて64ビット値を表現します。例えば、R0
とR1
を組み合わせて64ビット値を渡すといった規約が用いられます。
Goのアセンブリ(Plan 9 Assembly)
Go言語は、独自のPlan 9アセンブリ構文を使用しています。これは一般的なx86アセンブリとは異なる記法を持ち、Goランタイムの低レベルな部分や、特定のアーキテクチャに最適化されたコード(例えばアトミック操作)の実装に用いられます。
技術的詳細
このコミットの核心は、runtime.cas64
関数の呼び出し規約の変更にあります。
変更前(推測):
bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
このプロトタイプでは、old
値はuint64
へのポインタとして渡されます。つまり、関数内部でold
の値を参照するために、ポインタをデリファレンス(間接参照)する必要がありました。
変更後:
bool runtime·cas64(uint64 volatile *addr, uint64 old, uint64 new)
このプロトタイプでは、old
値はuint64
の「値そのもの」として渡されます。これにより、関数内部で直接old
値を使用でき、ポインタのデリファレンスが不要になります。
ARM 32ビットアーキテクチャにおいて、64ビットの引数を値渡しする場合、通常は2つの32ビットレジスタ(例えば、old
の低位32ビットがR1
、高位32ビットがR2
など)に分割して渡されます。ポインタ渡しから値渡しへの変更は、アセンブリコードが引数をスタックからロードする方法、またはレジスタに配置する方法に直接影響を与えます。
具体的には、asm_linux_arm.s
内のgeneralCAS64
関数(GoのCompareAndSwapUint64
が内部的に利用する汎用CAS64実装)がこの変更に対応しています。
- シンボル名の変更:
generalCAS64<>(SB)
から·generalCAS64(SB)
へ変更されています。Goのアセンブリでは、Goの関数や変数を示すために·
(中点)が使われます。これは、この関数がGoのリンケージ規約に従うことをより明確にするための変更です。 - 引数アクセスの変更:
- 変更前は
MOVW $4(FP), R1 // oldval
のように、フレームポインタ(FP
)からのオフセットでold
ポインタをロードしていました。 - 変更後は
MOVW oldlo+4(FP), R1
とMOVW oldhi+8(FP), R1
のように、old
値の低位(lo
)と高位(hi
)部分をそれぞれ異なるオフセットからロードしています。これは、old
がポインタではなく、スタック上に直接配置された64ビット値として扱われていることを示しています。
- 変更前は
- スタックフレームサイズの調整:
TEXT generalCAS64<>(SB),NOSPLIT,$20-21
やTEXT ·CompareAndSwapUint64(SB),NOSPLIT,$-21
のスタックフレームサイズが調整されています。これは、引数の渡し方が変わったことで、関数が使用するスタック領域のサイズが変わったためです。
また、この変更に伴い、テストコードも更新されています。atomic_test.go
では、TestCompareAndSwapUint64
関数が汎用的なtestCompareAndSwapUint64
関数にリファクタリングされ、CAS操作の実装を引数として受け取れるようになりました。これにより、ARM固有のGeneralCAS64
実装をテストするための新しいファイルatomic_linux_arm_test.go
が追加され、export_linux_arm_test.go
でgeneralCAS64
がテスト用にエクスポートされています。
コアとなるコードの変更箇所
src/pkg/sync/atomic/asm_linux_arm.s
--- a/src/pkg/sync/atomic/asm_linux_arm.s
+++ b/src/pkg/sync/atomic/asm_linux_arm.s
@@ -121,27 +121,32 @@ TEXT kernelCAS64<>(SB),NOSPLIT,$0-21
MOVW R0, 20(FP)
RET
-TEXT generalCAS64<>(SB),NOSPLIT,$20-21
- // bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
+TEXT ·generalCAS64(SB),NOSPLIT,$20-21
+ // bool runtime·cas64(uint64 volatile *addr, uint64 old, uint64 new)
MOVW addr+0(FP), R0
+ // trigger potential paging fault here,
+ // because a fault in runtime.cas64 will hang.
+ MOVW (R0), R2
// make unaligned atomic access panic
AND.S $7, R0, R1
BEQ 2(PC)
MOVW R1, (R1)
MOVW R0, 4(R13)
- MOVW $4(FP), R1 // oldval
+ MOVW oldlo+4(FP), R1
MOVW R1, 8(R13)
+ MOVW oldhi+8(FP), R1
+ MOVW R1, 12(R13)
MOVW newlo+12(FP), R2
- MOVW R2, 12(R13)
+ MOVW R2, 16(R13)
MOVW newhi+16(FP), R3
- MOVW R3, 16(R13)
+ MOVW R3, 20(R13)
BL runtime·cas64(SB)
- MOVW R0, 20(FP)
+ MOVB R0, ret+20(FP)
RET
GLOBL armCAS64(SB), $4
-TEXT setupAndCallCAS64<>(SB),NOSPLIT,$-21
+TEXT setupAndCallCAS64<>(SB),NOSPLIT,$-4-21
MOVW $0xffff0ffc, R0 // __kuser_helper_version
MOVW (R0), R0
// __kuser_cmpxchg64 only present if helper version >= 5
@@ -156,10 +161,10 @@ TEXT setupAndCallCAS64<>(SB),NOSPLIT,$-21
MOVW.CS R1, armCAS64(SB)
MOVW.CS R1, PC
// we are out of luck, can only use runtime's emulated 64-bit cas
- MOVW $generalCAS64<>(SB), R1
+ MOVW $·generalCAS64(SB), R1
MOVW R1, armCAS64(SB)
MOVW R1, PC
TEXT ·CompareAndSwapInt64(SB),NOSPLIT,$0
B ·CompareAndSwapUint64(SB)
-TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$-21
+TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$-4-21
MOVW armCAS64(SB), R0
CMP $0, R0
MOVW.NE R0, PC
src/pkg/sync/atomic/atomic_linux_arm_test.go
(新規ファイル)
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package atomic_test
import (
. "sync/atomic"
"testing"
)
func TestGeneralCAS64(t *testing.T) {
testCompareAndSwapUint64(t, GeneralCAS64)
}
src/pkg/sync/atomic/atomic_test.go
--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -379,7 +379,7 @@ func TestCompareAndSwapInt64(t *testing.T) {
}
}
-func TestCompareAndSwapUint64(t *testing.T) {
+func testCompareAndSwapUint64(t *testing.T, cas func(*uint64, uint64, uint64) bool) {
if test64err != nil {
t.Skipf("Skipping 64-bit tests: %v", test64err)
}
@@ -392,14 +392,14 @@ func TestCompareAndSwapUint64(t *testing.T) {
x.after = magic64
for val := uint64(1); val+val > val; val += val {
x.i = val
- if !CompareAndSwapUint64(&x.i, val, val+1) {
+ if !cas(&x.i, val, val+1) {
t.Fatalf("should have swapped %#x %#x", val, val+1)
}
if x.i != val+1 {
t.Fatalf("wrong x.i after swap: x.i=%#x val+1=%#x", x.i, val+1)
}
x.i = val + 1
- if CompareAndSwapUint64(&x.i, val, val+2) {
+ if cas(&x.i, val, val+2) {
t.Fatalf("should not have swapped %#x %#x", val, val+2)
}
if x.i != val+1 {
@@ -411,6 +411,10 @@ func TestCompareAndSwapUint64(t *testing.T) {
}
}
+func TestCompareAndSwapUint64(t *testing.T) {
+ testCompareAndSwapUint64(t, CompareAndSwapUint64)
+}
+
func TestCompareAndSwapUintptr(t *testing.T) {
var x struct {
before uintptr
src/pkg/sync/atomic/export_linux_arm_test.go
(新規ファイル)
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package atomic
func generalCAS64(*uint64, uint64, uint64) bool
var GeneralCAS64 = generalCAS64
コアとなるコードの解説
src/pkg/sync/atomic/asm_linux_arm.s
の変更点
generalCAS64
関数のプロトタイプ変更への対応:- コメント行が
// bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
から// bool runtime·cas64(uint64 volatile *addr, uint64 old, uint64 new)
に変更され、old
引数がポインタから値に変わったことを明示しています。 MOVW oldlo+4(FP), R1
とMOVW oldhi+8(FP), R1
が追加されました。これは、スタックフレームポインタFP
からのオフセット+4
と+8
に、それぞれ64ビット値old
の低位(oldlo
)と高位(oldhi
)部分が格納されていることを示しています。ARM 32ビットアーキテクチャでは、64ビット値は通常2つの32ビットワードとして扱われるため、このように2つのロード命令で取得します。以前のMOVW $4(FP), R1 // oldval
は、old
がポインタであったため、そのポインタ値をロードしていたのに対し、新しいコードはold
の値を直接ロードしています。- 同様に、
new
値のロードもnewlo+12(FP), R2
とnewhi+16(FP), R3
に変更され、64ビット値の低位と高位をそれぞれロードしています。 BL runtime·cas64(SB)
は、Goランタイムの実際のcas64
関数を呼び出しています。この呼び出し規約が変更されたため、引数の準備方法が変わったわけです。- 関数の戻り値(
bool
)を格納する命令がMOVW R0, 20(FP)
からMOVB R0, ret+20(FP)
に変更されました。bool
は1バイトで表現されるため、MOVB
(Move Byte)がより適切です。
- コメント行が
- スタックフレームサイズの調整:
TEXT generalCAS64<>(SB),NOSPLIT,$20-21
やTEXT ·CompareAndSwapUint64(SB),NOSPLIT,$-21
のスタックフレームサイズが調整されています。これは、引数の渡し方がポインタから値に変わったことで、関数が使用するスタック領域のサイズが変わったためです。
src/pkg/sync/atomic/atomic_linux_arm_test.go
の追加
- このファイルは、ARM Linux環境における
sync/atomic
パッケージのテストを目的としています。 TestGeneralCAS64
関数が定義されており、これはtestCompareAndSwapUint64
ヘルパー関数を呼び出し、引数としてGeneralCAS64
を渡しています。これにより、ARM固有のgeneralCAS64
アセンブリ実装が正しく動作するかどうかを検証します。
src/pkg/sync/atomic/atomic_test.go
の変更点
TestCompareAndSwapUint64
関数がtestCompareAndSwapUint64
というヘルパー関数にリファクタリングされました。このヘルパー関数は、CAS操作を実行する関数を引数cas func(*uint64, uint64, uint64) bool
として受け取るようになりました。これにより、異なるCAS実装(例えば、汎用的なGo実装とARM固有のアセンブリ実装)を同じテストロジックで検証できるようになります。- 元の
TestCompareAndSwapUint64
は、この新しいヘルパー関数を呼び出し、Go標準のCompareAndSwapUint64
関数を渡すように変更されました。
src/pkg/sync/atomic/export_linux_arm_test.go
の追加
- このファイルは、テスト目的で内部的な
generalCAS64
関数をエクスポートしています。 func generalCAS64(*uint64, uint64, uint64) bool
は、generalCAS64
関数のGo言語でのシグネチャを宣言しています。これは、アセンブリで実装された関数をGoコードから呼び出すための宣言です。var GeneralCAS64 = generalCAS64
により、generalCAS64
がGeneralCAS64
という名前でエクスポートされ、atomic_linux_arm_test.go
からアクセスできるようになります。
これらの変更は、Goランタイムの進化に合わせて、低レベルなアトミック操作の実装が適切に更新され、その正確性がテストによって保証されていることを示しています。
関連リンク
- Go言語の
sync/atomic
パッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語のアセンブリについて: https://go.dev/doc/asm
参考にした情報源リンク
- Go言語のソースコード(GitHub): https://github.com/golang/go
- Go CL (Change List) 13859043: https://golang.org/cl/13859043 (コミットメッセージに記載されているCLへのリンク)
- ARM Architecture Reference Manual (特定のバージョンは不明だが、64ビット値の扱いに関する一般的な情報源)
- アトミック操作とCASに関する一般的なコンピュータサイエンスの資料