55

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

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

  • _EM_INVALID(IEEEの「無効な」例外)は、NaNに遭遇したときのx87の動作を制御します
  • _EM_INVALIDがマスクされている(例外が無効になっている)場合、例外は生成されず、操作はクワイエットNaNを返すことができます。NaNのシグナリングを伴う操作では、例外はスローされませんが、クワイエットNaNに変換されます。
  • _EM_INVALIDがマスクされていない(例外が有効になっている)場合、無効な操作(sqrt(-1)など)により、無効な例外がスローされます。
  • x87はシグナリングNaNを生成しません。
  • _EM_INVALIDがマスクされていない場合シグナリング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に変換されるため、その後使用しても例外はスローされません。だからWTF?!

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

私がここで何かが足りないかどうか誰かに教えてもらえますか?


編集:

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

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

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

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を返すことを想像します。

4

3 に答える 3

12

私が理解しているように、NaNのシグナリングの目的はデータ構造を初期化することですが、もちろんCでのランタイム初期化は、初期化の一部としてNaNがfloatレジスタにロードされるリスクを伴い、コンパイラがこのfloat値は、整数レジスタを使用してコピーする必要があることに注意してください。

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

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

于 2010-02-11T21:51:16.867 に答える
3

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

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

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

于 2010-02-13T06:05:37.637 に答える
3

さまざまなダブルNaNのビットパターンは次のとおりです。

シグナリングNaNは、7FF0000000000001と7FF7FFFFFFFFFFFFの間、またはFFF0000000000001とFFF7FFFFFFFFFFFFの間の任意のビットパターンで表されます。

クワイエットNaNは、7FF8000000000000と7FFFFFFFFFFFFFFFの間、またはFFF8000000000000とFFFFFFFFFFFFFFFFの間の任意のビットパターンで表されます。

出典:https ://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html

免責事項:他の人が指摘しているように、魔法をかけることは潜在的に危険であり、未定義の動作を引き起こす可能性があります。より安全な代替手段として、memcpyの使用が提案されています。

そうは言っても、学術的な目的で、または意図したハードウェアで安全であることがわかっている場合は、次のようにします。

理論的には、ビットがシグナリングnanのビットに設定されているconstuint64_tを使用するだけで機能するようです。整数型として扱う限り、シグナリングnanは他の整数と同じです。次に、アーキテクチャの特殊なケースの問題を除けば、ポインタキャストを使用して必要な場所に書き込むことができます。意図したとおりに機能する場合は、memcpyよりも高速である可能性があります。一部の組み込みシステムでは、それが役立つ場合もあります。

例:

const uint64_t sNan = 0xFFF7FFFFFFFFFFFF;
double[] myData;
...
uint64_t* copier = (uint64_t*) &myData[index];
*copier = sNan & ~myErrorFlags;
于 2015-08-17T18:55:03.833 に答える