web-dev-qa-db-ja.com

NaNのシグナリングの有用性?

私は最近、IEEE754とx87アーキテクチャについてかなり読んだ。作業中の数値計算コードでNaNを「欠落値」として使用することを考えていましたが、signalingNaNを使用すると「値の欠落」を続行したくない場合に、浮動小数点の例外をキャッチできるようにします。逆に、quietNaNを使用して、「欠落値」が計算全体に伝播できるようにします。ただし、NaNのシグナリングは、そこに存在する(非常に限られた)ドキュメントに基づいて考えたようには機能しません。

これが私が知っていることの要約です(これはすべてx87とVC++を使用しています):

  • _EM_INVALID(IEEEの「無効な」例外)は、NaNに遭遇したときのx87の動作を制御します
  • _EM_INVALIDがマスクされている(例外が無効になっている)場合、例外は生成されず、操作はクワイエットNaNを返すことができます。 NaNのシグナリングを含む操作では、not例外がスローされますが、クワイエットNaNに変換されます。
  • _EM_INVALIDがマスクされていない(例外が有効になっている)場合、無効な操作(sqrt(-1)など)により、無効な例外がスローされます。
  • X87 neverはシグナリングNaNを生成します。
  • _EM_INVALIDがマスクされていない場合、anyシグナリングNaNを使用すると(変数を初期化する場合でも)、無効な例外がスローされます。

標準ライブラリは、NaN値にアクセスする方法を提供します。

_std::numeric_limits<double>::signaling_NaN();
_

そして

_std::numeric_limits<double>::quiet_NaN();
_

問題は、NaNのシグナリングにはまったく役に立たないということです。 _EM_INVALIDがマスクされている場合、クワイエットNaNとまったく同じように動作します。他のNaNに匹敵するNaNはないため、論理的な違いはありません。

_EM_INVALIDがマスクされていないマスクされていない(例外が有効になっている)場合、シグナルNaNで変数を初期化することさえできません:double dVal = std::numeric_limits<double>::signaling_NaN();例外をスローします(シグナリングNaN値がx87レジスタにロードされ、メモリアドレスに格納されます)。

あなたは私がしたように次のことを考えるかもしれません:

  1. マスク_EM_INVALID。
  2. シグナリングNaNを使用して変数を初期化します。
  3. Unmask_EM_INVALID。

ただし、手順2ではシグナリングNaNがクワイエットNaNに変換されるため、その後使用するとnot例外がスローされます。だからWTF ?!

シグナリングNaNに何か有用性や目的はありますか?元々の目的の1つは、メモリを初期化して、単位化された浮動小数点値の使用をキャッチできるようにすることであったことを理解しています。

ここで何かが足りない場合、誰かに教えてもらえますか?


編集:

私がやりたかったことをさらに説明するために、ここに例を示します。

データのベクトル(double)に対して数学演算を実行することを検討してください。一部の操作では、ベクトルに「欠落値」が含まれるようにします(たとえば、一部のセルに値がないが、その存在が重要であるスプレッドシート列に対応していると仮定します)。一部の操作では、ベクトルに「欠落値」を含めることを許可したくないnot。セットに「欠落値」が存在する場合は、別のアクションを実行したい場合があります。おそらく、別の操作を実行します(したがって、これは無効な状態ではありません)。

この元のコードは次のようになります。

_const double MISSING_VALUE = 1.3579246e123;
using std::vector;

vector<double> missingAllowed(1000000, MISSING_VALUE);
vector<double> missingNotAllowed(1000000, MISSING_VALUE);

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it); // sqrt() could be any operation
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it);
    else *it = 0;
}
_

「欠落値」のチェックを実行する必要があることに注意してくださいループの反復ごと。ほとんどの場合、sqrt関数(またはその他の数学演算)がこのチェックを覆い隠す可能性がありますが、演算が最小限(おそらく単なる加算)であり、チェックにコストがかかる場合があります。 「欠落値」が正当な入力値を無効にし、計算が合法的にその値に到達した場合にバグを引き起こす可能性があるという事実は言うまでもありません(そうである可能性は低いですが)。また、技術的に正確であるためには、ユーザー入力データをその値と照合し、適切な措置を講じる必要があります。私は、このソリューションがエレガントでなく、パフォーマンス的に最適ではないと感じています。これはパフォーマンスが重要なコードであり、並列データ構造やある種のデータ要素オブジェクトの贅沢は絶対にありません。

NaNバージョンは次のようになります。

_using std::vector;

vector<double> missingAllowed(1000000, std::numeric_limits<double>::quiet_NaN());
vector<double> missingNotAllowed(1000000, std::numeric_limits<double>::signaling_NaN());

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    *it = sqrt(*it); // if *it == QNaN then sqrt(*it) == QNaN
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    try {
        *it = sqrt(*it);
    } catch (FPInvalidException&) { // assuming _seh_translator set up
        *it = 0;
    }
}
_

これで、明示的なチェックが削除され、パフォーマンスが向上するはずです。 FPUレジスタに触れずにベクトルを初期化できれば、これはすべてうまくいくと思います...

さらに、自尊心のあるsqrt実装がNaNをチェックし、すぐにNaNを返すことを想像します。

52
user123456

私が理解しているように、NaNのシグナリングの目的はデータ構造を初期化することですが、もちろんruntime Cでの初期化は、初期化の一部としてNaNをfloatレジスタにロードしてトリガーするリスクがあります。コンパイラは、この浮動小数点値を整数レジスタを使用してコピーする必要があることを認識していないため、シグナルです。

static値をシグナリングNaNで初期化できることを願っていますが、それでも、クワイエットNaNに変換されないように、コンパイラーによる特別な処理が必要になります。初期化中にfloat値として扱われるのを避けるために、少しキャストマジックを使用することもできます。

ASMで書いている場合、これは問題にはなりません。しかし、C、特にC++では、変数をNaNで初期化するために、型システムを破壊する必要があると思います。 memcpyを使用することをお勧めします。

9
John Knoeller

特別な値(NULLであっても)を使用すると、データが非常に濁り、コードが非常に乱雑になる可能性があります。 QNaNの結果とQNaNの「特別な」値を区別することは不可能です。

有効性を追跡するために並列データ構造を維持するか、有効なデータのみを保持するためにFPデータを別の(スパース)データ構造に含める方がよい場合があります。

これはかなり一般的なアドバイスです。特別な値は、特定の場合(たとえば、メモリやパフォーマンスの制約が非常に厳しい場合)に非常に役立ちますが、コンテキストが大きくなると、価値があるよりも困難になる可能性があります。

2
Matt Curtis

ビットがシグナリングnanのビットに設定されているconstuint64_tを持っているだけではありませんか?整数型として扱う限り、シグナリングnanは他の整数と同じです。ポインタをキャストすることで、好きな場所に書くことができます。

Const uint64_t sNan = 0xfff0000000000000;
Double[] myData;
...
Uint64* copier = (uint64_t*) &myData[index];
*copier=sNan | myErrorFlags;

設定するビットの詳細: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html

2
Jan Heldal