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

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

このコミットは、Goランタイムにおけるソフトウェア浮動小数点ルーチン実行中のプリエンプション(横取り)を無効化する変更です。具体的には、ARMアーキテクチャ向けのsrc/pkg/runtime/vlop_arm.sファイルに修正が加えられ、ソフトウェア浮動小数点演算中にGoランタイムのスケジューラによるゴルーチンの切り替えが発生しないように制御されます。これにより、浮動小数点レジスタの状態が予期せず変更されることを防ぎ、正確な計算を保証します。

コミット

commit e03dd0798163b267d8db6b4d2c95dc281be5a064
Author: Russ Cox <rsc@golang.org>
Date:   Thu Aug 1 20:07:01 2013 -0400

    runtime: disable preemption during software fp routines
    
    It's okay to preempt at ordinary function calls because
    compilers arrange that there are no live registers to save
    on entry to the function call.
    
    The software floating point routines are function calls
    masquerading as individual machine instructions. They are
    expected to keep all the registers intact. In particular,
    they are expected not to clobber all the floating point
    registers.
    
    The floating point registers are kept per-M, because they
    are not live at non-preemptive goroutine scheduling events,
    and so keeping them per-M reduces the number of 132-byte
    register blocks we are keeping in memory.
    
    Because they are per-M, allowing the goroutine to be
    rescheduled during software floating point simulation
    would mean some other goroutine could overwrite the registers
    or perhaps the goroutine would continue running on a different
    M entirely.
    
    Disallow preemption during the software floating point
    routines to make sure that a function full of floating point
    instructions has the same floating point registers throughout
    its execution.
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/12298043

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

https://github.com/golang/go/commit/e03dd0798163b267d8db6b4d2c95dc281be5a064

元コミット内容

Goランタイムにおいて、ソフトウェア浮動小数点ルーチンの実行中にプリエンプションを無効化する。

通常の関数呼び出しでは、コンパイラが関数呼び出し時に保存すべきライブレジスタがないように調整するため、プリエンプションは問題ない。

ソフトウェア浮動小数点ルーチンは、個々の機械命令を装った関数呼び出しである。これらはすべてのレジスタを無傷に保つことが期待されており、特に浮動小数点レジスタを破壊しないことが期待されている。

浮動小数点レジスタはM(マシン)ごとに保持される。これは、非プリエンプティブなゴルーチンスケジューリングイベント時にはそれらがライブではないためであり、Mごとに保持することでメモリに保持する132バイトのレジスタブロックの数を減らすことができる。

Mごとに保持されるため、ソフトウェア浮動小数点シミュレーション中にゴルーチンが再スケジュールされると、他のゴルーチンがレジスタを上書きしたり、あるいはゴルーチンが完全に異なるM上で実行を継続したりする可能性がある。

浮動小数点命令で満たされた関数が、その実行全体を通して同じ浮動小数点レジスタを持つことを確実にするため、ソフトウェア浮動小数点ルーチン中のプリエンプションを許可しない。

変更の背景

このコミットの背景には、Goランタイムのスケジューラと、特定のアーキテクチャ(ここではARM)におけるソフトウェア浮動小数点演算の特性との間の相互作用に関する課題がありました。

Goのランタイムは、ゴルーチンと呼ばれる軽量な並行処理単位を効率的にスケジューリングします。このスケジューリングには、実行中のゴルーチンを一時停止し、別のゴルーチンにCPUを割り当てる「プリエンプション」のメカニズムが含まれます。通常の関数呼び出しの前後では、コンパイラがレジスタの状態を適切に管理するため、プリエンプションが発生しても問題ありません。つまり、関数呼び出しの境界では、ライブな(後で必要となる)レジスタはすべてスタックに保存されるか、呼び出し規約によって保護されるため、ゴルーチンが切り替わってもレジスタの状態が失われることはありません。

しかし、ARMのような一部のアーキテクチャでは、ハードウェアによる浮動小数点演算ユニット(FPU)が利用できない場合や、特定の浮動小数点命令がハードウェアでサポートされていない場合に、ソフトウェアで浮動小数点演算をエミュレートするルーチン(ソフトウェア浮動小数点ルーチン)が使用されます。これらのルーチンは、Goランタイムの視点からは通常の関数呼び出しとして扱われますが、その実態は単一の機械命令のように振る舞い、実行中に特定のレジスタ(特に浮動小数点レジスタ)の状態を維持することを期待します。

問題は、Goランタイムが浮動小数点レジスタを「M」(OSスレッドに相当するGoランタイムの抽象化)ごとに管理していた点にありました。これは、通常のゴルーチン切り替えポイント(関数呼び出しなど)では浮動小数点レジスタがライブではないと見なされ、ゴルーチンコンテキストの一部として保存・復元する必要がないため、メモリ使用量を削減するための最適化でした。しかし、ソフトウェア浮動小数点ルーチンは、その性質上、実行中に浮動小数点レジスタを頻繁に使用し、その状態が継続することを前提としています。

もしソフトウェア浮動小数点ルーチンの実行中にプリエンプションが発生し、ゴルーチンが別のMに移動したり、同じM上で別のゴルーチンが実行されたりすると、Mに紐付けられた浮動小数点レジスタの状態が上書きされたり、失われたりする可能性がありました。これにより、ソフトウェア浮動小数点演算の結果が不正になるというバグが発生する恐れがありました。

このコミットは、このようなデータ破損のリスクを排除し、ソフトウェア浮動小数点演算の正確性を保証するために、これらのルーチン実行中のプリエンプションを一時的に無効化するという解決策を導入しました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念と、コンピュータアーキテクチャに関する基本的な知識が必要です。

1. Goランタイムとゴルーチン (Goroutines)

  • ゴルーチン (Goroutines): Go言語における軽量な並行処理単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによって管理され、OSスレッドにマッピングされます。
  • Goスケジューラ (Go Scheduler): Goランタイムの一部であり、ゴルーチンをOSスレッド(M)に割り当て、実行を管理します。Goスケジューラは、ゴルーチンがI/O待ちになったり、システムコールを実行したり、あるいは長時間CPUを占有したりする際に、他のゴルーチンにCPUを切り替える役割を担います。
  • M-P-Gモデル: Goスケジューラの内部モデルです。
    • M (Machine): OSスレッドを表します。Goランタイムは、M上でゴルーチンを実行します。
    • P (Processor): 論理プロセッサを表します。Mがゴルーチンを実行するためのコンテキストを提供します。Pは、実行可能なゴルーチンのキューを保持し、Mにゴルーチンを供給します。
    • G (Goroutine): ゴルーチンそのものです。 このモデルにより、Goランタイムは効率的なゴルーチンのスケジューリングと並行実行を実現しています。

2. プリエンプション (Preemption)

  • プリエンプション: 実行中のタスク(この場合はゴルーチン)を、そのタスクが自発的にCPUを解放するのを待たずに、強制的に一時停止させ、別のタスクにCPUを割り当てるメカニズムです。Goランタイムでは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、定期的にプリエンプションポイントを挿入し、スケジューラが介入できるようにしています。
  • プリエンプションポイント: スケジューラがゴルーチンの実行を安全に中断できる場所です。Goでは、関数呼び出しの前後などが一般的なプリエンプションポイントとなります。これは、関数呼び出し規約によってレジスタの状態が適切に保存されるため、ゴルーチンが切り替わっても問題が発生しにくいからです。

3. ソフトウェア浮動小数点 (Software Floating Point)

  • 浮動小数点演算: 小数点を含む数値をコンピュータで表現し、計算するための方法です。通常、CPUには浮動小数点演算ユニット(FPU)が搭載されており、高速に浮動小数点演算を実行できます。
  • ソフトウェア浮動小数点: ハードウェアFPUが存在しない、または特定の浮動小数点命令がハードウェアでサポートされていない場合に、ソフトウェア(CPUの汎用レジスタと命令セット)で浮動小数点演算をエミュレートすることです。これはハードウェアFPUに比べてはるかに低速ですが、互換性や特定の環境での実行を可能にします。
  • レジスタ: CPU内部にある高速な記憶領域です。プログラムの実行中にデータやアドレスを一時的に保持するために使用されます。浮動小数点演算には、専用の浮動小数点レジスタが使用されることがあります。

4. m->locks (Mのロックカウンタ)

Goランタイムの内部構造であるm(OSスレッドを表す抽象化)には、locksというフィールドが存在します。これは、そのM上で実行されているゴルーチンが、スケジューラによるプリエンプションやMの切り替えを一時的に禁止したい場合にインクリメントするカウンタです。m->locksが0より大きい間は、そのM上で実行されているゴルーチンはプリエンプションされません。これは、クリティカルセクションや、レジスタの状態が厳密に維持される必要がある処理中に使用されます。

技術的詳細

このコミットの技術的な核心は、Goランタイムがソフトウェア浮動小数点ルーチンを「通常の関数呼び出し」とは異なる特殊なコンテキストとして扱い、その実行中にプリエンプションを明示的に無効化する点にあります。

  1. ソフトウェア浮動小数点ルーチンの特性:

    • これらのルーチンは、Goランタイムの視点からは関数呼び出しとして扱われますが、その内部では単一の機械命令の動作をエミュレートしています。
    • エミュレーションの性質上、これらのルーチンは実行中に特定の浮動小数点レジスタの状態が継続的に維持されることを強く期待します。つまり、ルーチンの途中でレジスタの値が予期せず変更されると、計算結果が不正になる可能性があります。
  2. 浮動小数点レジスタの管理:

    • Goランタイムは、浮動小数点レジスタをゴルーチン(G)のコンテキストの一部としてではなく、M(OSスレッド)のコンテキストの一部として管理していました。
    • これは、通常のゴルーチン切り替えポイント(関数呼び出しなど)では浮動小数点レジスタがライブではないと見なされ、ゴルーチンごとに保存・復元する必要がないため、メモリ使用量(132バイト/G)を削減するための最適化でした。
  3. プリエンプションによる問題:

    • Mごとに浮動小数点レジスタが管理されている状況で、ソフトウェア浮動小数点ルーチンの実行中にプリエンプションが発生すると、以下の問題が生じます。
      • レジスタの上書き: プリエンプションにより、現在のゴルーチンが一時停止され、同じM上で別のゴルーチンが実行される可能性があります。この別のゴルーチンが浮動小数点演算を行うと、Mに紐付けられた浮動小数点レジスタの状態が上書きされてしまいます。
      • Mの切り替え: プリエンプション後、現在のゴルーチンが別のMに移動して実行を再開する可能性があります。この場合、元のMに保存されていた浮動小数点レジスタの状態は新しいMには引き継がれないため、ゴルーチンは不正なレジスタ状態で実行を継続することになります。
    • いずれのシナリオでも、ソフトウェア浮動小数点ルーチンが期待するレジスタの状態が維持されず、計算結果の破損につながります。
  4. 解決策としてのプリエンプション無効化:

    • このコミットでは、ソフトウェア浮動小数点ルーチン(具体的には_sfloat関数)の開始直前にm->locksカウンタをインクリメントし、ルーチン終了直後にデクリメントすることで、このルーチン実行中のプリエンプションを一時的に無効化します。
    • m->locksが0より大きい間は、GoスケジューラはそのM上で実行されているゴルーチンをプリエンプトしません。これにより、ソフトウェア浮動小数点ルーチンが中断されることなく、その実行全体を通して浮動小数点レジスタの状態が保証されます。
    • コミットメッセージでは、代替案として浮動小数点レジスタをG(ゴルーチン)のコンテキストに移動することも検討されたことが示唆されていますが、これは「通常のゴルーチン切り替えポイントでは必要ないため、Gごとに132バイトのメモリを無駄にする」という理由で却下されています。このことから、この問題が特定のクリティカルなセクションでのみ発生し、全体的なメモリオーバーヘッドを避けるための慎重な設計判断であったことが伺えます。

この変更により、GoランタイムはARMアーキテクチャのような環境でソフトウェア浮動小数点演算を使用する際に、データの一貫性と正確性を維持できるようになりました。

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

変更は src/pkg/runtime/vlop_arm.s ファイルに集中しています。このファイルは、ARMアーキテクチャ向けのGoランタイムの低レベルなアセンブリコードを含んでいます。

--- a/src/pkg/runtime/vlop_arm.s
+++ b/src/pkg/runtime/vlop_arm.s
@@ -23,6 +23,8 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
+#include "zasm_GOOS_GOARCH.h"
+
 arg=0
 
 /* replaced use of R10 by R11 because the former can be the data segment base register */
@@ -54,7 +56,28 @@ TEXT _sfloat(SB), 7, $64-0 // 4 arg + 14*4 saved regs + cpsr
  	MOVW	R1, 60(R13)
  	WORD	$0xe10f1000 // mrs r1, cpsr
  	MOVW	R1, 64(R13)
+\t// Disable preemption of this goroutine during _sfloat2 by
+\t// m->locks++ and m->locks-- around the call.
+\t// Rescheduling this goroutine may cause the loss of the
+\t// contents of the software floating point registers in 
+\t// m->freghi, m->freglo, m->fflag, if the goroutine is moved
+\t// to a different m or another goroutine runs on this m.
+\t// Rescheduling at ordinary function calls is okay because
+\t// all registers are caller save, but _sfloat2 and the things
+\t// that it runs are simulating the execution of individual
+\t// program instructions, and those instructions do not expect
+\t// the floating point registers to be lost.
+\t// An alternative would be to move the software floating point
+\t// registers into G, but they do not need to be kept at the 
+\t// usual places a goroutine reschedules (at function calls),\n+\t// so it would be a waste of 132 bytes per G.\n+\tMOVW\tm_locks(m), R1
+\tADD\t$1, R1
+\tMOVW\tR1, m_locks(m)
  	BL\truntime·_sfloat2(SB)
+\tMOVW\tm_locks(m), R1
+\tSUB\t$1, R1
+\tMOVW\tR1, m_locks(m)
  	MOVW\tR0, 0(R13)
  	MOVW\t64(R13), R1
  	WORD\t$0xe128f001\t// msr cpsr_f, r1

コアとなるコードの解説

このコミットの主要な変更は、_sfloat というアセンブリ関数内で行われています。この関数は、Goランタイムがソフトウェア浮動小数点演算を実行する際に呼び出されるエントリポイントです。

変更の核心は、runtime·_sfloat2(SB) の呼び出しを挟む形で、m->locks カウンタをインクリメントおよびデクリメントする処理が追加された点です。

  1. #include "zasm_GOOS_GOARCH.h" の追加:

    • これは、Goのアセンブリファイルでプラットフォーム固有のマクロや定義をインクルードするための一般的な慣習です。この行自体が直接的な機能変更をもたらすわけではありませんが、m_locks(m) のようなシンボルが正しく解決されるために必要となる可能性があります。
  2. プリエンプション無効化の開始:

    MOVW	m_locks(m), R1
    ADD	$1, R1
    MOVW	R1, m_locks(m)
    
    • MOVW m_locks(m), R1: 現在のM(OSスレッド)のlocksフィールドの値をレジスタR1にロードします。mは現在のMのポインタを指す特別なレジスタ(または擬似レジスタ)です。m_locksm構造体内のlocksフィールドへのオフセットを示すシンボルです。
    • ADD $1, R1: R1m->locksの値)に1を加算します。
    • MOVW R1, m_locks(m): 更新されたR1の値をm->locksフィールドに書き戻します。
    • この3つの命令により、m->locksカウンタがインクリメントされます。m->locksが0より大きくなると、Goスケジューラは現在のM上で実行されているゴルーチンをプリエンプトしなくなります。これにより、_sfloat2の実行中にゴルーチンが中断されることが保証されます。
  3. ソフトウェア浮動小数点演算の実行:

    BL	runtime·_sfloat2(SB)
    
    • BL runtime·_sfloat2(SB): runtime·_sfloat2関数を呼び出します。この関数が実際のソフトウェア浮動小数点演算のロジックを含んでいます。この呼び出しの間、m->locksがインクリメントされているため、プリエンプションは無効化されています。
  4. プリエンプション無効化の終了:

    MOVW	m_locks(m), R1
    SUB	$1, R1
    MOVW	R1, m_locks(m)
    
    • MOVW m_locks(m), R1: 再びm->locksの現在の値をR1にロードします。
    • SUB $1, R1: R1から1を減算します。
    • MOVW R1, m_locks(m): 更新されたR1の値をm->locksフィールドに書き戻します。
    • この処理により、m->locksカウンタがデクリメントされ、ソフトウェア浮動小数点ルーチンが完了した後にプリエンプションが再び有効になります。

この一連の変更により、Goランタイムはソフトウェア浮動小数点演算のクリティカルセクションを保護し、レジスタの状態が予期せず変更されることによるバグを防ぐことができます。

関連リンク

参考にした情報源リンク

  • Go Scheduler: M-P-G Model: https://go.dev/doc/articles/go_scheduler.html (Goスケジューラの基本的な概念を理解するために参照)
  • Go runtime preemption: https://go.dev/src/runtime/preempt.go (Goランタイムのプリエンプションに関するコードやコメントを理解するために参照)
  • ARM Architecture Reference Manual (浮動小数点レジスタやアセンブリ命令の一般的な理解のため)
  • Go Assembly Language (Goのアセンブリ構文と慣習を理解するため)
  • Go runtime source code (特に src/runtime/proc.gosrc/runtime/mprof.go など、m構造体やlocksフィールドの定義に関する部分)
  • Go issue tracker (関連するバグ報告や議論がないか確認するため)
  • Stack Overflow や Goコミュニティの議論 (Goランタイムの内部動作に関する一般的な理解を深めるため)