静かなNaNとシグナリングNaNの違いは何ですか?


96

浮動小数点について読んだところ、NaNが演算から生じる可能性があることを理解しています。しかし、これらが概念であるかを正確に理解できません。それらの違いは何ですか?

C ++プログラミング中に作成できるのはどれですか?プログラマーとして、sNaNを引き起こすプログラムを作成できますか?

回答:


68

操作の結果としてNaNが静かになった場合、プログラムが結果をチェックしてNaNを確認するまで、異常が発生したことはありません。つまり、浮動小数点がソフトウェアで実装されている場合、浮動小数点ユニット(FPU)またはライブラリからの信号なしで計算が続行されます。シグナルNaNは、通常FPUからの例外の形でシグナルを生成します。例外がスローされるかどうかは、FPUの状態によって異なります。

C ++ 11は、浮動小数点環境にいくつかの言語制御を追加し、NaNを作成およびテストするための標準化された方法を提供します。ただし、コントロールが実装されているかどうかは十分に標準化されておらず、浮動小数点例外は通常、標準のC ++例外と同じ方法でキャッチされません。

POSIX / Unixシステムでは、浮動小数点例外は通常、SIGFPEのハンドラーを使用してキャッチされます。


34
これに追加:一般に、シグナルNaN(sNaN)の目的はデバッグ用です。たとえば、浮動小数点オブジェクトはsNaNに初期化される場合があります。次に、プログラムがそれらのいずれかに値を使用する前に失敗した場合、プログラムが算術演算でsNaNを使用すると例外が発生します。プログラムが誤ってsNaNを生成することはありません。通常の操作ではsNaNは生成されません。これらは、算術の結果としてではなく、シグナリングNaNを持つ目的でのみ作成されます。
Eric Postpischil 2013

18
対照的に、NaNはより通常のプログラミング用です。これらは、数値結果がない場合(たとえば、結果が実数でなければならない場合に負の数の平方根をとるなど)の通常の操作で生成できます。それらの目的は一般に、算術がある程度正常に進むことを可能にすることです。たとえば、膨大な数の配列があり、そのいくつかは通常は処理できない特殊なケースを表す場合があります。この配列を処理するために複雑な関数を呼び出すことができ、NaNを無視して通常の算術で配列を操作できます。それが終了したら、特別なケースを分離してさらに作業を行います。
Eric Postpischil 2013

@wrdieterありがとうございます。それでは、わずかな違いのみが例外を生成するかどうかです。
JalalJaberi 2013

@EricPostpischil 2番目の質問にご関心をお寄せいただきありがとうございます。
JalalJaberi 2013

@JalalJaberiはい、例外が主な違いです
wrdieter 2013

34

qNaNとsNaNは実験的にどのように見えますか?

最初に、sNaNまたはqNaNがあるかどうかを識別する方法を学びましょう。

この回答では、CではなくC ++を使用します。これは、C ++では便利std::numeric_limits::quiet_NaNstd::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は必須ですか、オプションですか?):

  • 指数== 0xFFおよび分数!= 0を持つものはすべてNaN
  • そして、最も高い小数ビットはqNaNをsNaNと区別します

しかし、無限大と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つは次のとおりです。

  • qNaNは、奇妙な値を持つ通常の組み込み(ソフトウェアまたはハードウェア)算術演算によって生成されます
  • sNaNは組み込み操作によって生成されることはなく、プログラマーが明示的に追加できるのは、たとえば 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で確認できます。

唯一の方法は、次のように使用setjmplongjmpて別の場所にジャンプすることです。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

参照:C ++でNaNをトレースする方法

シグナルフラグとは何で、どのように操作されますか?

すべてがCPUハードウェアに実装されています。

フラグはいくつかのレジスタに存在し、例外/シグナルが発生するかどうかを示すビットも存在します。

これらのレジスターはユーザーランドからアクセスできます、ほとんどのアーチのから。

glibc 2.29コードのこの部分は、実際には非常に簡単に理解できます。

たとえばfetestexceptsysdeps / 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アップストリーム:


en.wikipedia.org/wiki/IEEE_754#Interchange_formatsは、IEEE-754がNaNをシグナリングするための0は、無限大(significand = 0)にするリスクなしにNaNをクワイエットするための適切な実装選択であること示唆しているだけであることを指摘しています。どうやらそれは標準化されていませんが、それはx86が行うことです。(qNaNとsNaNを決定するのは仮数のMSBであるという事実標準化されています)。 en.wikipedia.org/wiki/Single-precision_floating-point_formatによると、x86とARMは同じですが、PA-RISCは反対の選択をしました。
Peter Cordes

@PeterCordesはい、IEEE 754 20atの「すべき」==「必須」または「推奨」が何なのかわかりません。「シグナルNaNビット文字列は、末尾の仮数フィールドの最初のビットが0でエンコードされている必要があります。
Ciro Santilli郝海东冠状病六四事件法轮功

re:しかし、無限大とNaNを区別するためにどのビットを使用すべきかを指定していないようです。 あなたはあなたが期待したように、sNaNを無限大と区別するために標準が推奨する設定がある特定のビットがあると予想したように書いた。IDKがそのようなビットがあると予想する理由。ゼロ以外の選択でも問題ありません。後でsNaNがどこから来たかを特定するものを選ぶだけです。IDK、奇妙な言い回しのように聞こえますが、読んだときの最初の印象は、エンコーディングでinfとNaNを区別するもの(すべてゼロの仮数)をWebページが説明していないというものでした。
Peter Cordes

2008年以前は、IEEE 754は、どちらがシグナリング/クワイエットビット(ビット22)であるかを示していますが、どの値が何を指定していないかを示しています。ほとんどのプロセッサーは1 =クワイエットに収束していたため、2008年版の標準の一部になりました。同じ選択を不適合とした古い実装を避けるために、「必須」ではなく「すべき」と述べています。一般に、標準で「すべき」とは、「遵守しなければならない非常に説得力のある(そしてできれば十分に文書化された)理由がない限り、必ずしなければならないこと」を意味します。
John Cowan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.