私は最近、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レジスタにロードされ、メモリアドレスに格納されます)。
あなたは私がしたように次のことを考えるかもしれません:
- マスク_EM_INVALID。
- シグナリングNaNを使用して変数を初期化します。
- 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を返すことを想像します。