Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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はアトミック操作の一種で、以下のロジックを不可分に実行します。

  1. メモリ上の特定のアドレスにある現在の値(current)を読み込む。
  2. 読み込んだcurrentが、期待する値(old)と一致するかどうかを比較する。
  3. もし一致すれば、そのアドレスの値を新しい値(new)に更新する。
  4. 操作が成功したかどうか(値が更新されたかどうか)を示すブール値を返す。

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ビット値を表現します。例えば、R0R1を組み合わせて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実装)がこの変更に対応しています。

  1. シンボル名の変更: generalCAS64<>(SB)から·generalCAS64(SB)へ変更されています。Goのアセンブリでは、Goの関数や変数を示すために·(中点)が使われます。これは、この関数がGoのリンケージ規約に従うことをより明確にするための変更です。
  2. 引数アクセスの変更:
    • 変更前はMOVW $4(FP), R1 // oldvalのように、フレームポインタ(FP)からのオフセットでoldポインタをロードしていました。
    • 変更後はMOVW oldlo+4(FP), R1MOVW oldhi+8(FP), R1のように、old値の低位(lo)と高位(hi)部分をそれぞれ異なるオフセットからロードしています。これは、oldがポインタではなく、スタック上に直接配置された64ビット値として扱われていることを示しています。
  3. スタックフレームサイズの調整: TEXT generalCAS64<>(SB),NOSPLIT,$20-21TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$-21のスタックフレームサイズが調整されています。これは、引数の渡し方が変わったことで、関数が使用するスタック領域のサイズが変わったためです。

また、この変更に伴い、テストコードも更新されています。atomic_test.goでは、TestCompareAndSwapUint64関数が汎用的なtestCompareAndSwapUint64関数にリファクタリングされ、CAS操作の実装を引数として受け取れるようになりました。これにより、ARM固有のGeneralCAS64実装をテストするための新しいファイルatomic_linux_arm_test.goが追加され、export_linux_arm_test.gogeneralCAS64がテスト用にエクスポートされています。

コアとなるコードの変更箇所

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), R1MOVW 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), R2newhi+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-21TEXT ·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により、generalCAS64GeneralCAS64という名前でエクスポートされ、atomic_linux_arm_test.goからアクセスできるようになります。

これらの変更は、Goランタイムの進化に合わせて、低レベルなアトミック操作の実装が適切に更新され、その正確性がテストによって保証されていることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(GitHub): https://github.com/golang/go
  • Go CL (Change List) 13859043: https://golang.org/cl/13859043 (コミットメッセージに記載されているCLへのリンク)
  • ARM Architecture Reference Manual (特定のバージョンは不明だが、64ビット値の扱いに関する一般的な情報源)
  • アトミック操作とCASに関する一般的なコンピュータサイエンスの資料