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

[インデックス 17225] ファイルの概要

このコミットは、Go言語のsync/atomicパッケージにおけるARM Linuxアーキテクチャ向けのswap操作のバグ修正に関するものです。具体的には、src/pkg/sync/atomic/64bit_arm.gosrc/pkg/sync/atomic/asm_linux_arm.sの2つのファイルが変更されています。これらのファイルは、ARMプロセッサ上で64ビットのアトミック操作を効率的かつ正確に実行するためのGoランタイムの低レベルな実装を扱っています。

コミット

commit 883530c019f06d557f82707d35f7ee363ff12637
Author: Russ Cox <rsc@golang.org>
Date:   Wed Aug 14 00:50:47 2013 -0400

    sync/atomic: fix new swap on arm linux
    
    TBR=dvyukov
    CC=golang-dev
    https://golang.org/cl/12920043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/883530c019f06d557f82707d35f7ee363ff12637

元コミット内容

sync/atomic: fix new swap on arm linux

TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12920043

変更の背景

このコミットは、Go言語のsync/atomicパッケージにおけるARM Linux環境でのswap操作に存在するバグを修正するために行われました。sync/atomicパッケージは、複数のゴルーチン(Goの軽量スレッド)が共有データに同時にアクセスする際に、データ競合を防ぎ、正しく同期された操作を保証するためのプリミティブを提供します。

特に、swap(交換)操作は、メモリ位置の値を新しい値とアトミックに交換し、元の値を返すという重要な機能です。このコミットメッセージにある「new swap」という記述から、この修正が行われる少し前に、ARM Linux向けのアトミックなswap操作の実装が新しく導入されたか、あるいは既存の実装が変更されたことが示唆されます。その新しい実装に、特定の条件下で誤動作を引き起こすバグが含まれていたため、この修正が必要となりました。

アトミック操作は、並行プログラミングにおいて非常に重要であり、その正確性はシステムの安定性とデータの整合性に直結します。そのため、このようなバグは速やかに特定され、修正される必要がありました。

前提知識の解説

Go言語のsync/atomicパッケージ

sync/atomicパッケージは、Go言語で並行処理を行う際に、共有変数へのアトミックな操作(不可分な操作)を提供する標準ライブラリです。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、その操作が中断されることなく、単一の不可分なステップとして完了することを保証します。これにより、データ競合(data race)を防ぎ、プログラムの正確性を保ちます。

このパッケージは、主に以下の種類のアトミック操作を提供します。

  • Add: 指定された値にアトミックに加算する。
  • Load: 指定されたメモリ位置からアトミックに値を読み込む。
  • Store: 指定されたメモリ位置にアトミックに値を書き込む。
  • Swap: 指定されたメモリ位置の値を新しい値とアトミックに交換し、元の値を返す。
  • CompareAndSwap (CAS): 指定されたメモリ位置の値が期待する値と一致する場合にのみ、新しい値にアトミックに交換する。成功した場合はtrue、失敗した場合はfalseを返す。

これらの操作は、ミューテックス(sync.Mutex)のようなロック機構を使用するよりも、特定のシナリオではより効率的です。

アトミック操作(特にSwapCompareAndSwap

  • Swap (交換): Swap(addr, new)は、addrが指すメモリ位置の現在の値をnewに設定し、そのメモリ位置にあった元の値を返します。この操作全体がアトミックに行われるため、他のゴルーチンが同時にaddrにアクセスしても、中途半端な状態が観測されることはありません。

  • CompareAndSwap (CAS) (比較と交換): CompareAndSwap(addr, old, new)は、addrが指すメモリ位置の現在の値がold(期待する値)と等しい場合にのみ、その値をnewに設定します。この操作もアトミックに行われます。値が交換された場合はtrueを返し、そうでない場合はfalseを返します。CASは、ロックフリーなデータ構造やアルゴリズムを実装する際の基本的な構成要素です。多くの複雑なアトミック操作は、CASをループ内で繰り返し試行することで実装されます。

ARMアーキテクチャにおけるアトミック操作の特性

ARMアーキテクチャでは、アトミック操作を実装するために、通常、ロード・エクスクルーシブ(LDREX)とストア・エクスクルーシブ(STREX)という命令のペアを使用します。

  • LDREX (Load-Exclusive): 指定されたメモリ位置から値を読み込み、そのメモリ位置に対する「排他的アクセス」をマークします。
  • STREX (Store-Exclusive): 指定されたメモリ位置に値を書き込もうとします。この書き込みは、対応するLDREX命令以降、他のプロセッサやコアが同じメモリ位置に書き込みを行っていない場合にのみ成功します。成功した場合は特定のレジスタに0を、失敗した場合は非ゼロの値を返します。

これらの命令を組み合わせて、CASのようなアトミック操作を実装します。例えば、CASはLDREXで値を読み込み、期待する値と比較し、一致すればSTREXで新しい値を書き込もうとします。STREXが失敗した場合は、操作全体を再試行します。

Goのアセンブリ

Go言語は、パフォーマンスが重要な部分や、特定のハードウェア機能にアクセスする必要がある場合に、Goアセンブリ言語(Plan 9アセンブリに似た構文を持つ)を使用してコードを記述することをサポートしています。src/pkg/sync/atomic/asm_linux_arm.sのようなファイルは、Goのランタイムや標準ライブラリの低レベルな部分で、Goのコードから直接呼び出されるアトミック操作などのプリミティブを実装するために使用されます。

Goアセンブリでは、レジスタの利用、メモリへのアクセス、関数呼び出しなどが直接記述されます。このコミットでは、ARMプロセッサのレジスタ(R0, R1, R2, R4など)が直接操作されており、アトミック操作の内部的な挙動を理解するためには、これらのレジスタの役割と命令のセマンティクスを理解する必要があります。

技術的詳細

このコミットの技術的な核心は、ARM Linux環境におけるswapUint64関数の実装、特にその内部でCompareAndSwapUint64(CAS)を使用する際のレジスタの取り扱いに関するバグの修正です。

swapUint64は、addrが指すuint64の値をnewにアトミックに交換し、元の値をoldとして返すことを目的としています。一般的なSwap操作は、CASをループ内で使用して実装されることが多いです。つまり、「現在の値を読み込み、その値がまだ変わっていなければ新しい値に交換する」という試行を、成功するまで繰り返します。

問題は、src/pkg/sync/atomic/asm_linux_arm.s内のアセンブリコード、特にcas(CompareAndSwap)関数の呼び出しとレジスタの使用方法にありました。元のコードでは、swaploop1内でMOVW 0(R2), R0によってメモリから値を読み込み、その値をR0レジスタに格納していました。その後、BL cas<>(SB)によってcas関数が呼び出されます。

ここで重要なのは、ARMのABI(Application Binary Interface)やGoのアセンブリ規約において、関数呼び出し(BL命令)によってR0レジスタの内容が変更される可能性があるという点です。特に、cas関数がその結果や一時的な値をR0に書き込む場合、cas呼び出し前のR0に格納されていた「古い値」(old)が上書きされてしまいます。

元のコードでは、cas呼び出し後にMOVW R0, old+8(FP)として、cas呼び出し後のR0の値をoldとして返そうとしていました。しかし、casR0を「破壊」する(cas smashes R0というコメントが示唆するように)ため、このR0には期待する「古い値」ではなく、casの戻り値や副作用による値が入ってしまっていました。これにより、swapUint64が正しく元の値を返さないというバグが発生していました。

修正は、cas呼び出しによってR0が破壊される前に、R0の値を別のレジスタ(R4)に退避させることで行われました。これにより、cas呼び出し後も、退避しておいた正しい「古い値」をoldとして返すことができるようになりました。

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

src/pkg/sync/atomic/64bit_arm.go

--- a/src/pkg/sync/atomic/64bit_arm.go
+++ b/src/pkg/sync/atomic/64bit_arm.go
@@ -37,7 +37,7 @@ func addUint64(val *uint64, delta uint64) (new uint64) {
 
 func swapUint64(addr *uint64, new uint64) (old uint64) {
 	for {
-		old := *addr
+		old = *addr
 		if CompareAndSwapUint64(addr, old, new) {
 			break
 		}

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
@@ -88,9 +88,10 @@ TEXT ·SwapUint32(SB),NOSPLIT,$0-12
 	MOVW	new+4(FP), R1
 swaploop1:
 	MOVW	0(R2), R0
+	MOVW	R0, R4 // cas smashes R0
 	BL	cas<>(SB)
 	BCC	swaploop1
-	MOVW	R0, old+8(FP)
+	MOVW	R4, old+8(FP)
 	RET
 
 TEXT ·SwapUintptr(SB),NOSPLIT,$0

コアとなるコードの解説

src/pkg/sync/atomic/64bit_arm.goの変更

このファイルでは、swapUint64関数の内部でold変数の宣言方法が変更されています。

  • 変更前: old := *addr
  • 変更後: old = *addr

この変更は、old変数がループの各イテレーションで再宣言されるのではなく、関数のスコープで一度宣言され、ループ内で値が再代入されるようにしています。Go言語では、:=は短い変数宣言演算子であり、変数を宣言し、初期値を代入します。=は代入演算子です。この変更自体は、直接的なバグ修正というよりは、コードのスタイルや、コンパイラが生成するアセンブリコードのわずかな最適化に関連している可能性があります。しかし、本質的なバグはアセンブリコード側にありました。

src/pkg/sync/atomic/asm_linux_arm.sの変更

このファイルは、ARM Linux向けのアトミック操作の低レベルなアセンブリ実装を含んでいます。特にTEXT ·SwapUint32(SB)セクション(これはSwapUint64の内部で呼び出される可能性のあるcasの実装に関連しているか、あるいは同様のロジックがSwapUint64のアセンブリにも適用されることを示唆しています)に重要な変更があります。

  • 変更前:

    swaploop1:
    	MOVW	0(R2), R0
    	BL	cas<>(SB)
    	BCC	swaploop1
    	MOVW	R0, old+8(FP)
    
  • 変更後:

    swaploop1:
    	MOVW	0(R2), R0
    	MOVW	R0, R4 // cas smashes R0
    	BL	cas<>(SB)
    	BCC	swaploop1
    	MOVW	R4, old+8(FP)
    

この変更の核心は以下の2行です。

  1. MOVW R0, R4 // cas smashes R0

    • MOVW 0(R2), R0でメモリから読み込んだ値(交換前の値、つまり返すべきoldの値)がR0レジスタに格納されます。
    • その直後に、このR0の値をR4レジスタにコピーしています。
    • コメント// cas smashes R0が非常に重要です。これは、次に呼び出されるcas関数(CompareAndSwapの実装)が、R0レジスタの内容を破壊する(上書きする)ことを示しています。cas関数は、その結果(成功/失敗)やその他の内部的な目的のためにR0を使用する可能性があります。この行は、cas呼び出しによってR0が上書きされる前に、元の値をR4に「退避」させています。
  2. MOVW R4, old+8(FP)

    • BL cas<>(SB)cas関数が呼び出され、アトミックな比較と交換が試行されます。
    • BCC swaploop1は、casが失敗した場合(キャリーフラグがクリアされている場合)にループを再試行します。
    • casが成功し、ループを抜けた後、oldとして返す値は、cas呼び出しによって破壊されたR0ではなく、事前にR4に退避しておいた正しい元の値です。この行は、R4に保存されていた値を、関数の戻り値であるold(スタックフレーム上のold+8(FP)の位置)に格納しています。

このアセンブリレベルの修正により、swapUint64関数が、アトミックな交換操作の成功後に、正しく交換前の値を返すことが保証されるようになりました。これは、並行プログラミングにおけるアトミック操作の正確性を維持するために不可欠な修正です。

関連リンク

参考にした情報源リンク

  • Go言語のsync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic
  • ARMアーキテクチャのリファレンスマニュアル(LDREX/STREX命令に関する情報)
  • Goアセンブリに関するドキュメントやチュートリアル(例: Goの公式ドキュメントの「Go Assembly Language」セクション)
  • Go言語のソースコード(特にsrc/runtimesrc/sync/atomicディレクトリ)
  • Go言語のIssueトラッカーやメーリングリスト(関連するバグ報告や議論がある場合)