| 数値計算ガイド |
第 4 章
例外と例外処理
この章では、IEEE の浮動小数点例外と、浮動小数点例外の検出、特定、処理について説明します。
SPARC、x86 プラットフォーム上の Sun WorkShop 6 Compiler および Solaris オペレーティングシステムで提供される浮動小数点環境では、IEEE 標準規格で定められているすべての例外処理機能、およびその他の推奨されている機能がサポートされています。IEEE 標準の目的の 1 つは、「IEEE 854 標準規格」の次の部分で述べられています (IEEE 854 標準規格の 18 ページを参照してください)。
- ... ユーザーのために、例外条件の発生に伴う複雑な処理を最小限に抑えることです。算術システムは、できるだけ長時間に渡って計算を続行することを目的としています。すなわち、異常な事態が発生しても、適切なフラグを設定するなど、合理的なデフォルトの応答によって対処できる必要があります。
標準規格では、例外演算に対するデフォルトの結果が指定されています。ユーザーが検出、設定、クリアすることによって例外が発生したことを示すステータスフラグを実装する必要があるとしています。また、例外が発生した時に、ブロックをトラップする (通常の制御フローを中断する) 方法を実装することも推奨しています。
例外演算に代替の結果を渡して実行を再開するなど、プログラムで適切な方法で例外を処理するトラップハンドラを用意することもできます。この章では、IEEE 754 で定義されている例外、そのデフォルトの結果、ステータスフラグ、トラップ、例外処理をサポートする浮動小数点環境について説明します。
例外とは
例外の定義は困難ですが、W.Kahan によると以下のようになっています (W.Kahan 著『Handling Arithmetic Exceptions』を参照してください)。
- 算術演算例外は、不可分 (atomic) な算術演算の結果が、一般に受け入れ可能なものでないときに発生します。「不可分な」および「受け入れ可能な (acceptable)」の意味は場合によって異なります。
負数の平方根を取ろうとするのは例外であり、通常は無効な演算によって引き起こされた例外と見られます。この場合、システムでは以下のうちのいずれかのことがらが起こります。
- 例外トラップが無効である場合 (デフォルト)、例外が発生したことがシステムに記録され、 IEEE 754 で指定されている例外処理に関するデフォルト結果を使用して、プログラムの実行を続行します。
- 例外トラップが有効である場合は、
SIGFPEシグナルが生成されます。プログラムにSIGFPEシグナルハンドラが設定されていると、そのシグナルハンドラに制御が移ります。シグナルハンドラが設定されていない場合は、プログラムが異常終了します。IEEE の浮動小数点例外は、無効な演算、ゼロ除算、オーバーフロー、アンダーフロー、および不正確の 5 種類があります。最初の 3 つ (無効な演算、ゼロ除算、オーバーフロー) の例外は共通の例外と呼ばれており、無視することはできません。
ieee_handler(3m) には共通の例外だけをトラップする簡単な方法が提供されています。他の 2 つの例外 (アンダーフロー、不正確) はより頻繁に発生します。実際、ほとんどの浮動小数点演算に不正確例外を発生しており、ほとんどの場合は無視できます。表 4-1 は IEEE 標準規格 754 の内容を要約したものです。5 種類の浮動小数点例外および例外発生時の IEEE 演算機能環境のデフォルトの応答を定義しています。
表 4-1 の注
- 非順序付け比較:
任意の浮動小数値の組は、形式が異なっていても比較することができます。次の 4 つの相互に排他的な比較演算 (より小さい、より大きい、等しい、および非順序付け) が可能です。非順序付けとは、オペランドのうち少なくとも 1 つが NaN (非数) であることを意味します。
表 4-2 非順序付け比較 = == .EQ.無効 ≠ != .NE.無効 > > .GT.無効ではない ≧ >= .GE.無効ではない < < .LT.無効ではない ≦ <= .LE.無効ではない
- 無効な変換:
NaN、または無限大から整数に変換しようとすること。または浮動小数点形式からの変換時に発生した整数値オーバーフロー。- IEEE の単精度、倍精度、および拡張倍精度の形式で表現可能な最小の正規数は、それぞれ 2-126、2-1022、2-16382 です。IEEE の浮動小数点形式については、第 2 章を参照してください。
x86 浮動小数点環境には、IEEE 規格にはない例外 (指数が最小の非正規数オペランド例外) があります。この例外は、浮動小数点演算が非正規数に対して実行された場合に発生します。
表 4-3 例外の優先順位 無効 無効 オーバーフロー オーバーフロー 除算 除算 アンダーフロー アンダーフロー 不正確 不正確 非正規数
同時に発生する可能性のある標準的な例外は、オーバーフローと不正確、およびアンダーフローと不正確だけです。x86 では、5 つの標準的な例外のどれとでも同時に非正規数例外が発生します。オーバーフロー、アンダーフロー、および不正確のトラップが可能になっている場合は、オーバーフローとアンダーフローのトラップが不正確トラップよりも優先されます。これらはすべて x86 の非正規数よりも優先されます。
例外の検出
IEEE 規格で要求されているように、SPARC、x86 プラットフォームの浮動小数点環境では、浮動小数点例外の発生を記録する状態フラグが提供されています。どの例外が発生したかを検出するために、プログラムでこれらのフラグをテストできます。これらのフラグは、明示的に設定またはクリアすることもできます。
ieee_flags関数はこれらの例外にアクセスする方法の 1 つです。C および C++ で書き込まれたプログラムでは、libm9x.soに含まれる C99 浮動小数点環境関数により別の状態フラグが提供されています。SPARC では、各例外に現状を示す「現在フラグ」と「累積フラグ」の 2 つが用意されています。現在の例外フラグは、最後に実行された浮動小数点の演算結果によってその都度更新されます。これらの現在の例外フラグは累積例外フラグにも累積されます。これによって、プログラムの実行開始後またはプログラムにより累積フラグが最後にクリアされた時点以降に発生し、まだトラップされていないすべての例外が記録されます。浮動小数点演算がトラップされた例外の原因である場合、そのトラップを発生させた例外に対応する現在の例外フラグがセットされますが、累積フラグは変更されません。現在の例外フラグと累積例外フラグは、浮動小数点状態レジスタ
%fsr内に保存されます。x86 では、累積フラグは、最初の例外発生時に設定されます。明示的にクリアしない限り、ユーザープロセスの終了までその値を保持します。
ieee_flags(3m)
ieee_flags(3m) 呼び出しの構文は次の通りです。
i = ieee_flags (action,mode,in,out);2 つ目の引数に
exceptionという文字列を指定すると、プログラムはieee_flags(3m) 関数を使用して、発生した例外のステータスフラグを、検査、設定、クリアします。たとえば、FORTRAN でオーバーフロー例外フラグをクリアするには、次のように記述します。
character*8 outcall ieee_flags('clear', 'exception', 'overflow', out)C または C++ では、例外が発生したかどうかの問い合わせは以下のようにします。
i = ieee_flags("get", "exception", in, out);処理が特定できた場合に出力パラメータ out に返される文字列は、以下の通りです。
not available― 例外に関する情報が取得できません。- " " (空白の文字列) ― 累積例外が一度も発生していません。または x86 の場合、非正規オペランドが唯一の累積例外です。
- 例外が発生した場合は、3 番目の引数 in に指定されている例外の名前が返されます。
- そうでない場合は、最も高い優先順位を持つ例外の名前が返されます。in が他の文字列に設定されている場合には、最も高い優先順位を持つ累積例外が返されます。
character*8 outi = ieee_flags ('get', 'exception', 'division', out)ゼロによる除算の例外が発生している場合には、out は
"division"に設定されます。設定されていない場合には、最も優先順位の高い例外がoutに返されます。inに特定の例外が指定されてない場合には、それは無視されます。たとえば、次の呼び出しでは
"all"には特別な意味はありません。
i = ieee_flags("get","exception","all", out);
さらにieee_flagsは、現在発生している例外フラグをすべて組み合わせた整数値も返します。この値は、すべての例外フラグのビット単位の論理和で、それぞれのフラグは表 4-4 に示すようにビットで表わします。sys/ieeefp.hファイルは、それぞれの例外に対応するビット位置を定義します。このビット位置はマシンによって異なり、連続 (隣接) している必要はありません。
下記の C または C++ のプログラムの一部は、
iの値をデコードする方法を示しています。
C99 例外フラグ関数
C および C++ プログラムでは、
libm9x.soに含まれる C99 浮動小数点環境関数を使用して、浮動小数点の例外フラグのテスト、セット、およびクリアができます。ヘッダーファイルfenv.hは、5 つの標準的な例外、FE_INEXACT、FE_UNDERFLOW、FE_OVERFLOW、FE_DIVBYZERO、およびFE_INVALIDに対応する 5 つのマクロを定義しています。fenv.hは、マクロFE_ALL_EXCEPTが 5 つの例外マクロすべてのビット単位の論理和となるようにも定義しています。これらのマクロを組み合わせることにより、例外フラグの任意のサブセットのテストやクリアを行ったり、例外の任意の組み合わせを発生させたりできます。次に、これらのマクロを C99 浮動小数点環境関数のいくつかとあわせて使用した例を示します。詳細は、feclearexcept(3M) のマニュアルページを参照してください。一貫した動作を保つため、libm9x.soに含まれる C99 浮動小数点環境関数と拡張機能、およびlibsunmathに含まれるieee_flagsとieee_handler関数の両方を同じプログラム内で使用することは避けてください。5 つの例外フラグすべてをクリアするには、次のように記述します。
feclearexcept(FE_ALL_EXCEPT);無限演算またはゼロ除算が発生したかどうかをテストするには、次のように記述します。
int i;i = fetestexcept(FE_IMVALID | FE_DIVBYZERO);
if (i& FE_INVALID)
/* 無効な例外が発生しました */
else if (i & FE_DIVBYZERO)
/* ゼロ除算例外が発生しました */オーバーフロー例外の発生をシミュレートするには、次のように記述します (このコードは、オーバーフロートラップが有効な場合にはトラップを起こすことに注意)。
feraiseexcept(FE_OVERFLOW);
fegetexceptflagおよびfesetexceptflag関数は、フラグのサブセットの保存と復元に使用します。次に、この 2 つの関数を使用した例を示します。
例外の特定
プログラマが例外を考慮してプログラムを作成していないために、例外が検出されたときに、どこで例外が発生したのかが問題になることがよくあります。例外が発生した場所を特定する方法の 1 つは、プログラム中のさまざまな箇所で例外フラグをテストすることですが、この方法で正確に例外を特定するには、多くのテストと労力が必要になります。
例外の発生した場所を特定する簡単な方法は、例外トラップを有効にすることです。トラップが有効で、ある例外が発生すると、オペレーティングシステムは、
SIGFPEシグナルを送ってプログラムに通知します (詳細は、signal(5) のマニュアルページを参照してください)。例外のトラップを有効にすると、デバッガで実行してSIGFPEシグナルを受信した時点でプログラム終了するか、または例外が発生した命令のアドレスを出力するようにSIGFPEハンドラを設定して、例外の発生箇所を特定することができます。SIGFPEシグナルを生成する例外についてはトラップを有効にしておく必要があります。トラップが無効になっている場合に例外が発生すると、対応するフラグが設定され、プログラムの実行は 表 4-1 に示されているデフォルトの結果で継続されてますが、シグナルは送られません。デバッガを使用して例外を特定する
この節では、
dbx(ソースレベルのデバッガ) とadb(アセンブリレベルのデバッガ) の使用例を参考にして、浮動小数点例外の原因と、例外発生させた命令を調べます。dbxでソースレベルのデバッグを行うには、プログラムを-gオプション付きでコンパイルする必要があります。詳細は、『dbx コマンドによるデバッグ』マニュアルを参照してください。
program exdouble precision x,y sqrtm1x = -4.2d0y = sqrtm1(x)print * , x, yenddouble precision function sqrtm1(x)double precision xroutine = sqrt(x) - 1.0d0returnendこのプログラムをコンパイルして実行すると、次のように出力されます。
無効な演算の原因を突き止めるには、無効な演算についてのトラップを有効にするために、
-ftrapオプションを付けて再コンパイルし、dbxまたはadbを使用してSIGFPEシグナルが送信された場所を特定することができます。もう 1 つの方法として、無効な演算に対するトラップを有効にする起動ルーチンとリンクするか、または手動でトラップを有効にすると、プログラムを再コンパイルせずに、dbxまたはadbを使用することができます。
dbxを使用して例外の原因となっている命令を特定する浮動小数点例外の原因を突き止めるには、
-gオプションおよび-ftrapオプションを使って再コンパイルし、dbxを使用して例外が発生している場所を追跡します。
example%f77 -g -ftrap=invalid ex.f
-gオプションでコンパイルすると、dbxのソースレベルのデバッグ機能を使用することができます。-ftrap=invalidを指定すると、無効な演算に対する例外のトラップを有効にしてプログラムが実行されます。次に、
dbxを起動し、SIGFPEが出されたときにプログラムを停止するようにcatch fpeコマンドを実行し、プログラムを実行します。結果は次のようになります。
この出力例では、負の数の平方根を求めようとした結果、
sqrtm1関数で例外が発生していることがわかります。
adbを使用して例外の原因となっている命令を特定する
adbを使用して例外の原因を特定することもできます。ただし、dbxとは異なり、adbではソースファイルや行番号は特定できません。adbを使用する場合も、最初の手順は-ftrapを指定してプログラムを再コンパイルします。
example%f77 -ftrap=invalid ex.f次に
adbを起動してプログラムを実行します。無効な演算例外が発生すると、adbは例外の原因である命令の次の命令で停止します。例外の原因である命令を見つけるには、いくつかの命令を逆アセンブルし、adbが停止した命令より前にある最後の浮動小数点命令を探します。SPARC アーキテクチャのシステムでは、結果は次の例のようになります。
この出力例は、
fsqrtd命令が原因で例外が発生したことを示しています。ソースレジスタを調べると、負の数の平方根を求めようとしたためにこの例外が発生したことがわかります。x86 では、命令が固定長ではないため、コードの逆アセンブルを開始する正しいアドレスを見つけるには試行錯誤が必要になります。この例では、関数の先頭付近で例外が発生しているので、ここから逆アセンブルできます。一般的な結果は次のようになります。
この出力例は、
fsqrt命令が原因で例外が発生したことを示しています。浮動小数点レジスタを調べると、負の数の平方根を求めようとしたためにこの例外が発生したことがわかります。再コンパイルせずにトラップを有効にする
上記の例では、
-ftrapフラグを指定して、主プログラムを再コンパイルすることにより、無効な演算の例外トラップを有効にしました。主プログラムの再コンパイルが不可能な場合があり、他の方法でトラップを有効にしなければならないことがあります。トラップを有効にするにはいくつかの方法があります。
dbxの使用中に、浮動小数点状態レジスタを直接変更することによって、トラップを有効にすることができます。SPARC では、この方法には注意が必要です。オペレーティングシステムでは、プログラム内で最初に使用されるまで浮動小数点ユニットは使用可能になりません。浮動小数点ユニットが最初に使用された時点で、浮動小数点状態レジスタはリセットされ、すべてのトラップは無効になります。したがって、プログラムが少なくとも 1 つの浮動小数点命令を実行するまでは手動でトラップを有効にすることはできません。ここで示した例では、
sqrtm1関数が呼び出されるまでに浮動小数点ユニットはアクセスされているので、この関数への入口にブレークポイントを設定し、無効な演算に対する例外トラップを有効にし、SIGFPEシグナルの受信時にdbxを停止するよう設定して、実行を継続できます。アーキテクチャのシステムでの手順は次のようになります。無効な演算例外に対するトラップを有効にするために、assignコマンドを使用して%fsrを変更していることに注意してください。
x86 では、コンパイラが各プログラム内に自動的にリンクする起動コードは、制御をメインプログラムに引き渡す前に浮動小数点ユニットを初期化します。そのため、メインプログラムが開始した後で、いつでも手動でトラップを有効にすることできます。次に、この処理のステップ例を示します。
トラップを有効にする初期化ルーチンを作成すると、メインプログラムを再コンパイルしたり
dbxを使用したりすることなくトラップを有効にできます。この方法は、例外が発生する場合に、デバッガ上で実行することなくプログラムを中止したい場合などに便利です。このようなルーチンを作成する方法は 2 つあります。プログラムを構成するオブジェクトファイルとライブラリが使用可能な場合、プログラムを適切な初期化ルーチンに再リンクするだけでトラップを有効にすることができます。最初に、次のような C のソースファイルを作成します。
#include <ieeefp.h>#pragma init (trapinvalid)void trapinvalid(){/* FP_X_INV などは ieeefp.h 内で定義されています*/fpsetmask(FP_X_INV);}次に、このファイルをコンパイルしてオブジェクトファイルを作成し、元のプログラムをこのオブジェクトファイルにリンクします。
example%cc -c init.cexample%f77 ex.o init.oexample%a.outFloating point exception 7, invalid operand, occurred at address8048afd.Abort再リンクが不可能であるがプログラムは動的にリンクされているという場合は、実行時リンカーの、共有オブジェクトをプリロードする機能を使用してトラップを有効にすることができます。SPARC システムでこのように行う場合は、上記と同じ C ソースファイルを作成し、次に示す方法でコンパイルしてください。
example%cc -Kpic -G -ztext init.c -o init.so -lc次にトラップを有効にします。
init.soオブジェクトのパス名を、環境変数LD_PRELOADによって指定されたプリロードする共有オブジェクトの一覧に追加します。次に例を示します。
example%env LD_PRELOAD=./init.so a.outFloating point exception 7, invalid operand, occurred at address 10c24.
Abort共有オブジェクトの作成とプリロードの詳細は、『リンカーとライブラリ』を参照してください。
上記で説明しているように、浮動小数点の制御モードの初期化方法は、共有オブジェクトをプリロードすることにより原則として変更できます。しかし、共有オブジェクト内の初期化ルーチンは、プリロードされる場合も明示的にリンクされる場合も、メインの実行プログラムの一部である起動コードに制御を渡す前に、実行時リンカーによって実行されます。続いて起動コードは、
-ftrap、-fround、-fns(SPARC)、または-fprecision(x86) コンパイラフラグを介して選択される非デフォルトモードを確立します。そして、メインの実行プログラムの一部である初期化ルーチン (静的にリンクされているものを含む) を実行し、最後に制御をメインプログラムに渡します。そのため、SPARC では、(i) 上記の例で有効にされているトラップのような、共有オブジェクト内の初期化ルーチンにより確立される浮動小数点制御モードはすべて、無効にされないかぎりプログラムの実行中は有効な状態が継続します。(ii) コンパイラフラグを介して選択される非デフォルトモードは、共有オブジェクト内の初期化ルーチンによって確立されるモードを無効にします (ただし、コンパイラフラグを介して選択されるデフォルトモードは、以前に確立されているモードを無効にしません)。(iii) メインの実行プログラムの一部である初期化ルーチンまたは、メインプログラム自体によって確立されるモードはすべて、両方とも無効にします。x86 では状況が複雑です。新しいプロセスが開始されるたびにシステムカーネルが非デフォルトモードをいくつか使用して浮動小数点ハードウェアを初期化しますが、メインプログラムに制御を渡す前に、コンパイラにより自動提供される起動コードがそれらのモードの一部をデフォルトにリセットします。そのため、共有オブジェクト内の初期化ルーチンは、それらが浮動小数点制御モードを変更しないかぎり、無効演算、ゼロ除算、およびオーバーフロー例外に対するトラップを有効にし、丸め精度を 53 個の有効ビットに設定した状態で動作します。実行時リンカーが制御を起動コードに渡した後は、起動コードが標準 C ライブラリ
libc内のルーチン__fpstartを呼び出します。これにより、トラップがすべて無効になり、丸め精度が 64 個の有効ビットに設定されます。起動コードは続いて、-fround、-ftrap、または-fprecisionフラグにより選択されている非デフォルトモードを確立し、そのあと静的にリンクされた初期化ルーチンを実行し、制御をメインプログラムに渡します。そのため、初期化ルーチンを持つ共有オブジェクトをプリロードすることにより x86 プラットフォーム上でトラップを有効にしたり丸め精度モードを変更したりするには、トラップイネーブルモードと丸め精度モードを__fpstartルーチンがリセットしないようにこのルーチンを無効にする必要があります。しかし、代替の__fpstartルーチンは、標準のルーチンが行う初期化機能の残りを実行します。次に、この処理を行うコード例を示します。
このソースファイルを共有オブジェクトにコンパイルしてプリロードすると、期待される結果が生成されます。
example%cc -Kpic -G -ztext init.c -o init.so -lcexample%
env LD_PRELOAD=./init.so a.outFloating point exception 7, invalid operand, occurred at address 8048afd.
Abortシグナルハンドラを使用して例外を特定する
前の節では、例外の最初の発生を特定するためにプログラムの外でトラップを有効にする方法をいくつか紹介しました。これに対して、プログラム内でトラップを有効にして例外の特定の発生を検出することもできます。トラップを有効にしていても、
SIGFPEハンドラを設定していない場合は、トラップされた例外の次の発生時にプログラムが異常終了します。SIGFPEハンドラを設定している場合は、トラップされた例外が次に発生すると、システムは制御をハンドラに渡します。ハンドラは例外が発生した命令のアドレスなどの診断情報を出力し、異常終了するか、実行を再開します。実行を再開して意味のある結果を得るには、次の節で説明する例外処理のための結果をハンドラで設定する必要があります。
ieee_handlerを使用して、5 つの IEEE 浮動小数点例外のすべてのトラップを有効にすると同時に、指定した例外が発生したときにプログラムを異常終了するか、SIGFPEハンドラを設定するように指定することができます。SIGFPEハンドラは、低レベルの関数sigfpe(3)、singal(3c)、sigaction(2) のいずれかを使用して設定することもできますが、ieee_handlerとは異なり、これらの関数ではトラップを有効にできません。浮動小数点例外は、そのトラップが有効である場合のみSIGFPEシグナルを引き起こすことができます。
ieee_handler(3m)
ieee_handlerの呼び出し時の構文は、次の通りです。
i = ieee_handler(action, exception, handler)2 つの入力パラメータ action (動作) と exception (例外) は文字列です。3 つ目のパラメータ handler (ハンドラ) は、型が
sigfpe_handler_typeの関数で、floatingpoint.h(FORTRAN の場合はf77_floatingpoint.h) 内に定義されています。
action char *get、set、clearexception char *invalid、division、overflow、underflow、inexact、all、commonhandler sigfpe_handler_typeユーザー定義ルーチン SIGFPE_DEFAULTSIGFPE_IGNORESIGFPE_ABORT
action が
setの場合、ieee_handlerは exception で指定した例外について handler で指定した処理関数を確立します。処理関数は、SIGFPE_DEFAULTやSIGFPE_IGNORE(いずれもデフォルトの IEEE 動作を選択する)、SIGFPE_ABORT(指定した例外が発生するとプログラムを異常終了させる)、ユーザー定義のサブルーチンのアドレス (指定した例外のいずれかが発生するとサブルーチンを起動する) のいずれかです。ユーザー定義のサブルーチンには、SA_SIGINFOフラグを設定して指定したシグナルハンドラと同じパラメータ (sigaction(2) のマニュアルページを参照) が渡されます。ハンドラがSIGFPE_DEFAULTまたはSIGFPE_IGNOREの場合、ieee_handlerは指定した例外のトラップを無効にします。その他のハンドラの場合、ieee_handlerはトラップを有効にします。x86 プラットフォームでは、例外のトラップが有効になり対応するフラグが発生するたびに、浮動小数点ハードウェアがトラップします。そのため、疑似トラップを防ぐには、ieee_handlerを呼び出してトラップを有効にする前に、プログラムは指定された例外ごとにフラグをクリアする必要があります。action が
clearの場合、ieee_handlerは指定した例外について現在設定されている処理関数を取り消し、例外のトラップを無効にします。これは、SIGFPE_DEFAULTを設定した場合と同じです。action がclearの場合、3 番目のパラメータは無視されます。action が
setおよびclearのいずれの場合も、要求された動作が成功した場合はゼロが返されます。動作が失敗した場合はゼロ以外の値が返されます。action が
getの場合、ieee_handlerは指定した例外について現在設定されているハンドラ (ハンドラが設定されていない場合はSIGFPE_DEFAULT) のアドレスを返します。以下に、
ieee_handlerの使用方法を示すコード例を示します。この C のコードでは、ゼロによる除算が発生した場合はプログラムを終了します。
次の C のコードは、すべての例外について IEEE のデフォルトの例外処理に戻します。
#include <sunmath.h>if (ieee_handler("clear", "all", 0) != 0)printf("could not clear exception handlers\n");
i = ieee_handler('clear', 'all', 0)if (i.ne.0) print *, 'could not clear exception handlers'シグナルハンドラからの例外の報告
ieee_handlerによって設定されたSIGFPEハンドラが呼び出されると、オペレーティングシステムによって、発生した例外の型、例外の原因である命令のアドレス、マシンの整数レジスタおよび浮動小数点レジスタの内容を示す情報が通知されます。ハンドラはこの情報を調査して、例外と例外の発生場所を示すメッセージを出力します。システムから提供される情報にアクセスするには、ハンドラを次のように宣言します。この章の以降の部分では、C のコード例を示します。FORTRAN の
SIGFPEハンドラの例については、付録 Aを参照してください。
#include <siginfo.h>#include <ucontext.h>void handler(int sig, siginfo_t *sip, ucontext_t *uap){...}このハンドラを呼び出すと、送られたシグナルの番号がパラメータ sig に格納されます。シグナル番号は
sys/siginfo.h内で定義されています。SIGFPEのシグナル番号は 8 です。パラメータ sip は、シグナルに関する追加情報を記録する構造体を指します。
SIGFPEシグナルの場合、この構造体の関連メンバーはsip->si_codeおよびsip->si_addrです (sys/siginfo.hを参照してください)。これらのメンバーの重要性は、システムとどのようなイベントでSIGFPEシグナルが発生するかによって異なります。
sip->si_codeメンバーは、表 4-5 に示すSIGFPEシグナルの型のいずれかです。これらはsys/machsig.h内で定義されています。
表 4-5 算術例外の型 FPE_INTDIV
FPE_INTOVF
FPE_FLTRES 不正確FPE_FLTDIV 除算FPE_FLTUND アンダーフローFPE_FLTINV 無効FPE_FLTOVF オーバーフロー
この表に示されているように、各 IEEE 浮動小数点例外の型には
SIGFPEシグナル型が対応しています。整数のゼロによる除算 (FPE_INTDIV) および整数オーバーフロー (FPE_INTOVF) はSIGFPEの型にも含まれていますが、これらは IEEE 浮動小数点例外ではないため、ieee_handlerでハンドラを設定することはできません。これらのSIGFPE型のハンドラはsigfpe(3) で設定できます。ただし、整数オーバーフローは、デフォルトでは SPARC および x86 プラットフォーム のすべてのシステムで無視されます。特殊な命令によってFPE_INTOVF型のSIGFPEシグナルを発生させることができますが、Sun のコンパイラはこのような命令を生成しません。IEEE 浮動小数点例外に対応する
SIGFPEシグナルの場合、sip->si_codeのメンバーは、SPARC システムではどのような例外が発生したかを示しますが、x86 プラットフォームではフラグが立った最も優先度の高い例外を示します (非正規オペランドフラグを除く)。sip->si_addrのメンバーは、SPARC システムでは例外の原因である命令のアドレスを保持し、x86 プラットフォームではトラップされた時点の命令 (例外の原因である命令の次の浮動小数点命令) のアドレスを保持します。最後に、パラメータ uap はトラップされた時点のシステムの状態を記録する構造体を指します。この構造体の内容はシステムによって異なります。メンバーの定義については、
sys/reg.hを参照してください。オペレーティングシステムから提供される情報を使用して、発生した例外の型と例外の原因である命令のアドレスを報告する
SIGFPEハンドラを作成することができます。以下のコード例 4-1 で、このようなハンドラの例を示します。
SPARC システムでは、このプログラムからの出力は次のようになります。
min_normal = 2.22507e-308min_normal / 13.0 = 1.7116e-309max_normal = 1.79769e+308fp exception 4 at address 10d0cmax_normal * max_normal = 1.79769e+308Note: IEEE floating-point exception flags raised:Inexact; Underflow;IEEE floating-point exception traps enabled:overflow; division by zero; invalid operation;See the Numerical Computation Guide, ieee_flags(3M), ieee_handler(3M)[日本語訳]注: 以下の IEEE 浮動小数点例外が発生しました:不正確、アンダーフロー以下の IEEE 浮動小数点例外のトラップが有効です:オーバーフロー、ゼロによる除算、無効な演算詳細は、『数値計算ガイド』のieee_flags(3M),ieee_handler(3M)
に関する説明を参照してください。x86 プラットフォームでは、オペレーティングシステムが累積例外フラグを保存し、
SIGFPEハンドラを呼び出す前にそれをクリアします。ハンドラによって保存されない限り、累積例外フラグはハンドラから戻されたときに失われます。したがって、上記のプログラムからの出力にはアンダーフロー例外が発生したことが示されません。
min_normal = 2.22507e-308min_normal / 13.0 = 1.7116e-309max_normal = 1.79769e+308fp exception 4 at address 8048fe6max_normal * max_normal = 1.79769e+308Note: IEEE floating-point exception traps enabled:overflow; division by zero; invalid operation;See the Numerical Computation Guide, ieee_handler(3M)[日本語訳]注: 以下の IEEE 浮動小数点例外のトラップが有効です:オーバーフロー、ゼロによる除算、無効な演算詳細は、『数値計算ガイド』のieee_handler(3M)に関する説明を参照してくだ さい。多くの場合、トラップが有効であれば、例外の原因である命令が IEEE デフォルトの結果を発生させる必要はありません。上記の出力では、
max_normal * max_normalで通知される値は、オーバーフローする演算のデフォルトの結果 (正確な符号付き無限大) ではありません。通常は、意味のある値を出す計算を続行するために、トラップされた例外の原因である演算の結果を、SIGFPEハンドラが提供する必要があります。その方法の例は、「例外処理」を参照してください。
libm9x.soの例外処理機能を使用して例外を検出するC および C++ プログラムでは、
libm9x.soに含まれる C99 浮動小数点環境関数の例外処理機能を使用し、いくつかの方法で例外を検出できます。これらの拡張機能には、ieee_handlerによる処理とまったく同様に、ハンドラを確立して同時にトラップを有効にする関数が含まれますが、これらの関数はieee_handlerよりも柔軟です。これらの拡張機能は、選択されたファイルに対する、浮動小数点例外についての遡及診断メッセージのログ記録もサポートします。
fex_set_handling(3m)
fex_set_handling関数を使用すると、浮動小数点例外のそれぞれの種類を処理するいつくかのオプション (モード) の 1 つを選択できます。fex_set_handlingの呼び出しの構文は次のとおりです。
ret = fex_set_handling (ex, mode, handler);引数 ex は、呼び出しを適用する一連の例外を示します。この引数は、次の表 4-6 の最初の列に示された値のビット単位の論理和でなければなりません。これらの値は、
fenv.hで定義されています。
表 4-6 fex_set_handlingの例外コードFEX_INEXACT不正確な結果 FEX_UNDERFLOWアンダーフロー FEX_OVERFLOWオーバーフロー FEX_DIVBYZEROゼロ除算 FEX_INV_ZDZ0/0 無効演算 FEX_INV_IDI無限大/無限大の無効演算 FEX_INV_ISI無限大-無限大の無効演算 FEX_INV_ZMI0 x 無限大の無効演算 FEX_INV_SQRT負の数の平方根 FEX_INV_SNANシグナルを発生する NaN に対する演算 FEX_INV_INT無効な整数変換 FEX_INV_CMP無効な非順序付け比較
fenv.hは、便宜上、FEX_NONE(例外なし)、FEX_INVALID(すべての無効な演算例外)、FEX_COMMON(オーバーフロー、ゼロ除算、およびすべての無効な演算)、およびFEX_ALL(すべての例外) の各値も定義しています。引数 mode は、示された例外について例外処理モードを確立することを指定します。次に可能な 5 つのモードを示します。
FEX_NONSTOPモードは、IEEE 754 のデフォルトの連続動作を提供します。これは、例外のトラップを無効にしておくのと同じです。ieee_handlerと異なり、fex_set_handlingでは、無効演算例外の一定の種類に対しデフォルト以外の処理を確立し、残る種類に IEEE のデフォルト処理を当てることができます。FEX_NOHANDLERモードは、ハンドラを提供せずに例外のトラップを有効にするのと同じです。例外が発生すると、システムは、前回インストールしたSIGFPEハンドラが存在する場合はこのハンドラに制御を渡し、存在しない場合は処理を中止します。FEX_ABORTモードでは、例外が発生するとプログラムはabort(3c) を呼び出します。FEX_SIGNALは、示された例外に対し、引数 handler で指定された処理関数をインストールします。これらの例外のどれかが発生すると、ieee_handlerによってインストールされたかのように、同じ引数によってハンドラが呼び出されます。FEX_CUSTOMは、示された例外に対し、handler で指定された処理関数をインストールします。FEX_SIGNALモードと異なり、例外が発生すると、簡略化された引数リストによってハンドラが呼び出されます。この引数は、整数 (値は表 4-6 に示された値の 1 つ) と、例外の原因となった演算についての追加情報を記録する構造体を指すポインタから構成されます。この構造体の内容は、次の節とfex_set_handling(3m) のマニュアルページで説明されています。指定された mode が
FEX_NONSTOP、FEX_NOHANDLER、またはFEX_ABORTである場合は、handler パラメータは無視されることに注意してください。fex_set_handlingは、示された例外に対して指定されたモードが確立される場合はゼロ以外を返し、それ以外ではゼロを返します (次の例では、戻り値は無視される)。次の例は、
fex_set_handlingを使用して特定の種類の例外を検出する方法を示しています。0/0 例外を停止するには、次のように記述します。
fex_set_handling(FEX_INV_ZDZ, FEX_ABORT, NULL);オーバーフローとゼロ除算に対して
SIGFPEハンドラをインストールするには、次のように記述します。
fex_set_handling(FEX_OVERFLOW | FEX_DIVBYZERO, FEX_SIGNAL,
handler);前記の例では、前の節で示しているように、
SIGFPEハンドラに対する sip パラメータを介して供給される診断情報を出力できました。一方、次の例では、FEX_CUSTOMモードでインストールされたハンドラに供給される例外についての情報を出力します (詳細は、fex_set_handling(3m) のマニュアルページを参照してください。
コード例 4-2 FEX_CUSTOMモードでインストールされたハンドラに供給される情報の出力
#include <fenv.h> void handler(int ex, fex_info_t *info){switch (ex) { case FEX_OVERFLOW:printf("Overflow in "); break; case FEX_DIVBYZERO:printf("Division by zero in "); break; default:printf("Invalid operation in "); }switch (info->op) { case fex_add:printf("floating point add\n"); break; case fex_sub:printf("floating point subtract\n"); break; case fex_mul:printf("floating point multiply\n"); break; case fex_div:printf("floating point divide\n"); break; case fex_sqrt:printf("floating point square root\n"); break; case fex_cnvt:printf("floating point conversion\n"); break; case fex_cmp:printf("floating point compare\n"); break; default:printf("unknown operation\n"); }switch (info->op1.type) { case fex_int:printf("operand 1: %d\n", info->op1.val.i); break; case fex_llong:printf("operand 1: %lld\n", info->op1.val.l); break; case fex_float:printf("operand 1: %g\n", info->op1.val.f); break; case fex_double:printf("operand 1: %g\n", info->op1.val.d); break; case fex_ldouble:printf("operand 1: %Lg\n", info->op1.val.q); break; }switch (info->op2.type) { case fex_int:printf("operand 2: %d\n", info->op2.val.i); break; case fex_llong:printf("operand 2: %lld\n", info->op2.val.l); break; case fex_float:printf("operand 2: %g\n", info->op2.val.f); break; case fex_double:printf("operand 2: %g\n", info->op2.val.d); break; case fex_ldouble:printf("operand 2: %Lg\n", info->op2.val.q); break; } } ... fex_set_handling(FEX_COMMON, FEX_CUSTOM, handler);上記例のハンドラは、発生した例外の種類、原因となった演算の種類、およびオペランドを報告します。このハンドラは、例外の発生場所は示しません。例外の発生場所を見つけるには、遡及診断を使用できます。
遡及診断
libm9x.soの例外処理機能を使用して例外を検出するもう 1 つの方法として、浮動小数点例外についての遡及診断メッセージのログ記録を有効にできます。遡及診断のログ記録を有効にすると、システムは特定の例外についての情報を記録します。この情報には、例外の種類、その原因となった命令のアドレス、その処理方法、およびデバッガによって生成されるものに類似したスタックトレースが含まれます。遡及診断メッセージに記録されるスタックトレースには、命令のアドレスと関数名しか示されません。行番号、ソースファイル名、引数の値のようなほかのデバッグ情報を調べるには、デバッガを使用する必要があります。遡及診断ログには、発生する例外ごとの情報は含まれません。例外ごとの情報を記録するとなると、巨大なログとなり、異常な例外を抜き出すことは不可能になります。代わりに、ロギング機構は冗長なメッセージは取り除きます。メッセージは、次の 2 つの状況のいずれかにある場合、冗長と見なされます。
具体的には、ほとんどのプログラムでは、例外のそれぞれの種類が最初に発生する場合だけログに記録されます。ある例外に対して
FEX_NONSTOP処理モードが有効な場合、任意の C99 浮動小数点環境関数を使用してそのフラグをクリアすると、その例外の次の発生は、直前にログ記録された位置での発生ではない場合だけログに記録されます。ログ記録を有効にするには、
fex_set_log関数を使用してメッセージを転送するファイルを指定します。たとえば、メッセージを標準のエラーファイルに記録するには、次のように記述します。
fex_set_;pg(stderr);次の例では、遡及診断のログ記録を、前の節に示されている共有オブジェクトのプリロード機能と組み合わせています。次の C ソースファイルを作成し、それを共有オブジェクトにコンパイルし、
LD_PRELOAD環境変数内でそのパス名を指定してその共有オブジェクトをプリロードし、FTRAP環境変数で 1 つ以上の例外の名前をコンマで区切って指定すると、指定した例外の発生時にプログラムを停止すると同時に、各例外がどこで発生したかを示す遡及診断出力を得ることができます。
コード例 4-3 遡及診断のログ記録と共有オブジェクトのプリロード機能との組み合わせ
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fenv.h>static struct ftrap_string { const char *name; int value;} ftrap_table[] = {{ "inexact", FEX_INEXACT },{ "division", FEX_DIVBYZERO },{ "underflow", FEX_UNDERFLOW },{ "overflow", FEX_OVERFLOW },{ "invalid", FEX_INVALID },{ NULL, 0 } }; #pragma init (set_ftrap) void set_ftrap(){ struct ftrap_string *f; char *s, *s0; int ex = 0;if ((s = getenv("FTRAP")) == NULL) return; if ((s0 = strtok(s, ",")) == NULL) return;do {for (f = &trap_table[0]; f->name != NULL; f++) { if (!strcmp(s0, f->name)) ex |= f->value; } } while ((s0 = strtok(NULL, ",")) != NULL); fex_set_handling(ex, FEX_ABORT, NULL); fex_set_log(stderr); }上記のコードをこの節の初めで示しているプログラム例とともに使用すると、次のような結果が出力されます (SPARC の場合)。
example%cc -Kpic -G -ztext init.c -o init.so -R/opt/SUNWspro/lib -L/opt/SUNWspro/lib -lm9x -lcexample%env FTRAP=invalid LD_PRELOAD=./init.soa.outFloating point invalid operation (sqrt) at 0x00010c24 sqrtm1_, abort0x00010c30 sqrtm1_0x00010b48 MAIN_0x00010ccc mainAbortこの出力は、ルーチン
sqrtm1内の平方根演算の結果として無効な演算例外が発生したことを示しています。上記で触れたように、x86 プラットフォームにおいて共有オブジェクト内の初期化ルーチンからトラップを有効にするには、標準の
__fpstartルーチンを無効にする必要があります。典型的なログ出力を示した例については、付録 Aを参照してください。また、一般的な情報については、
fex_set_log(3m) のマニュアルページを参照してください。例外処理
歴史的に、(さまざまな理由から) 数値計算ソフトウェアは例外を考慮せずに作成されてきました。また、多くのプログラマは、例外が発生するとプログラムがただちに異常終了するという環境に慣れていました。現在では、LAPACK などの高品質なソフトウェアパッケージでは、ゼロによる除算や無効な演算などの例外を回避し、入力を基準化 (スケール) してオーバーフローや結果が不正確になる可能性のあるアンダーフローを除外するように設計されています。ただし、このように例外を処理する方法は、どのような状況でも適切であるというわけではありません。例外を無視すると、あるプログラマが作成したプログラムやサブルーチンを、(ソースコードにアクセスできない) 他のプログラマが使用する場合に、問題となる場合があります。すべての例外を回避しようとすると、多くのテストと分岐が必要になり、非常に手間がかかります (Demmel、Li 共著『Faster Numerical Algorithms via Exception Handling』IEEE Trans. Comput. 43、1994 年), pp. 983-992 を参照)。
第 3 の選択肢として、IEEE 算術演算のデフォルトの例外応答や状態フラグ、およびオプションのトラップ機能によって、例外が発生しても計算を続行して後で例外を検出するか、発生時に解釈および処理することができます。前述のように、
ieee_flagsや C99 浮動小数点環境関数を使用して後で例外を検出したり、ieee_handlerやfex_set_handlingを使用してトラップを有効にし、発生時に例外を解釈するSIGFPEハンドラを設定することができます。計算を続行するために、IEEE 規格では、例外の原因となった演算の結果をトラップハンドラで指定するように推奨しています。FEX_SIGNALモードでieee_handlerまたはfex_set_handlingを介してインストールされるSIGFPEハンドラは、Solaris オペレーティング環境がシグナルハンドラに供給している uap パラメータを使用して指定できます。fex_set_handlingを介してインストールされるFEX_CUSTOMモードハンドラは、このようなハンドラに供給される info パラメータを使用して結果を提供できます。C では、
SIGFPEシグナルハンドラは次のように宣言します。
#include <siginfo.h>#include <ucontext.h>void handler(int sig, siginfo_t *sip, ucontext_t *uap){...}トラップされた浮動小数点例外の結果として
SIGFPEシグナルハンドラが呼び出されると、uap パラメータは、そのコンピュータの整数レジスタおよび浮動小数点レジスタのコピーや、その他の例外が記述されているシステム依存の情報を格納したデータ構造体を指します。このシグナルハンドラが正常に返されると、保存されたデータが復元され、トラップが行われた箇所からプログラムの実行が再開されます。このように、例外を記述したデータ構造体の情報にアクセスして解読し、可能であれば保存されたデータを変更することによって、SIGFPEハンドラは例外演算の結果をユーザーが指定した値に置換して計算を続行することができます。
FEX_CUSTOMモードハンドラは、次の方法で宣言できます。
#include <fenv.h>void handler(int ex, fex_info_t *info){...}
FEX_CUSTOMハンドラが呼び出されるとき、ex パラメータはどの種類の例外が発生したか (表 4-6 に挙げられた値の 1 つ) を示し、info パラメータはその例外の詳細情報を含むデータ構造を示します。このデータ構造は、例外の発生原因である算術演算を表現するコードと、オペランドを記録する構造体 (利用できる場合) を含みます。また、例外がトラップされない場合に置換されていたはずのデフォルトの結果を記録する構造体と、発生したはずの例外フラグのビット単位の論理和を保持する整数値も含みます。ハンドラは、この 2 つの情報を変更して異なる結果に置き換えたり、発生したフラグのセットを変更したりできます。これらのデータを変更することなくハンドラが戻る場合、プログラムは、例外がトラップされないかのように、デフォルトのトラップされない結果とフラグを使用して継続します。次の節に、アンダーフローまたはオーバーフローになる演算を基準化 (スケール) された結果に置換する方法を示します。詳細は、付録 Aを参照してください。
IEEE トラップされたアンダーフローおよびオーバーフローの置換
IEEE 規格では、アンダーフローおよびオーバーフローがトラップされた場合、指数部がラップ (wrap) された結果をトラップハンドラによって置換できるような方法をシステムで提供するように推奨されています。指数部がラップされた結果は、指数部がその通常の範囲を超えてラップされているということを除けば、その値はオーバーフローまたはアンダーフローを起こさずに演算が行われた場合の結果と一致します。つまり、値が 2 のべき乗によって基準化 (スケール) されているということです。以降の計算でアンダーフローやオーバーフローが発生しないように、指数範囲の中央にできるだけ近くにアンダーフローまたはオーバーフローとなった結果を割り当てるような倍率が選択されます。発生したアンダーフローやオーバーフローの回数を追跡することによって、プログラムは最終的な結果を基準化 (スケール) し、ラップされた指数を補正することができます。また、有効な浮動小数点フォーマットの範囲を超えてしまうような計算において、正確な結果を出すことができます (P. Sterbenz 著『Floating-Point Computation』を参照)。
SPARC アーキテクチャのシステムでは、浮動小数点命令がトラップされた例外の原因である場合、システムは宛先レジスタを変更しません。このため、指数がラップされた結果を置換するには、アンダーフローまたはオーバーフローのハンドラが命令をデコードし、オペランドレジスタを調べ、基準化 (スケール) された結果自体を生成しなければなりません。次の例では、この手順を実行するハンドラを示します。このハンドラを UltraSPARC システムでコンパイルされたコードで使用するには、Solaris 2.6、Solaris 7、または Solaris 8 を実行するシステム 上でこのハンドラをコンパイルし、プリプロセッサトークン
V8PLUSを定義します。
コード例 4-4 SPARC システムでの、IEEE トラップされたアンダーフローおよびオーバーフローの置換 #include <stdio.h> #include <ieeefp.h> #include <math.h> #include <sunmath.h> #include <siginfo.h> #include <ucontext.h> #ifdef V8PLUS/* 上位 32 ビットの浮動小数点レジスタは、uap->uc_mcontext.xrs.xrs_prt
によって示される領域に格納されます。このポインタは、
uap->mcontext.xrs.xrs_id == XRS_ID
(sys/procfs.h で定義されている) の場合のみ有効です。 */ #include <assert.h> #include <sys/procfs.h> #define FPxreg(x) ((prxregset_t*)uap->uc_mcontext.xrs.xrs_ptr) ->pr_un.pr_v8p.pr_xfr.pr_regs[(x)] #endif #define FPreg(x) uap->uc_mcontext.fpregs.fpu_fr.fpu_regs[(x)] /* * トラップされたアンダーフローまたはオーバーフローについて、* IEEE 754 のデフォルトの結果が提供されます */ void ieee_trapped_default(int sig, siginfo_t *sip, ucontext_t *uap){ unsigned instr, opf, rs1, rs2, rd; long double qs1, qs2, qd, qscl; double ds1, ds2, dd, dscl; float fs1, fs2, fd, fscl; /* 例外の原因となっている命令を取得 */ instr = uap->uc_mcontext.fpregs.fpu_q->FQu.fpq.fpq_instr; /* 演算コード、ソース、宛先レジスタ番号を抽出 */ opf = (instr >> 5) & 0x1ff; rs1 = (instr >> 14) & 0x1f; rs2 = instr & 0x1f; rd = (instr >> 25) & 0x1f; /* オペランドを取得 */switch (opf & 3) { case 1: /* single precision */ fs1 = *(float*)&FPreg(rs1); fs2 = *(float*)&FPreg(rs2); break; case 2: /* 倍精度 */ #ifdef V8PLUS if (rs1 & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); ds1 = *(double*)&FPxreg(rs1 & 0x1e); } else ds1 = *(double*)&FPreg(rs1); if (rs2 & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); ds2 = *(double*)&FPxreg(rs2 & 0x1e); } else ds2 = *(double*)&FPreg(rs2); #else ds1 = *(double*)&FPreg(rs1); ds2 = *(double*)&FPreg(rs2); #endif break; case 3: /* 4 倍精度 */ #ifdef V8PLUS if (rs1 & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); qs1 = *(long double*)&FPxreg(rs1 & 0x1e); } else qs1 = *(long double*)&FPreg(rs1); if (rs2 & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); qs2 = *(long double*)&FPxreg(rs2 & 0x1e); } else qs2 = *(long double*)&FPreg(rs2); #else qs1 = *(long double*)&FPreg(rs1); qs2 = *(long double*)&FPreg(rs2); #endif break; } /* 倍率を設定 */if (sip->si_code == FPE_FLTOVF) { fscl = scalbnf(1.0f, -96); dscl = scalbn(1.0, -768); qscl = scalbnl(1.0, -12288);} else { fscl = scalbnf(1.0f, 96); dscl = scalbn(1.0, 768); qscl = scalbnl(1.0, 12288); } /* トラップを無効にして基準化(スケール)された結果を生成 */ fpsetmask(0);switch (opf) { case 0x41: /* 単精度の加算 */ fd = fscl * (fscl * fs1 + fscl * fs2); break; case 0x42: /* 倍精度の加算 */ dd = dscl * (dscl * ds1 + dscl * ds2); break; case 0x43: /* 4 倍精度の加算 */ qd = qscl * (qscl * qs1 + qscl * qs2); break; case 0x45: /* 単精度の減算 */ fd = fscl * (fscl * fs1 - fscl * fs2); break; case 0x46: /* 倍精度の減算 */ dd = dscl * (dscl * ds1 - dscl * ds2); break; case 0x47: /* 4 倍精度の減算 */ qd = qscl * (qscl * qs1 - qscl * qs2); break; case 0x49: /* 単精度の乗算 */ fd = (fscl * fs1) * (fscl * fs2); break; case 0x4a: /* 倍精度の乗算 */ dd = (dscl * ds1) * (dscl * ds2); break; case 0x4b: /* 4 倍精度の乗算 */ qd = (qscl * qs1) * (qscl * qs2); break; case 0x4d: /* 単精度の除算 */ fd = (fscl * fs1) / (fs2 / fscl); break; case 0x4e: /* 倍精度の除算 */ dd = (dscl * ds1) / (ds2 / dscl); break; case 0x4f: /* 4 倍精度の除算 */ qd = (qscl * qs1) / (qs2 / dscl); break; case 0xc6: /* 倍精度を単精度に変換 */ fd = (float) (fscl * (fscl * ds1)); break; case 0xc7: /* 4 倍精度を単精度に変換 */ fd = (float) (fscl * (fscl * qs1)); break; case 0xcb: /* 4 倍精度を倍精度に変換 */ dd = (double) (dscl * (dscl * qs1)); break; } /* 宛先に結果を格納 */if (opf & 0x80) { /* 変換演算 */if (opf == 0xcb) { /* 4 倍精度を倍精度に変換 */ #ifdef V8PLUS if (rd & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); *(double*)&FPxreg(rd & 0x1e) = dd; } else *(double*)&FPreg(rd) = dd; #else *(double*)&FPreg(rd) = dd; #endif } else /* 4 倍精度/倍精度を単精度に変換 */ *(float*)&FPreg(rd) = fd;} else { /* 算術演算 */switch (opf & 3) { case 1: /* 単精度 */ *(float*)&FPreg(rd) = fd; break; case 2: /* 倍精度 */ #ifdef V8PLUS if (rd & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); *(double*)&FPxreg(rd & 0x1e) = dd; } else *(double*)&FPreg(rd) = dd; #else *(double*)&FPreg(rd) = dd; #endif break; case 3: /* 4 倍精度 */ #ifdef V8PLUS if (rd & 1){ assert(uap->uc_mcontext.xrs.xrs_id == XRS_ID); *(long double*)&FPxreg(rd & 0x1e) = qd; } else *(long double*)&FPreg(rd & 0x1e) = qd; #else *(long double*)&FPreg(rd & 0x1e) = qd; #endif break; } } } int main(){ volatile float a, b; volatile double x, y;ieee_handler("set", "underflow", ieee_trapped_default);ieee_handler("set", "overflow", ieee_trapped_default); a = b = 1.0e30f; a *= b; /* オーバーフロー ; 適切な数にラップされる */ printf( "%g\n", a ); a /= b; printf( "%g\n", a ); a /= b; /* アンダーフロー ; 逆方向にラップされる */ printf( "%g\n", a ); x = y = 1.0e300; x *= y; /* オーバーフロー ; 適切な数にラップされる */ printf( "%g\n", x ); x /= y; printf( "%g\n", x ); x /= y; /* アンダーフロー ; 逆方向にラップされる */ printf( "%g\n", x ); ieee_retrospective(stdout); return 0; }
この例で変数
a、b、x、およびyがvolatileと宣言されているのは、コンパイラがコンパイル時にa * bなどを評価することを防ぐためにすぎません。通常の使用では、volatile宣言は必要ありません。
159.3091.59309e-2814.14884e+1374.14884e-1631Note: IEEE floating-point exception traps enabled:underflow; overflow;See the Numerical Computation Guide, ieee_handler(3M)[日本語訳]注: 以下の IEEE 浮動小数点例外のトラップが有効です:アンダーフロー、オーバーフロー詳細は、『数値計算ガイド』のieee_handler(3M)に関する説明を参照してくだ さい。x86 では、浮動小数点命令によってアンダーフローまたはオーバーフローがトラップされ、その宛先がレジスタである場合、浮動小数点ハードウェアによって指数がラップされた結果が提供されます。ただし、浮動小数点のストア命令でアンダーフローまたはオーバーフローがトラップされる時には、ハードウェアはストアが未完了のままトラップを行います。また、その命令がストアおよびポップの命令である場合には、スタックのポップも行われません。このため、ストア命令においてトラップが発生した時のアンダーフローまたはオーバーフローの数を追跡するには、アンダーフローまたはオーバーフローのハンドラが基準化 (スケール) された結果を生成し、スタックを修正する必要があります。このようなハンドラを、以下の例で示します。
コード例 4-5 x86 システムでの IEEE トラップされたアンダーフローおよびオーバーフローの置換 #include <stdio.h> #include <ieeefp.h> #include <math.h> #include <sunmath.h> #include <siginfo.h> #include <ucontext.h> /* 保存された fp 環境へのオフセット */ #define CW 0 /* 制御ワード */ #define SW 1 /* ステータスワード */ #define TW 2 /* タグワード */ #define OP 4 /* 演算コード */ #define EA 5 /* オペランドのアドレス */ #define FPenv(x) uap->uc_mcontext.fpregs.fp_reg_set.fpchip_state.state[(x)] #define FPreg(x) *(long double *)(10*(x)+(char*)&uap->uc_mcontext.fpregs.fp_reg_set.fpchip_state.state[7]) /* * トラップされたアンダーフローまたはオーバーフローについて* IEEE 754 デフォルトの結果を提供 */ void ieee_trapped_default(int sig, siginfo_t *sip, ucontext_t *uap){ double dscl; float fscl; unsigned sw, op, top; int mask, e; /* トラップされなかった例外のフラグを保存 */ sw = uap->uc_mcontext.fpregs.fp_reg_set.fpchip_state.status; FPenv(SW) |= (sw & (FPenv(CW) & 0x3f)); /* 例外となった命令が記憶領域にある場合、スタックを一番上に
基準化(スケール)して格納し、必要に応じてスタックをポップ */ fpsetmask(0); op = FPenv(OP) >> 16;switch (op & 0x7f8) { case 0x110: case 0x118: case 0x150: case 0x158: case 0x190: case 0x198: fscl = scalbnf(1.0f, (sip->si_code == FPE_FLTOVF)?-96 : 96); *(float *)FPenv(EA) = (FPreg(0) * fscl) * fscl;if (op & 8) { /* スタックをポップする */ FPreg(0) = FPreg(1); FPreg(1) = FPreg(2); FPreg(2) = FPreg(3); FPreg(3) = FPreg(4); FPreg(4) = FPreg(5); FPreg(5) = FPreg(6); FPreg(6) = FPreg(7); top = (FPenv(SW) >> 10) & 0xe; FPenv(TW) |= (3 << top); top = (top + 2) & 0xe; FPenv(SW) = (FPenv(SW) & ~0x3800) | (top << 10); } break; case 0x510: case 0x518: case 0x550: case 0x558: case 0x590: case 0x598: dscl = scalbn(1.0, (sip->si_code == FPE_FLTOVF)?-768 : 768); *(double *)FPenv(EA) = (FPreg(0) * dscl) * dscl;if (op & 8) { /* スタックをポップする */ FPreg(0) = FPreg(1); FPreg(1) = FPreg(2); FPreg(2) = FPreg(3); FPreg(3) = FPreg(4); FPreg(4) = FPreg(5); FPreg(5) = FPreg(6); FPreg(6) = FPreg(7); top = (FPenv(SW) >> 10) & 0xe; FPenv(TW) |= (3 << top); top = (top + 2) & 0xe; FPenv(SW) = (FPenv(SW) & ~0x3800) | (top << 10); } break; } } int main(){ volatile float a, b; volatile double x, y;ieee_handler("set", "underflow", ieee_trapped_default);ieee_handler("set", "overflow", ieee_trapped_default); a = b = 1.0e30f; a *= b; printf( "%g\n", a ); a /= b; printf( "%g\n", a ); a /= b; printf( "%g\n", a ); x = y = 1.0e300; x *= y; printf( "%g\n", x ); x /= y; printf( "%g\n", x ); x /= y; printf( "%g\n", x ); ieee_retrospective(stdout); return 0; }
SPARC アーキテクチャのシステムでは、上記のプログラムの出力は次のようになります。
159.3091.59309e-2814.14884e+1374.14884e-1631Note: IEEE floating-point exception traps enabled:underflow; overflow;See the Numerical Computation Guide, ieee_handler(3M)[日本語訳]注: 以下の IEEE 浮動小数点例外のトラップが有効です:アンダーフロー、オーバーフロー詳細は、『数値計算ガイド』のieee_handler(3M)に関する説明を参照してくだ さい。C および C++ プログラムでは、
libm9x.soに含まれるfex_set_handling関数を使用して、アンダーフローおよびオーバーフローに対するFEX_CUSTOMハンドラをインストールできます。SPARC システムでは、このようなハンドラに供給される情報には例外の原因である演算とオペランドが常に含まれています。上記に示しているように、ハンドラは、この情報を使用して IEEE の指数がラップされた結果を計算できます。x86 では、例外を引き起こした演算、および超越命令の 1 つが例外を発生させる時点 (info->opパラメータがfex_otherに設定されるなど。説明はfenv.hファイルを参照) を、提供される情報が常に示すとはかぎりません。また、x86 ハードウェアは指数がラップされた結果を自動的に提供するため、例外を発生させている命令の宛先が浮動小数点レジスタである場合は、オペランドの 1 つが上書きされる場合があります。
fex_set_handling機能を使用すると、FEX_CUSTOMモードでインストールされているハンドラは、アンダーフローまたはオーバーフローする演算を IEEE 指数がラップされた結果に容易に置換できます。これらの例外のどちらかがトラップされる場合、指数がラップされた結果を配布することを示すため、ハンドラは次のようにセットできます。
info->res.type = fex_nodata;
#include <stdio.h>#include <fenv.h>void handler(int ex, fex_info_t *info) {info->res.type = fex_nodata;}int main(){volatile float a, b;volatile double x, y;fex_set_log(stderr);fex_set_handling(FEX_UNDERFLOW | FEX_OVERFLOW, FEX_CUSTOM,handler);a = b = 1.0e30f;a *= b; /* オーバーフロー ; 適切な数値にラップされる */printf("%g\n", a);a /= b;printf("%g\n", a);a /= b; /* アンダーフロー ; 逆方向にラップされる */printf("%g\n", a);x = y = 1.0e300;x *= y; /* オーバーフロー ; 適切な数値にラップされる */printf("%g\n", x);x /= y;printf("%g\n", x);x /= y; /* アンダーフロー ; 逆方向にラップされる */printf("%g\n", x);return 0;}
Floating point overflow at 0x00010924 main, handler: handler0x00010928 main159.3091.59309e-28Floating point underflow at 0x00010994 main, handler: handler0x00010998 main1Floating point overflow at 0x000109e4 main, handler: handler0x000109e8 main4.14884e+1374.14884e-163Floating point underflow at 0x00010a4c main, handler: handler0x00010a50 main1
|
サン・マイクロシステムズ株式会社 Copyright information. All rights reserved. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |