数値計算ガイド ホーム目次前ページへ次ページへ索引


第 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 の浮動小数点例外は、無効な演算、ゼロ除算、オーバーフロー、アンダーフロー、および不正確の 5 種類があります。最初の 3 つ (無効な演算、ゼロ除算、オーバーフロー) の例外は共通の例外と呼ばれており、無視することはできません。ieee_handler(3m) には共通の例外だけをトラップする簡単な方法が提供されています。他の 2 つの例外 (アンダーフロー、不正確) はより頻繁に発生します。実際、ほとんどの浮動小数点演算に不正確例外を発生しており、ほとんどの場合は無視できます。

表 4-1 は IEEE 標準規格 754 の内容を要約したものです。5 種類の浮動小数点例外および例外発生時の IEEE 演算機能環境のデフォルトの応答を定義しています。

表 4-1   IEEE 浮動小数点例外  
IEEE 例外
例外の発生理由
例
トラップ未設定時の
デフォルト結果
無効な演算 実行しようとする演算に対してオペランドが無効 0 ×
0/ 0
/
X REM 0
シグナルを発生
しない NaN

(x86 では、浮動小数点スタックがアンダーフローまたはオーバーフローする場合にもこの例外が発生する。しかし、このことは IEEE 規格には含まれない)
負のオペランドの平方根
シグナルを発生する NaN オペランドを持つ任意の演算
非順序付け比較 (注 1 を参照)
無効な変換 (注 2 を参照)

ゼロによる除算
有限オペランドに対する演算により結果が純無限数となっている
有限でゼロでない x に対する
x /0
log(0)

正しい符号の無限大
オーバーフロー 正しく丸めを行なった結果、移動先の形式で表現可能な最大数を超えている (指数部分を超えた) 倍精度:
DBL_MAX + 1.0e294
exp(709.8)
単精度:
(float)DBL_MAX
FLT_MAX
+ 1.0e32
expf(88.8)
丸めモード (RM) と中間結果の符号に依存する
アンダーフロー 正確な結果、正しく丸められた結果のどちらも、宛先の形式で表現可能な最小の正規数よりも絶対値が小さい (注 3 を参照)
倍精度:
nextafter(min_normal,-·)
nextafter(min_subnormal,-·)
DBL_MIN �3.0
exp(-708.5)
単精度:
(float)DBL_MIN
nextafterf
(FLT_MIN, -·)
expf(-87.4)
非正規数または 0
不正確 丸められた有効な演算結果が、真の演算結果と異なる
(ほとんどの浮動小数点演算ではこの例外が発生する)
2.0/3.0
(float)1.12345678
log(1.1)
DBL_MAX + DBL_MAX,
オーバーフローがトラップされないとき
演算結果
(丸め、オーバーフロー、またはアンダーフローなど)


表 4-1 の注

  1. 非順序付け比較:
    任意の浮動小数値の組は、形式が異なっていても比較することができます。次の 4 つの相互に排他的な比較演算 (より小さい、より大きい、等しい、および非順序付け) が可能です。非順序付けとは、オペランドのうち少なくとも 1 つが NaN (非数) であることを意味します。

    それぞれの NaN は、その NaN 自体も含めてすべての値に対して非順序付けで比較します。次の表は非順序付け関係のとき、どの演算子が無効な演算例外を発生するかを示したものです。

表 4-2   非順序付け比較
演算子 無効な例外 (非順序付けの場合)
数学 C, C++ F77
= == .EQ. 無効
≠ != .NE. 無効
> > .GT. 無効ではない
≧ >= .GE. 無効ではない
< < .LT. 無効ではない
≦ <= .LE. 無効ではない


  1. 無効な変換:
    NaN、または無限大から整数に変換しようとすること。または浮動小数点形式からの変換時に発生した整数値オーバーフロー。

  2. IEEE の単精度、倍精度、および拡張倍精度の形式で表現可能な最小の正規数は、それぞれ 2-126、2-1022、2-16382 です。IEEE の浮動小数点形式については、第 2 章を参照してください。

x86 浮動小数点環境には、IEEE 規格にはない例外 (指数が最小の非正規数オペランド例外) があります。この例外は、浮動小数点演算が非正規数に対して実行された場合に発生します。

例外の優先順位は次のとおりです。

表 4-3   例外の優先順位  
x86 SPARC および PowerPC
無効 無効
オーバーフロー オーバーフロー
除算 除算
アンダーフロー アンダーフロー
不正確 不正確
非正規数


同時に発生する可能性のある標準的な例外は、オーバーフローと不正確、およびアンダーフローと不正確だけです。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 out
      call ieee_flags('clear', 'exception', 'overflow', out)

C または C++ では、例外が発生したかどうかの問い合わせは以下のようにします。

      i = ieee_flags("get", "exception", in, out);

処理が特定できた場合に出力パラメータ out に返される文字列は、以下の通りです。

FORTRAN 呼び出しにおける例を示します。

      character*8 out
      i = 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 ファイルは、それぞれの例外に対応するビット位置を定義します。このビット位置はマシンによって異なり、連続 (隣接) している必要はありません。

表 4-4   例外ビット
例外 ビット位置 例外ビット
無効 fp_invalid i & (1 << fp_invalid)
オーバーフロー fp_overflow i & (1 << fp_overflow)
除算 fp_division i & (1 << fp_division)
アンダーフロー fp_underflow i & (1 << fp_underflow)
不正確 fp_inexact i & (1 << fp_inexact)
非正規数 fp_denormalized i & (1 << fp_denormalized)
(x86 のみ)


下記の C または C++ のプログラムの一部は、i の値をデコードする方法を示しています。

/*
 *	すべての累積例外を示す整数値をデコードします。
 *	fp_inexact などは、<sys/ieeefp.h> 内に定義されています。
 */

 
char *out;
int invalid, division, overflow, underflow, inexact;
code = ieee_flags("get", "exception", "", &out);
printf ("out is %s, code is %d, in hex: 0x%08X\n",
         out, code, code);
inexact	=	(code >> fp_inexact)	& 0x1; 
division	=	(code >> fp_division)	& 0x1; 
underflow	=	(code >> fp_underflow)	& 0x1; 
overflow	=	(code >> fp_overflow)	& 0x1; 
invalid	=	(code >> fp_invalid)	& 0x1; 
printf("%d %d %d %d %d \n", invalid, division, overflow,
		underflow, inexact); 

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 つの関数を使用した例を示します。

fexcept_t flags;

 
/* アンダーフロー、オーバーフロー、および不正確のフラグを保存します */
fegetexceptflag(&flags, FE_UNDERFLOW | FE_OVERFLOW | FE_INEXACT);
/* これらのフラグをクリアします */
feclearexcept(FE_UNDERFLOW | FE_OVERFLOW | FE_INEXACT);
/* アンダーフローまたはオーバーフローの可能性のある演算を行います */
...
/* アンダーフローまたはオーバーフローを調べます */
if (fetestexcept(FE_UNDERFLOW | FE_OVERFLOW) != 0) {
...
}
/* アンダーフロー、オーバーフロー、および不正確のフラグを復元します */
fesetexceptflag(&flags, FE_UNDERFLOW | FE_OVERFLOW, | FE_INEXACT);

例外の特定

プログラマが例外を考慮してプログラムを作成していないために、例外が検出されたときに、どこで例外が発生したのかが問題になることがよくあります。例外が発生した場所を特定する方法の 1 つは、プログラム中のさまざまな箇所で例外フラグをテストすることですが、この方法で正確に例外を特定するには、多くのテストと労力が必要になります。

例外の発生した場所を特定する簡単な方法は、例外トラップを有効にすることです。トラップが有効で、ある例外が発生すると、オペレーティングシステムは、SIGFPE シグナルを送ってプログラムに通知します (詳細は、signal(5) のマニュアルページを参照してください)。例外のトラップを有効にすると、デバッガで実行して SIGFPE シグナルを受信した時点でプログラム終了するか、または例外が発生した命令のアドレスを出力するように SIGFPE ハンドラを設定して、例外の発生箇所を特定することができます。SIGFPE シグナルを生成する例外についてはトラップを有効にしておく必要があります。トラップが無効になっている場合に例外が発生すると、対応するフラグが設定され、プログラムの実行は 表 4-1 に示されているデフォルトの結果で継続されてますが、シグナルは送られません。

デバッガを使用して例外を特定する

この節では、dbx (ソースレベルのデバッガ) と adb (アセンブリレベルのデバッガ) の使用例を参考にして、浮動小数点例外の原因と、例外発生させた命令を調べます。dbx でソースレベルのデバッグを行うには、プログラムを -g オプション付きでコンパイルする必要があります。詳細は、『dbx コマンドによるデバッグ』マニュアルを参照してください。

次のプログラムを見てみます。

      program ex
      double precision x,y sqrtm1
      x = -4.2d0 
      y = sqrtm1(x) 
      print * , x, y 
      end 

 
      double precision function sqrtm1(x) 
      double precision x 
      routine = sqrt(x) - 1.0d0 
      return 
      end 

このプログラムをコンパイルして実行すると、次のように出力されます。

   -4.2000000000000 NaN                   
Note: IEEE floating-point exception flags raised: 
   Inexact; Invalid Operation; 
See the Numerical Computation Guide, ieee_flags(3M)  

 
[日本語訳]
注: 以下の IEEE 浮動小数点例外が発生しました:
	不正確、無効な演算
詳細は、『数値計算ガイド』の ieee_flags(3M) に関する説明を参照してくださ
い。

無効な演算の原因を突き止めるには、無効な演算についてのトラップを有効にするために、-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 コマンドを実行し、プログラムを実行します。結果は次のようになります。

example% dbx a.out
Reading symbolic information for a.out
Reading symbolic information for rtld /usr/lib/ld.so.1
Reading symbolic information for libF77.so.3
Reading symbolic information for libsunmath.so.1
Reading symbolic information for libm.so.1
Reading symbolic information for libc.so.1
Reading symbolic information for libdl.so.1
(dbx) catch fpe
(dbx) run
Running: a.out
(process id 17516)
signal FPE (invalid floating point operation) in sqrtm1 at line 
10 in file "ex.f"
   10         sqrtm1 = sqrt(x) - 1.0d0
(dbx) print x
x = -4.2
(dbx)

この出力例では、負の数の平方根を求めようとした結果、sqrtm1 関数で例外が発生していることがわかります。

adb を使用して例外の原因となっている命令を特定する

adb を使用して例外の原因を特定することもできます。ただし、dbx とは異なり、adb ではソースファイルや行番号は特定できません。adb を使用する場合も、最初の手順は
-ftrap を指定してプログラムを再コンパイルします。

 example% f77 -ftrap=invalid ex.f

次に adb を起動してプログラムを実行します。無効な演算例外が発生すると、adb は例外の原因である命令の次の命令で停止します。例外の原因である命令を見つけるには、いくつかの命令を逆アセンブルし、adb が停止した命令より前にある最後の浮動小数点命令を探します。SPARC アーキテクチャのシステムでは、結果は次の例のようになります。

example% adb a.out
:r
SIGFPE 8: numerical exception (invalid floating point operation)
stopped at      routine_+0x20:  ldd     [%l0], %f2
routine_+10?5i
routine_+0x10:  ld      [%l0 + 0x4], %f3
                fsqrtd  %f2, %f4
                sethi   %hi(0x11c00), %l0
                or      %l0, 0x1d8, %l0
                ldd     [%l0], %f2
<f2=F
                -4.2000000000000002e+00

この出力例は、fsqrtd 命令が原因で例外が発生したことを示しています。ソースレジスタを調べると、負の数の平方根を求めようとしたためにこの例外が発生したことがわかります。

x86 では、命令が固定長ではないため、コードの逆アセンブルを開始する正しいアドレスを見つけるには試行錯誤が必要になります。この例では、関数の先頭付近で例外が発生しているので、ここから逆アセンブルできます。一般的な結果は次のようになります。

example% adb a.out
:r
SIGFPE: Arithmetic Exception (invalid floating point operation)
stopped at      sqrtm1_+0x13:  faddp  %st,%st(1)
sqrtm1_?8i
sqrtm1_:
sqrtm1_:        pushl  %ebp
                movl   %esp,%ebp
                subl   $0x24,%esp
                fldl   $0x8048b88
                movl   0x8(%ebp),%eax
                fldl   (%eax)
                fsqrt
                faddp  %st,%st(1)
$x
80387 chip is present.
cw      0x137e
sw      0x3000
cssel 0x17  ipoff 0x8acd               datasel 0x1f  dataoff 0x0

 
 st[0]  -4.2000000000000001776356839             VALID
 st[1]  -1.0                                     VALID
 st[2]  +0.0                                     EMPTY
 st[3]  +0.0                                     EMPTY
 st[4]  +0.0                                     EMPTY
 st[5]  +0.0                                     EMPTY
 st[6]  +0.0                                     EMPTY
 st[7]  +0.0                                     EMPTY

この出力例は、fsqrt 命令が原因で例外が発生したことを示しています。浮動小数点レジスタを調べると、負の数の平方根を求めようとしたためにこの例外が発生したことがわかります。

再コンパイルせずにトラップを有効にする

上記の例では、-ftrap フラグを指定して、主プログラムを再コンパイルすることにより、無効な演算の例外トラップを有効にしました。主プログラムの再コンパイルが不可能な場合があり、他の方法でトラップを有効にしなければならないことがあります。トラップを有効にするにはいくつかの方法があります。

dbx の使用中に、浮動小数点状態レジスタを直接変更することによって、トラップを有効にすることができます。SPARC では、この方法には注意が必要です。オペレーティングシステムでは、プログラム内で最初に使用されるまで浮動小数点ユニットは使用可能になりません。浮動小数点ユニットが最初に使用された時点で、浮動小数点状態レジスタはリセットされ、すべてのトラップは無効になります。したがって、プログラムが少なくとも 1 つの浮動小数点命令を実行するまでは手動でトラップを有効にすることはできません。

ここで示した例では、sqrtm1 関数が呼び出されるまでに浮動小数点ユニットはアクセスされているので、この関数への入口にブレークポイントを設定し、無効な演算に対する例外トラップを有効にし、SIGFPE シグナルの受信時に dbx を停止するよう設定して、実行を継続できます。アーキテクチャのシステムでの手順は次のようになります。無効な演算例外に対するトラップを有効にするために、assign コマンドを使用して %fsr を変更していることに注意してください。

example% dbx a.out
Reading symbolic information for a.out
Reading symbolic information for rtld /usr/lib/ld.so.1
Reading symbolic information for libF77.so.3
Reading symbolic information for libsunmath.so.1
Reading symbolic information for libm.so.1
Reading symbolic information for libc.so.1
Reading symbolic information for libdl.so.1
(dbx) stop in sqrtm1_
dbx: warning: 'sqrtm1_' has no debugger info -- will trigger on
first instruction
(2) stop in sqrtm1_
(dbx) run
Running: a.out
(process id 6631)
stopped in sqrtm1_ at 0x10c00
0x00010c00: sqrtm1_       :    save    %sp, -0x68, %sp
(dbx) assign $fsr=0x08000000
dbx: warning: unknown language, 'fortran' assumed
(dbx) catch fpe
(dbx) cont
signal FPE (invalid floating point operation) in sqrtm1_ at 
0x10c20
0x00010c20: sqrtm1_+0x0020:    ldd     [%l0], %f2
(dbx)

x86 では、コンパイラが各プログラム内に自動的にリンクする起動コードは、制御をメインプログラムに引き渡す前に浮動小数点ユニットを初期化します。そのため、メインプログラムが開始した後で、いつでも手動でトラップを有効にすることできます。次に、この処理のステップ例を示します。

example% dbx a.out
Reading symbolic information for a.out
Reading symbolic information for rtld /usr/lib/ld.so.1
Reading symbolic information for libF77.so.3
Reading symbolic information for libsunmath.so.1
Reading symbolic information for libm.so.1
Reading symbolic information for libc.so.1
Reading symbolic information for libdl.so.1
(dbx) stop in main
dbx: warning: 'main' has no debugger info -- will trigger on
first instruction
(2) stop in main
(dbx) run
Running: a.out
(process id 3285)
stopped in main at 0x8048b00
0x00010c00: main       :         push1  %ebp
(dbx) assign $fctrl=0x137e
(dbx) catch fpe
(dbx) cont
signal FPE (invalid floating point operation) in main at 
0x8048add
0x08048add: sqrtm1_+0x000d:    fadd     0x8048bac
(dbx)

トラップを有効にする初期化ルーチンを作成すると、メインプログラムを再コンパイルしたり dbx を使用したりすることなくトラップを有効にできます。この方法は、例外が発生する場合に、デバッガ上で実行することなくプログラムを中止したい場合などに便利です。このようなルーチンを作成する方法は 2 つあります。

プログラムを構成するオブジェクトファイルとライブラリが使用可能な場合、プログラムを適切な初期化ルーチンに再リンクするだけでトラップを有効にすることができます。最初に、次のような C のソースファイルを作成します。

#include <ieeefp.h>

 
#pragma init (trapinvalid)

 
void trapinvalid()
{
     /* FP_X_INV などは ieeefp.h 内で定義されています*/
     fpsetmask(FP_X_INV);
}

次に、このファイルをコンパイルしてオブジェクトファイルを作成し、元のプログラムをこのオブジェクトファイルにリンクします。

example% cc -c init.c
example% f77 ex.o init.o
example% a.out
Floating point exception 7, invalid operand, occurred at address
8048afd.
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.out
Floating 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 ルーチンは、標準のルーチンが行う初期化機能の残りを実行します。次に、この処理を行うコード例を示します。

#include <ieeefp.h>
#include <sunmath.h>
#include <sys/sysi86.h>

#pragma init (trapinvalid)

void trapinvalid()
{
/* FP_X_INV et al は ieeefp.h で定義されます */
fpsetmask(FP_X_INV);
}

extern int __fltrounds(), __flt_rounds;
extern long _fp_hw;

void __fpstart()
{

char *out;

/* System V ABI Intel プロセッササプリメントによって定義されている
標準の __fpstart() 関数と同じ浮動小数点初期化を実行しますが、
すべてのトラップモードをそのままにします */
__flt_rounds = __fltrounds();
sysi86(SI86FPHW, &_fp_hw);
_fp_hw &= 0xff;
ieee_flags("set", "precision", "extended", &out);
}

このソースファイルを共有オブジェクトにコンパイルしてプリロードすると、期待される結果が生成されます。

example% cc -Kpic -G -ztext init.c -o init.so -lc
example% env LD_PRELOAD=./init.so a.out
Floating 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) 内に定義されています。

入力パラメータの取り得る値は、以下の通りです。

入力パラメータ C または C++ の型 取り得る値
action char * get、set、clear
exception char * invalid、division、overflow、underflow、inexact、 all、common
handler sigfpe_handler_type ユーザー定義ルーチン SIGFPE_DEFAULT SIGFPE_IGNORE SIGFPE_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 のコードでは、ゼロによる除算が発生した場合はプログラムを終了します。

#include <sunmath.h> 
/* x86 システムでは、次の行のコメントを解除します */
	/*ieee_flags("clear", "exception", "division", NULL); */
    if (ieee_handler("set", "division", SIGFPE_ABORT) != 0)
        printf("ieee trapping not supported here \n"); 

FORTRAN の場合は次のようになります。

#include <f77_floatingpoint.h> 
c x86 システムでは、次の行のコメントを解除します
c			ieee_flags(`clear', `exception', `division', %val(0))
			i = ieee_handler('set', 'division', SIGFPE_ABORT) 
			if(i.ne.0) print *,'ieee trapping not supported here' 

次の C のコードは、すべての例外について IEEE のデフォルトの例外処理に戻します。

#include <sunmath.h> 
    if (ieee_handler("clear", "all", 0) != 0) 
        printf("could not clear exception handlers\n"); 

FORTRAN の場合は次のようになります。

      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   算術例外の型 
SIGFPE 型名 IEEE 型
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 で、このようなハンドラの例を示します。

コード例 4-1   SIGFPE ハンドラ  

#include <stdio.h>
#include <sys/ieeefp.h>
#include <sunmath.h>
#include <siginfo.h>
#include <ucontext.h>

void handler(int sig, siginfo_t *sip, ucontext_t *uap)
{
    unsigned    code, addr;

#ifdef i386
    unsigned    sw;

    sw = uap->uc_mcontext.fpregs.fp_reg_set.fpchip_state.status &
       ~uap->uc_mcontext.fpregs.fp_reg_set.fpship_state.state[0];
    if (sw & (1 << fp_invalid))
        code = FPE_FLTINV;
else if (sw & (1 << fp_division))
        code = FPE_FLTDIV;
    else if (sw & (1 << fp_overflow))
        code = FPE_FLTOVF;
    else if (sw & (1 << fp_underflow))
        code = FPE_FLTUND;
    else if (sw & (1 << fp_inexact))
        code = FPE_FLTRES;
    else
        code = 0;
    addr = uap->uc_mcontext.fpregs.fp_reg_set.fpchip_state.
       state[3];
#else
    code = sip->si_code;
    addr = (unsigned) sip->si_addr;
#endif
    fprintf(stderr, "fp exception %x at address %x\n", code,
        addr);
}
int main()
{
    double  x;

    /* 共通の浮動小数点例外をトラップします */
    if (ieee_handler("set", "common", handler) != 0)
        printf("Did not set exception handler\n");

    /* アンダーフロー例外を発生させます(報告されません) */
    x = min_normal();
    printf("min_normal = %g\n", x);
    x = x / 13.0;
    printf("min_normal / 13.0 = %g\n", x);

    /* オーバーフロー例外を発生させます(報告されます) */
    x = max_normal();
    printf("max_normal = %g\n", x);
    x = x * x;
    printf("max_normal * max_normal = %g\n", x);
    ieee_retrospective(stderr);
    return 0;
}

SPARC システムでは、このプログラムからの出力は次のようになります。

min_normal = 2.22507e-308
min_normal / 13.0 = 1.7116e-309
max_normal = 1.79769e+308
fp exception 4 at address 10d0c
max_normal * max_normal = 1.79769e+308
 Note: 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-308
min_normal / 13.0 = 1.7116e-309
max_normal = 1.79769e+308
fp exception 4 at address 8048fe6
max_normal * max_normal = 1.79769e+308
 Note: 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_ZDZ 0/0 無効演算
FEX_INV_IDI 無限大/無限大の無効演算
FEX_INV_ISI 無限大-無限大の無効演算
FEX_INV_ZMI 0 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 つのモードを示します。

指定された 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 -lc
example% env FTRAP=invalid LD_PRELOAD=./init.so a.out
Floating point invalid operation (sqrt) at 0x00010c24 sqrtm1_, 
abort
  0x00010c30  sqrtm1_
  0x00010b48  MAIN_
  0x00010ccc  main
Abort

この出力は、ルーチン 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.309
1.59309e-28
1
4.14884e+137
4.14884e-163
1
 Note: 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.309
1.59309e-28
1
4.14884e+137
4.14884e-163
1
 Note: 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: handler
  0x00010928 main
159.309
1.59309e-28
Floating point underflow at 0x00010994 main, handler: handler
  0x00010998 main
1
Floating point overflow at 0x000109e4 main, handler: handler
  0x000109e8 main
4.14884e+137
4.14884e-163
Floating point underflow at 0x00010a4c main, handler: handler
  0x00010a50 main
1


サン・マイクロシステムズ株式会社
Copyright information. All rights reserved.
ホーム   |   目次   |   前ページへ   |   次ページへ   |   索引