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

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

このコミットは、Go言語の sync/atomic パッケージにおいて、NetBSD/ARM (ARM11以降) 環境でのアトミック操作をサポートするためのアセンブリコードを追加するものです。具体的には、src/pkg/sync/atomic/asm_netbsd_arm.s という新しいファイルが追加され、32ビットおよび64ビットの整数、ポインタに対するアトミックなロード、ストア、比較交換、加算操作の実装が含まれています。

コミット

commit 4d8b1feb7988d21dcf07219250e972bfbc02020a
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Wed Feb 13 01:04:13 2013 +0800

    sync/atomic: support NetBSD/ARM (ARM11 or above)
    
    R=golang-dev, rsc, dave
    CC=golang-dev
    https://golang.org/cl/7287044

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

https://github.com/golang/go/commit/4d8b1feb7988d21dcf07219250e972bfbc02020a

元コミット内容

このコミットは、Go言語の標準ライブラリである sync/atomic パッケージに、NetBSDオペレーティングシステム上で動作するARMアーキテクチャ(具体的にはARM11プロセッサ以降)向けの低レベルなアトミック操作の実装を追加します。これにより、NetBSD/ARM環境でGoプログラムが共有メモリを安全かつ効率的に操作できるようになります。コミットメッセージには、レビュー担当者(R=)とCC(CC=)のリスト、および関連するGo Change List (CL) へのリンクが含まれています。

変更の背景

Go言語は、その並行処理モデルと効率的なランタイムで知られています。sync/atomic パッケージは、ミューテックスのような高レベルな同期プリミティブを使用せずに、共有変数に対するアトミックな操作(不可分な操作)を提供します。これは、複数のゴルーチンが同時に同じデータにアクセスする際に、データ競合を防ぎ、正確性を保証するために不可欠です。

Go言語はクロスプラットフォーム対応を目指しており、様々なオペレーティングシステムとCPUアーキテクチャの組み合わせをサポートしています。NetBSDは、その移植性の高さで知られるUNIX系OSであり、ARMアーキテクチャは組み込みシステムからモバイルデバイス、サーバーまで幅広く利用されています。

このコミット以前は、NetBSD/ARM環境においてGoの sync/atomic パッケージが適切に機能するための特定の実装が存在しなかったと考えられます。アトミック操作は通常、CPUの特定の命令セット(例: ARMのLDREX/STREX命令)に依存するため、各アーキテクチャとOSの組み合わせに対して専用のアセンブリコードが必要となります。この変更は、NetBSD/ARM環境でのGoの安定性とパフォーマンスを向上させるために、このギャップを埋めることを目的としています。特に、コメントにある「TODO(minux): this only supports ARMv6K or higher.」という記述から、この実装がARMv6K以降のアーキテクチャに特化していることがわかります。ARMv6Kは、マルチプロセッシングとメモリ管理の強化を目的としたARMv6アーキテクチャの拡張であり、アトミック操作に必要な排他ロード/ストア命令(LDREX/STREX)を導入しています。

前提知識の解説

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

sync/atomic パッケージは、Go言語において、ミューテックスやチャネルといった高レベルな同期メカニズムを使用せずに、共有メモリ上のプリミティブ型(整数やポインタ)に対して不可分な操作(アトミック操作)を実行するための機能を提供します。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生しないことを保証します。これにより、カウンターのインクリメント、フラグの管理、ロックフリーデータ構造の実装など、特定のシナリオで高いパフォーマンスと安全性を実現できます。

主なアトミック操作には以下のようなものがあります。

  • Load: メモリから値をアトミックに読み込む。
  • Store: メモリに値をアトミックに書き込む。
  • Add: メモリ上の値にアトミックに加算する。
  • Swap: メモリ上の値と新しい値をアトミックに交換する。
  • CompareAndSwap (CAS): メモリ上の値が期待値と一致する場合にのみ、新しい値にアトミックに交換する。これはロックフリーアルゴリズムの基礎となる重要な操作です。

これらの操作は、通常、CPUが提供する特殊な命令(例: x86のLOCKプレフィックス付き命令、ARMのLDREX/STREX命令)を利用して実装されます。

ARMアーキテクチャとARM11/ARMv6K

ARMアーキテクチャ: ARM (Advanced RISC Machine) は、低消費電力と高性能を両立するRISC (Reduced Instruction Set Computer) アーキテクチャです。スマートフォン、タブレット、組み込みシステム、IoTデバイス、最近ではサーバーやデスクトップPCなど、幅広い分野で利用されています。

ARM11プロセッサ: ARM11は、2002年に導入された32ビットARMプロセッサコアのファミリーです。ARMv6命令セットアーキテクチャを初めて実装したプロセッサであり、以下の特徴を持ちます。

  • ARMv6 ISA: SIMDメディア命令、マルチプロセッササポート、新しいキャッシュアーキテクチャなどを含む。
  • 改良されたパイプライン: 8段の整数パイプラインを持ち、高いクロック速度とスループットを実現。
  • 64ビットデータパス: 内部データバスが64ビット幅で、データ集約型操作で高性能を発揮。
  • 低消費電力: クロックゲーティングやパワーダウンモードなどにより、低消費電力を実現。

ARMv6Kアーキテクチャ: ARMv6Kは、ARMv6アーキテクチャの拡張であり、主にマルチプロセッシングとメモリ管理の強化に焦点を当てています。重要な追加機能として、以下のものが挙げられます。

  • 排他ロード/ストア命令 (LDREX/STREX): マルチプロセッサシステムでの同期プリミティブの実装に不可欠な命令。
  • SMP (Symmetric MultiProcessing) および TLS (Thread Local Storage) 命令セット: マルチプロセッサ環境での効率的な動作をサポート。
  • 真のNOP (No-Operation) 命令とYield命令: コード最適化とプロセッサリソース管理に利用。

このコミットがARMv6K以降をサポートすると明記しているのは、アトミック操作の実装に不可欠なLDREX/STREX命令がARMv6Kで導入されたためです。

LDREX/STREX命令によるアトミック操作

ARMアーキテクチャでは、アトミックな読み書き変更 (Read-Modify-Write) 操作を実現するために、LDREX (Load Exclusive) と STREX (Store Exclusive) という一対の命令が使用されます。これらは「ロードロック/ストア条件付き」のメカニズムを提供し、複数のプロセッサやスレッドが共有メモリにアクセスする際のデータ競合を防ぎます。

  1. LDREX (Load Exclusive):

    • 指定されたメモリアドレスから値をロードし、そのアドレスを現在のプロセッサに対して「排他的」としてマークします。
    • このマークは、CPU内部の「排他モニター」によって追跡されます。
    • LDREXが実行されると、排他モニターは「排他」状態に移行し、ターゲットアドレスを監視します。
  2. STREX (Store Exclusive):

    • メモリアドレスへの値のストアを試みます。
    • ストア操作は、対応するLDREX命令以降、他のプロセッサやコンテキストが監視対象のメモリ位置に干渉していない場合にのみ成功します。
    • STREX命令は、成功した場合は0、失敗した場合は1をレジスタに返します。失敗した場合、それは他のプロセッサがそのメモリ位置を書き込んだか、排他モニターの状態がクリアされたことを意味します。

アトミック操作の実現: LDREXSTREXは通常、ループ内で使用されます。

  1. LDREXで値を読み込む。
  2. 読み込んだ値に対して必要な変更を行う。
  3. STREXで変更した値を書き戻すことを試みる。
  4. STREXが失敗した場合(競合が発生した場合)、ステップ1に戻って操作をリトライする。

このループにより、読み込み、変更、書き込みの一連の操作が、外部からは不可分な単一の操作として見えるようになります。

技術的詳細

このコミットで追加された asm_netbsd_arm.s ファイルは、Goのアトミック操作をNetBSD/ARM環境で実現するためのアセンブリ言語による実装です。Goのランタイムは、特定のアーキテクチャとOSの組み合わせに対して、sync/atomic パッケージの関数を呼び出す際に、これらのアセンブリルーチンにディスパッチします。

ファイルの内容を見ると、TEXTディレクティブを使用してGoの関数名に対応するアセンブリルーチンが定義されています。例えば、·CompareAndSwapInt32·AddUint32·LoadUint32·StoreUint32 などです。これらの多くは、より汎用的な内部アセンブリルーチン(例: ·armCompareAndSwapUint32·armAddUint32·armCompareAndSwapUint64·addUint64·loadUint64·storeUint64)にジャンプ(B命令)しています。これは、異なる型(Int32, Uint32, Uintptr, Pointerなど)に対して同じアトミックロジックを再利用するための一般的なパターンです。

特に注目すべきは、LoadUint32StoreUint32の実装です。これらは、前述のLDREXSTREX命令を直接使用して、アトミックな読み込みと書き込みを実現しています。

  • LoadUint32:

    • LDREXでアドレスから値を読み込みます。
    • 読み込んだ値をSTREXで同じアドレスに書き戻すことを試みます。これは、排他モニターの状態をクリアし、他のプロセッサがそのアドレスにアクセスしても排他状態が失われないようにするためのテクニックです。
    • STREXが成功するまでループを繰り返します。成功すれば、読み込んだ値がアトミックに取得されたことになります。
  • StoreUint32:

    • LDREXでアドレスから現在の値を読み込みます(これはストア操作の準備として排他モニターを設定するため)。
    • 新しい値をSTREXで書き戻すことを試みます。
    • STREXが成功するまでループを繰り返します。成功すれば、新しい値がアトミックに書き込まれたことになります。

これらの実装は、ARMv6K以降のプロセッサが提供するハードウェアサポートを最大限に活用し、Goの並行処理モデルにおけるデータの一貫性と安全性を保証します。

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

このコミットによって追加された唯一のファイルは src/pkg/sync/atomic/asm_netbsd_arm.s です。このファイルは、NetBSD/ARM環境における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.

// NetBSD/ARM atomic operations.
// TODO(minux): this only supports ARMv6K or higher.

TEXT ·CompareAndSwapInt32(SB),7,$0
	B ·CompareAndSwapUint32(SB)

TEXT ·CompareAndSwapUint32(SB),7,$0
	B ·armCompareAndSwapUint32(SB)

TEXT ·CompareAndSwapUintptr(SB),7,$0
	B ·CompareAndSwapUint32(SB)

TEXT ·CompareAndSwapPointer(SB),7,$0
	B ·CompareAndSwapUint32(SB)

TEXT ·AddInt32(SB),7,$0
	B ·AddUint32(SB)

TEXT ·AddUint32(SB),7,$0
	B ·armAddUint32(SB)

TEXT ·AddUintptr(SB),7,$0
	B ·AddUint32(SB)

TEXT ·CompareAndSwapInt64(SB),7,$0
	B ·CompareAndSwapUint64(SB)

TEXT ·CompareAndSwapUint64(SB),7,$-4
	B ·armCompareAndSwapUint64(SB)

TEXT ·AddInt64(SB),7,$0
	B ·addUint64(SB)

TEXT ·AddUint64(SB),7,$0
	B ·addUint64(SB)

TEXT ·LoadInt32(SB),7,$0
	B ·LoadUint32(SB)

TEXT ·LoadUint32(SB),7,$0
	MOVW addr+0(FP), R1
load32loop:
	LDREX (R1), R2		// loads R2
	STREX R2, (R1), R0	// stores R2
	CMP $0, R0
	BNE load32loop
	MOVW R2, val+4(FP)
	RET

TEXT ·LoadInt64(SB),7,$0
	B ·loadUint64(SB)

TEXT ·LoadUint64(SB),7,$0
	B ·loadUint64(SB)

TEXT ·LoadUintptr(SB),7,$0
	B ·LoadUint32(SB)

TEXT ·LoadPointer(SB),7,$0
	B ·LoadUint32(SB)

TEXT ·StoreInt32(SB),7,$0
	B ·StoreUint32(SB)

TEXT ·StoreUint32(SB),7,$0
	MOVW addr+0(FP), R1
	MOVW val+4(FP), R2
storeloop:
	LDREX (R1), R4		// loads R4
	STREX R2, (R1), R0	// stores R2
	CMP $0, R0
	BNE storeloop
	RET

TEXT ·StoreInt64(SB),7,$0
	B ·storeUint64(SB)

TEXT ·StoreUint64(SB),7,$0
	B ·storeUint64(SB)

TEXT ·StoreUintptr(SB),7,$0
	B ·StoreUint32(SB)

TEXT ·StorePointer(SB),7,$0
	B ·StoreUint32(SB)

コアとなるコードの解説

このアセンブリコードは、Goのsync/atomicパッケージが提供する様々なアトミック操作(CompareAndSwapAddLoadStore)をNetBSD/ARM環境で実装しています。

  • 関数定義 (TEXT ディレクティブ):

    • TEXT ·FunctionName(SB),7,$0 の形式でGoの関数がアセンブリルーチンとして定義されています。SBはStatic Baseレジスタを示し、7はGoのABI(Application Binary Interface)におけるフレームポインタのオフセット、$0はスタックフレームサイズを示します。
    • 多くの関数(例: CompareAndSwapInt32)は、対応する符号なし整数バージョン(例: CompareAndSwapUint32)に直接ジャンプ(B命令)しています。これは、アトミック操作のロジックが符号の有無に依存しないため、コードの再利用性を高めるためです。
    • さらに、CompareAndSwapUint32·armCompareAndSwapUint32 に、AddUint32·armAddUint32 にジャンプしています。これは、これらの操作の具体的な実装が別の共通アセンブリファイル(このコミットには含まれていないが、Goの他のARMアセンブリファイルに存在する可能性が高い)で定義されていることを示唆しています。同様に、64ビット操作も ·armCompareAndSwapUint64·addUint64·loadUint64·storeUint64 といった共通ルーチンに委譲されています。
  • LoadUint32 の実装:

    TEXT ·LoadUint32(SB),7,$0
    	MOVW addr+0(FP), R1
    load32loop:
    	LDREX (R1), R2		// loads R2
    	STREX R2, (R1), R0	// stores R2
    	CMP $0, R0
    	BNE load32loop
    	MOVW R2, val+4(FP)
    	RET
    
    1. MOVW addr+0(FP), R1: 関数引数として渡されたアドレス(addr)をレジスタR1にロードします。FPはフレームポインタです。
    2. load32loop:: ループの開始ラベルです。
    3. LDREX (R1), R2: R1が指すメモリ位置から値を排他的にロードし、その値をR2に格納します。同時に、このメモリ位置が排他モニターによって監視されます。
    4. STREX R2, (R1), R0: R2の値をR1が指すメモリ位置に排他的にストアしようとします。このSTREXは、LDREXで読み込んだ値をそのまま書き戻すという特殊な使い方をしています。これは、排他モニターの状態をクリアし、LDREX/STREXペアが成功したことを確認するための一般的なパターンです。ストア操作の結果(成功/失敗)はR0に格納されます。
    5. CMP $0, R0: STREXの結果が成功(R0が0)かどうかをチェックします。
    6. BNE load32loop: STREXが失敗した場合(R0が0以外)、load32loopに戻り、操作をリトライします。
    7. MOVW R2, val+4(FP): STREXが成功した場合、アトミックに読み込んだ値(R2)を関数の戻り値として設定します。
    8. RET: 関数から戻ります。 このLoadUint32の実装は、LDREX/STREXのペアを使用して、メモリから値をアトミックに読み込むことを保証しています。
  • StoreUint32 の実装:

    TEXT ·StoreUint32(SB),7,$0
    	MOVW addr+0(FP), R1
    	MOVW val+4(FP), R2
    storeloop:
    	LDREX (R1), R4		// loads R4
    	STREX R2, (R1), R0	// stores R2
    	CMP $0, R0
    	BNE storeloop
    	RET
    
    1. MOVW addr+0(FP), R1: ストア対象のアドレスをR1にロードします。
    2. MOVW val+4(FP), R2: ストアする新しい値(val)をR2にロードします。
    3. storeloop:: ループの開始ラベルです。
    4. LDREX (R1), R4: R1が指すメモリ位置から値を排他的にロードし、その値をR4に格納します。これは、ストア操作の準備として排他モニターを設定するためです。R4にロードされた値自体は、このストア操作では直接使用されません。
    5. STREX R2, (R1), R0: R2(新しい値)をR1が指すメモリ位置に排他的にストアしようとします。結果はR0に格納されます。
    6. CMP $0, R0: STREXの結果が成功(R0が0)かどうかをチェックします。
    7. BNE storeloop: STREXが失敗した場合、storeloopに戻り、操作をリトライします。
    8. RET: 関数から戻ります。 このStoreUint32の実装は、LDREX/STREXのペアを使用して、メモリに値をアトミックに書き込むことを保証しています。

これらのアセンブリルーチンは、Goのsync/atomicパッケージが提供する高レベルなアトミック操作を、NetBSD/ARM環境の低レベルなハードウェア命令にマッピングすることで、Goプログラムがこの環境で正しく並行処理を実行できるようにしています。

関連リンク

参考にした情報源リンク