回答:
操作の結果としてNaNが静かになった場合、プログラムが結果をチェックしてNaNを確認するまで、異常が発生したことはありません。つまり、浮動小数点がソフトウェアで実装されている場合、浮動小数点ユニット(FPU)またはライブラリからの信号なしで計算が続行されます。シグナルNaNは、通常FPUからの例外の形でシグナルを生成します。例外がスローされるかどうかは、FPUの状態によって異なります。
C ++ 11は、浮動小数点環境にいくつかの言語制御を追加し、NaNを作成およびテストするための標準化された方法を提供します。ただし、コントロールが実装されているかどうかは十分に標準化されておらず、浮動小数点例外は通常、標準のC ++例外と同じ方法でキャッチされません。
POSIX / Unixシステムでは、浮動小数点例外は通常、SIGFPEのハンドラーを使用してキャッチされます。
qNaNとsNaNは実験的にどのように見えますか?
最初に、sNaNまたはqNaNがあるかどうかを識別する方法を学びましょう。
この回答では、CではなくC ++を使用します。これは、C ++では便利std::numeric_limits::quiet_NaN
でstd::numeric_limits::signaling_NaN
あり、Cで便利に見つけることができなかったためです。
ただし、NaNがsNaNかqNaNかを分類する関数を見つけることができなかったので、NaN生バイトを出力してみましょう。
main.cpp
#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits
#pragma STDC FENV_ACCESS ON
void print_float(float f) {
std::uint32_t i;
std::memcpy(&i, &f, sizeof f);
std::cout << std::hex << i << std::endl;
}
int main() {
static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
static_assert(std::numeric_limits<float>::has_infinity, "");
// Generate them.
float qnan = std::numeric_limits<float>::quiet_NaN();
float snan = std::numeric_limits<float>::signaling_NaN();
float inf = std::numeric_limits<float>::infinity();
float nan0 = std::nanf("0");
float nan1 = std::nanf("1");
float nan2 = std::nanf("2");
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
// Print their bytes.
std::cout << "qnan "; print_float(qnan);
std::cout << "snan "; print_float(snan);
std::cout << " inf "; print_float(inf);
std::cout << "-inf "; print_float(-inf);
std::cout << "nan0 "; print_float(nan0);
std::cout << "nan1 "; print_float(nan1);
std::cout << "nan2 "; print_float(nan2);
std::cout << " 0/0 "; print_float(div_0_0);
std::cout << "sqrt "; print_float(sqrt_negative);
// Assert if they are NaN or not.
assert(std::isnan(qnan));
assert(std::isnan(snan));
assert(!std::isnan(inf));
assert(!std::isnan(-inf));
assert(std::isnan(nan0));
assert(std::isnan(nan1));
assert(std::isnan(nan2));
assert(std::isnan(div_0_0));
assert(std::isnan(sqrt_negative));
}
コンパイルして実行:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
私のx86_64マシンでの出力:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
QEMUユーザーモードでaarch64でプログラムを実行することもできます。
aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out
そして、それはまったく同じ出力を生成し、複数のアーチがIEEE 754を厳密に実装していることを示唆しています。
この時点で、IEEE 754浮動小数点数の構造に慣れていない場合は、以下を参照してください。非正規浮動小数点数とは何ですか?
バイナリでは、上記の値のいくつかは次のとおりです。
31
|
| 30 23 22 0
| | | | |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
| | | | |
| +------+ +---------------------+
| | |
| v v
| exponent fraction
|
v
sign
この実験から、次のことがわかります。
qNaNとsNaNはビット22によってのみ区別されるようです。1は無音を意味し、0はシグナリングを意味します
無限大も指数== 0xFFと非常に似ていますが、分数== 0です。
このため、NaNはビット21を1に設定する必要があります。そうしないと、sNaNを正の無限大と区別できません。
nanf()
はいくつかの異なるNaNを生成するため、複数の可能なエンコーディングが必要です。
7fc00000
7fc00001
7fc00002
以来nan0
と同じでstd::numeric_limits<float>::quiet_NaN()
、我々は、彼らがすべて異なる静かなのNaNをしていると推定します。
C11 N1570標準のドラフトを確認nanf()
するので、静かなNaNを生成しnanf
転送するstrtod
「にstrtod、strtof、およびstrtold機能」と7.22.1.3は言います:
文字シーケンスNANまたはNAN(n-char-sequence opt)は、戻り値の型でサポートされている場合、クワイエットNaNとして解釈されます。n-charシーケンスの意味は実装定義です。293)
以下も参照してください。
マニュアルではqNaNとsNaNはどのように見えますか?
IEEE 754 2008では、次のことを推奨しています(TODOは必須ですか、オプションですか?):
しかし、無限大とNaNを区別するためにどのビットが好ましいかについては述べられていないようです。
6.2.1「バイナリ形式のNaNエンコーディング」は次のように述べています。
この副次句では、NaNのエンコードがビット文字列として、それらが演算の結果である場合にさらに指定します。エンコードすると、すべてのNaNに符号ビットと、エンコードをNaNとして識別するために必要なビットのパターンと、その種類を決定するビットのパターン(sNaN対qNaN)があります。後続の仮数フィールドにある残りのビットはペイロードをエンコードします。これは診断情報である可能性があります(上記を参照)。34
すべてのバイナリNaNビット文字列は、バイアス指数フィールドEのすべてのビットが1に設定されています(3.4を参照)。クワイエットNaNビット文字列は、後続の仮数フィールドTの最初のビット(d1)が1でエンコードされている必要があります。シグナリングNaNビット文字列は、後続の仮数フィールドの最初のビットが0でエンコードされている必要があります。後続の仮数フィールドは0であり、NaNを無限大と区別するには、後続の仮数フィールドの他のビットがゼロ以外でなければなりません。今説明した好ましい符号化では、シグナリングNaNは、d1を1に設定することによってクワイエットされ、Tの残りのビットは変更されません。バイナリ形式の場合、ペイロードは後続の仮数フィールドのp-2最下位ビットでエンコードされます
インテル64およびIA-32アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル- 253665-056US 2015年9月-第1巻基本アーキテクチャのx86が最高の端数ビットではNaNとSNANを区別することにより、IEEE 754に従うことを4.8.3.4「NaNを」確認します:
IA-32アーキテクチャーは、2つのクラスのNaNを定義しています。クワイエットNaN(QNaN)とシグナリングNaN(SNaN)です。QNaNは、最上位の小数ビットが設定されたNaNであり、SNaNは、最上位の小数ビットがクリアされたNaNです。
そしてそうARMv8-AアーキテクチャプロファイルのARMv8、 - - DDI 0487C.aマニュアルARMアーキテクチャリファレンス A1.4.3「単精度浮動小数点フォーマット」:
fraction != 0
:値はNaNであり、クワイエットNaNまたはシグナリングNaNのいずれかです。NaNの2つのタイプは、最上位の小数ビットであるビット[22]によって区別されます。
bit[22] == 0
:NaNはシグナリングNaNです。符号ビットは任意の値を取ることができ、残りの小数部ビットはすべてゼロ以外の任意の値を取ることができます。bit[22] == 1
:NaNは静かなNaNです。符号ビットと残りの小数ビットは任意の値を取ることができます。qNanSとsNaNはどのように生成されますか?
qNaNとsNaNの主な違いの1つは次のとおりです。
std::numeric_limits::signaling_NaN
そのための明確なIEEE 754またはC11の引用符は見つかりませんでしたが、sNaNを生成する組み込み演算は見つかりませんでした;-)
Intelのマニュアルには、この原則が明記されていますが、4.8.3.4 "NaNs"です。
SNaNは通常、例外ハンドラーをトラップまたは呼び出すために使用されます。これらはソフトウェアで挿入する必要があります。つまり、浮動小数点演算の結果としてプロセッサーがSNaNを生成することはありません。
これは、次の両方の例から確認できます。
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
とまったく同じビットを生成しstd::numeric_limits<float>::quiet_NaN()
ます。
これらの操作は両方とも、ハードウェアで直接qNaNを生成する単一のx86アセンブリ命令にコンパイルされます(TODOはGDBで確認)。
qNaNとsNaNの違いは何ですか?
qNaNとsNaNがどのように見えるか、およびそれらを操作する方法がわかったので、ようやくsNaNを試して、いくつかのプログラムを爆破する準備ができました!
さて、さらに面倒なことはありません:
blow_up.cpp
#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>
#pragma STDC FENV_ACCESS ON
int main() {
float snan = std::numeric_limits<float>::signaling_NaN();
float qnan = std::numeric_limits<float>::quiet_NaN();
float f;
// No exceptions.
assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);
// Still no exceptions because qNaN.
f = qnan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;
// Now we can get an exception because sNaN, but signals are disabled.
f = snan + 1.0f;
assert(std::isnan(f));
if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
feclearexcept(FE_ALL_EXCEPT);
// And now we enable signals and blow up with SIGFPE! >:-)
feenableexcept(FE_INVALID);
f = qnan + 1.0f;
std::cout << "feenableexcept qnan + 1.0f" << std::endl;
f = snan + 1.0f;
std::cout << "feenableexcept snan + 1.0f" << std::endl;
}
コンパイルして実行し、終了ステータスを取得します。
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
出力:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
この動作-O0
はGCC 8.2でのみ発生することに注意してください。-O3
ください。GCCを使用すると、すべてのsNaN操作が事前に計算および最適化されます。それを防ぐ標準準拠の方法があるかどうかはわかりません。
したがって、この例から次のことを推測します。
snan + 1.0
原因FE_INVALID
となるが、でqnan + 1.0
はない
Linuxは、で有効になってfeenableexept
いる場合にのみ信号を生成します。
これはglibcの拡張機能であり、どの標準でもそれを行う方法を見つけることができませんでした。
シグナルが発生するのは、CPUハードウェア自体が例外を発生させ、Linuxカーネルが処理し、シグナルを通じてアプリケーションに通知したためです。
その結果、bashはを出力Floating point exception (core dumped)
し、終了ステータスは136
であり、これは signal 136 - 128 == 8
に対応します。
man 7 signal
ですSIGFPE
。
これSIGFPE
は、整数を0で除算しようとした場合に得られる信号と同じであることに注意してください。
int main() {
int i = 1 / 0;
}
ただし、整数の場合:
feenableexcept
SIGFPEの扱い方
正常に戻るハンドラを作成しただけでは、無限ループが発生します。これは、ハンドラが戻った後、除算が再び行われるためです。これはGDBで確認できます。
唯一の方法は、次のように使用setjmp
しlongjmp
て別の場所にジャンプすることです。CはシグナルSIGFPEを処理し、実行を継続します。
sNaNの実際のアプリケーションは何ですか?
正直なところ、私はまだsNaNの非常に便利なユースケースを理解していません。これは、NaNのシグナリングの有用性?
sNaNsは、私たちが最初の無効な操作(検出することができますので、特に役に立たない感じ0.0f/0.0f
でqNaNsを生成する)feenableexcept
:ように見えるsnan
だけで、より操作のためのエラーを発生させていますqnan
のために発生しませんが、例えば(qnan + 1.0f
)。
例えば:
main.c
#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>
int main(int argc, char **argv) {
(void)argv;
float f0 = 0.0;
if (argc == 1) {
feenableexcept(FE_INVALID);
}
float f1 = 0.0 / f0;
printf("f1 %f\n", f1);
feenableexcept(FE_INVALID);
float f2 = f1 + 1.0;
printf("f2 %f\n", f2);
}
コンパイル:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
次に:
./main.out
与える:
Floating point exception (core dumped)
そして:
./main.out 1
与える:
f1 -nan
f2 -nan
シグナルフラグとは何で、どのように操作されますか?
すべてがCPUハードウェアに実装されています。
フラグはいくつかのレジスタに存在し、例外/シグナルが発生するかどうかを示すビットも存在します。
これらのレジスターはユーザーランドからアクセスできます、ほとんどのアーチのから。
glibc 2.29コードのこの部分は、実際には非常に簡単に理解できます。
たとえばfetestexcept
、sysdeps / x86_64 / fpu / ftestexcept.cのx86_86に実装されています。
#include <fenv.h>
int
fetestexcept (int excepts)
{
int temp;
unsigned int mxscr;
/* Get current exceptions. */
__asm__ ("fnstsw %0\n"
"stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));
return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)
そのため、命令の使用が stmxcsr
「Store MXCSR Register State」を表すます。
そしてsysdeps / x86_64 / fpu / feenablxcpt.cにfeenableexcept
実装されています:
#include <fenv.h>
int
feenableexcept (int excepts)
{
unsigned short int new_exc, old_exc;
unsigned int new;
excepts &= FE_ALL_EXCEPT;
/* Get the current control word of the x87 FPU. */
__asm__ ("fstcw %0" : "=m" (*&new_exc));
old_exc = (~new_exc) & FE_ALL_EXCEPT;
new_exc &= ~excepts;
__asm__ ("fldcw %0" : : "m" (*&new_exc));
/* And now the same for the SSE MXCSR register. */
__asm__ ("stmxcsr %0" : "=m" (*&new));
/* The SSE exception masks are shifted by 7 bits. */
new &= ~(excepts << 7);
__asm__ ("ldmxcsr %0" : : "m" (*&new));
return old_exc;
}
C標準はqNaNとsNaNについて何を言っていますか?
C11 N1570標準草案は、明示的に標準はF.2.1「無限大、署名ゼロ、およびNaNの」でそれらを区別しないことを言います:
1この仕様は、シグナルNaNの動作を定義していません。通常、NaNという用語は静かなNaNを表すために使用されます。のNANおよびINFINITYマクロとnan関数は
<math.h>
、IEC 60559 NaNおよび無限大の指定を提供します。
Ubuntu 18.10、GCC 8.2でテスト済み。GitHubアップストリーム: