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

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

このコミットは、GoランタイムがPlan 9/amd64環境で実行される際に、SSE (Streaming SIMD Extensions) 浮動小数点例外を適切にマスクするように修正するものです。Goランタイムは、浮動小数点演算がプログラムの予期せぬ中断を引き起こさないよう、すべてのSSE浮動小数点例外がマスクされていることを前提としています。しかし、Plan 9の64ビットカーネルのデフォルト設定では、一部のSSE浮動小数点例外しかマスクされていなかったため、Goプログラムが不正な操作によって中断される可能性がありました。このコミットは、スレッドごとにすべてのSSE浮動小数点例外をマスクすることで、この問題を解決します。

コミット

commit 23599ca2f6a25aa43c24e24962dcd7616a83d508
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Fri Oct 5 16:23:30 2012 -0400

    runtime: mask SSE exceptions on plan9/amd64
    
    The Go run-time assumes that all SSE floating-point exceptions
    are masked so that Go programs are not broken by such invalid
    operations. By default, the 64-bit version of the Plan 9 kernel
    masks only some SSE floating-point exceptions. Here, we mask
    them all on a per-thread basis.
    
    R=rsc, rminnich, minux.ma
    CC=golang-dev
    https://golang.org/cl/6592056
---
 src/pkg/runtime/os_plan9.h        | 1 +
 src/pkg/runtime/sys_plan9_386.s   | 4 ++++\n src/pkg/runtime/sys_plan9_amd64.s | 9 +++++++++
 src/pkg/runtime/thread_plan9.c    | 3 +++
 4 files changed, 17 insertions(+)

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

https://github.com/golang/go/commit/23599ca2f6a25aa43c24e24962dcd7616a83d508

元コミット内容

Goランタイムは、Goプログラムが不正な浮動小数点操作によって中断されないように、すべてのSSE浮動小数点例外がマスクされていることを前提としています。しかし、Plan 9の64ビットカーネルのデフォルト設定では、一部のSSE浮動小数点例外しかマスクされていませんでした。このコミットは、スレッドごとにすべてのSSE浮動小数点例外をマスクすることで、この問題を解決します。

変更の背景

Go言語の設計哲学は、エラーハンドリングを明示的に行うことを重視しており、浮動小数点演算における例外(例えば、ゼロ除算や不正な操作)が発生した場合でも、通常は+Inf(正の無限大)、-Inf(負の無限大)、NaN(非数)といった特定の浮動小数点値を返すことで処理を継続します。これにより、プログラムが予期せずクラッシュすることを防ぎます。

しかし、ハードウェアレベルでは、SSE (Streaming SIMD Extensions) を使用した浮動小数点演算において、特定の条件で「浮動小数点例外」が発生する可能性があります。これらの例外がマスクされていない場合、プロセッサはハードウェア例外を発生させ、オペレーティングシステム(OS)がこれを処理しようとします。OSがこれらの例外を適切に処理できない場合、プログラムはクラッシュする可能性があります。

Plan 9の64ビットカーネル(9k)のデフォルト設定では、SSE浮動小数点例外の一部しかマスクされていませんでした。このため、Goランタイムが想定する「すべてのSSE例外がマスクされている」という前提が崩れ、GoプログラムがPlan 9/amd64環境で実行された際に、不正な浮動小数点操作によってクラッシュする可能性がありました。このコミットは、このOSレベルでの設定の不一致を解消し、Goランタイムの安定性を確保するために導入されました。

前提知識の解説

SSE (Streaming SIMD Extensions)

SSEは、Intelが開発したSIMD(Single Instruction, Multiple Data)命令セットの拡張機能です。主に浮動小数点演算の高速化を目的としており、複数のデータを単一の命令で同時に処理することができます。現代のCPUでは、浮動小数点演算の多くがSSE命令を使用して行われます。

浮動小数点例外

IEEE 754標準で定義されている浮動小数点演算には、以下のような例外条件があります。

  • Invalid Operation (#I): 0/0 や sqrt(-1) など、数学的に定義されない操作。結果はNaN。
  • Denormal Operand (#D): 非正規化数(非常に小さい非ゼロ数)がオペランドとして使用された場合。
  • Divide-by-Zero (#Z): 有限の非ゼロ数をゼロで除算した場合。結果は+Infまたは-Inf。
  • Overflow (#O): 結果が表現可能な最大値を超えた場合。結果は+Infまたは-Inf。
  • Underflow (#U): 結果が表現可能な最小値より小さくなった場合。結果は非正規化数またはゼロ。
  • Precision (#P): 結果が正確に表現できない場合(丸めが必要な場合)。

これらの例外が発生した際に、プロセッサがどのように振る舞うかは、MXCSRレジスタの設定によって制御されます。

MXCSR (Media eXtension Control and Status Register)

MXCSRは、SSEおよびAVX浮動小数点演算で使用される32ビットの制御/ステータスレジスタです。このレジスタは、浮動小数点例外のマスキング、ステータスフラグ、およびその他の制御ビット(例: Flush-to-Zero (FTZ) や Denormals-Are-Zeros (DAZ) モード)を管理します。

  • マスクビット (Bits 7-12): 各浮動小数点例外に対応するビットです。
    • ビットがセット (1) されている場合、対応する例外はマスクされます。マスクされた例外が発生しても、プロセッサは内部的に処理(例: NaNや無限大を生成)し、プログラムの実行を中断しません。例外のステータスフラグは設定されます。
    • ビットがクリア (0) されている場合、対応する例外はアンマスクされます。アンマスクされた例外が発生すると、プロセッサは通常、ハードウェア例外(例: #XM - SIMD Floating-Point Exception)を発生させ、プログラムのクラッシュやカスタム例外ハンドラの介入につながる可能性があります。
  • ステータスフラグ (Bits 0-5): 特定の例外が発生したかどうかを示す「スティッキー」なフラグです。一度設定されると、ソフトウェアによって明示的にクリアされるまで設定されたままになります。

MXCSRレジスタの操作は、通常、LDMXCSR(MXCSRをメモリからロード)およびSTMXCSR(MXCSRをメモリにストア)というアセンブリ命令を使用して行われます。

Goランタイムと浮動小数点例外

Goランタイムは、IEEE 754標準に準拠しており、浮動小数点例外が発生しても、通常はNaNInfといった特殊な値を返すことで処理を続行します。Go言語自体には、これらの浮動小数点「例外」をキャッチするメカニズム(try-catchのようなもの)は提供されていません。Goプログラムが「Floating point exception (core dumped)」のようなエラーでクラッシュする場合、それは通常、OSやハードウェアレベル、またはGoランタイムやGoが連携しているC/C++ライブラリ内のバグに起因するものです。

Plan 9カーネル

Plan 9は、ベル研究所で開発された分散オペレーティングシステムです。このコミットが対象としているのは、Plan 9の64ビット版カーネル(9k)です。OSは、ハードウェア例外が発生した際に、それを処理するためのメカニズム(Plan 9では「note handlers」と呼ばれる)を持っています。しかし、過去にはPlan 9の64ビットシステムにおいて、カーネルがSSEレジスタやコンテキストを「note handlers」内で適切に保存・復元できないという課題がありました。

技術的詳細

このコミットの核心は、Plan 9/amd64環境において、Goランタイムが起動する際に、すべてのSSE浮動小数点例外をマスクするようにMXCSRレジスタを設定することです。

MXCSRレジスタのビット構成において、浮動小数点例外のマスクビットはビット7からビット12に割り当てられています。これらのビットが1に設定されていると、対応する例外はマスクされます。

  • Bit 7: Invalid Operation Mask (IM)
  • Bit 8: Denormal Operation Mask (DM)
  • Bit 9: Divide-by-Zero Mask (ZM)
  • Bit 10: Overflow Mask (OM)
  • Bit 11: Underflow Mask (UM)
  • Bit 12: Precision Mask (PM)

これらのビットをすべてマスクするには、対応するビットを1に設定する必要があります。つまり、0x3F (バイナリで 00111111) をビット7からビット12の位置にシフトした値 (0x3F << 7) をMXCSRレジスタにOR演算で加えることで、すべての例外をマスクできます。

コミットのコードでは、以下の手順でMXCSRレジスタを操作しています。

  1. STMXCSR 0(SP): 現在のMXCSRレジスタの値をスタックポインタ (SP) が指すメモリ位置にストアします。
  2. MOVL 0(SP), AX: ストアしたMXCSRの値をAXレジスタにロードします。
  3. ANDL $~0x3F, AX: AXレジスタの値と ~0x3F (ビット0からビット5をクリアするマスク) のAND演算を行います。これは、既存のステータスフラグ(ビット0-5)をクリアし、将来の例外発生に備えるためと考えられます。ただし、MXCSRのマスクビットはビット7-12なので、このAND演算は主にステータスフラグをクリアする目的か、あるいはMXCSRの他の下位ビットをクリアする目的である可能性があります。
  4. ORL $(0x3F<<7), AX: AXレジスタの値と (0x3F << 7) (すべての例外マスクビットをセットするマスク) のOR演算を行います。これにより、すべてのSSE浮動小数点例外がマスクされます。
  5. MOVL AX, 0(SP): 変更されたAXレジスタの値をスタックポインタが指すメモリ位置にストアし直します。
  6. LDMXCSR 0(SP): スタックにストアされた新しい値をMXCSRレジスタにロードします。

この一連の操作により、現在のスレッドのMXCSRレジスタが更新され、すべてのSSE浮動小数点例外がマスクされた状態になります。

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

このコミットでは、以下の4つのファイルが変更されています。

  1. src/pkg/runtime/os_plan9.h:

    • runtime·setfpmasks(void); という新しい関数プロトタイプが追加されました。これは、浮動小数点マスクを設定するための関数です。
  2. src/pkg/runtime/sys_plan9_386.s:

    • runtime·setfpmasks という関数が追加されていますが、32ビット版のPlan 9では何もしない(RETのみ)スタブ実装です。コメントで「Only used by the 64-bit runtime.」と明記されています。
  3. src/pkg/runtime/sys_plan9_amd64.s:

    • runtime·setfpmasks という関数が追加されました。この関数は、MXCSRレジスタを操作してSSE浮動小数点例外をマスクするアセンブリコードを含んでいます。
  4. src/pkg/runtime/thread_plan9.c:

    • runtime·minit(void) 関数内に runtime·setfpmasks(); の呼び出しが追加されました。runtime·minit は、Goランタイムがスレッドを初期化する際に呼び出される関数であり、これにより各スレッドが起動する際にSSE例外マスクが設定されるようになります。

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/runtime/sys_plan9_amd64.s に追加された runtime·setfpmasks 関数と、それが src/pkg/runtime/thread_plan9.cruntime·minit から呼び出される点です。

src/pkg/runtime/sys_plan9_amd64.sruntime·setfpmasks 関数

TEXT runtime·setfpmasks(SB),7,$8
	STMXCSR	0(SP)
	MOVL	0(SP), AX
	ANDL	$~0x3F, AX
	ORL	$(0x3F<<7), AX
	MOVL	AX, 0(SP)
	LDMXCSR	0(SP)
	RET
  • TEXT runtime·setfpmasks(SB),7,$8: runtime·setfpmasks という名前の関数を定義しています。SB はStatic Baseレジスタを意味し、グローバルシンボルであることを示します。7 はアライメント、$8 はスタックフレームサイズ(MXCSRレジスタの値を一時的に保存するために8バイト使用)を示します。
  • STMXCSR 0(SP): 現在のMXCSRレジスタの値をスタックポインタ (SP) が指すアドレス(スタックの先頭)にストアします。
  • MOVL 0(SP), AX: スタックにストアされたMXCSRの値を32ビットレジスタAXにロードします。
  • ANDL $~0x3F, AX: AXレジスタの値と ~0x3F のビットAND演算を行います。0x3F はバイナリで 00111111 です。~0x3F はすべてのビットが1で、下位6ビット(ビット0-5)が0のマスクになります。この操作により、MXCSRのステータスフラグ(ビット0-5)がクリアされます。これは、過去に発生した例外のステータスをリセットし、新しいスレッドのクリーンな状態を保証するためです。
  • ORL $(0x3F<<7), AX: AXレジスタの値と (0x3F << 7) のビットOR演算を行います。0x3F を7ビット左シフトすると、バイナリで 1111110000000 となり、MXCSRのマスクビット(ビット7-12)がすべて1に設定されます。これにより、すべてのSSE浮動小数点例外がマスクされます。
  • MOVL AX, 0(SP): 変更されたAXレジスタの値を再びスタックにストアします。
  • LDMXCSR 0(SP): スタックにストアされた新しい値をMXCSRレジスタにロードします。これにより、プロセッサの浮動小数点ユニットの動作が変更され、SSE例外がマスクされます。
  • RET: 関数からリターンします。

src/pkg/runtime/thread_plan9.cruntime·minit 関数

void
runtime·minit(void)
{
	// Mask all SSE floating-point exceptions
	// when running on the 64-bit kernel.
	runtime·setfpmasks();
}
  • runtime·minit(void): この関数は、Goランタイムが新しいM(Machine、OSスレッド)を初期化する際に呼び出されます。Goのスケジューラは、G(Goroutine)をM上で実行します。したがって、新しいOSスレッドがGoランタイムによって起動されるたびに、このruntime·minitが実行されます。
  • runtime·setfpmasks();: ここで、上記のアセンブリ関数 runtime·setfpmasks が呼び出されます。これにより、各Goスレッドが起動する際に、そのスレッドのコンテキストにおけるMXCSRレジスタが設定され、すべてのSSE浮動小数点例外がマスクされるようになります。これは、Goランタイムが想定する浮動小数点例外の挙動を、Plan 9/amd64環境でも保証するために不可欠な変更です。

この変更により、GoプログラムはPlan 9/amd64環境においても、浮動小数点例外によって予期せずクラッシュすることなく、安定して動作するようになります。

関連リンク

参考にした情報源リンク