c++ - 信号NaN的有用性?




visual-c++ floating-point (2)

我最近读了很多关于IEEE 754和x87架构的内容。 我正在考虑在我正在研究的一些数值计算代码中使用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是否有任何实用性或目的? 我理解其中一个原始意图是使用它初始化内存,以便可以捕获使用单位化浮点值。

有人能告诉我,如果我在这里遗失了什么吗?

编辑:

为了进一步说明我希望做的事情,这里有一个例子:

考虑对数据向量(双精度)执行数学运算。 对于某些操作,我想允许向量包含“缺失值”(假设这对应于电子表格列,例如,其中一些单元格没有值,但它们的存在是重要的)。 对于某些操作,我希望允许向量包含“缺失值”。 如果集合中存在“缺失值”,也许我想采取不同的行动 - 可能执行不同的操作(因此这不是无效的状态)。

这个原始代码看起来像这样:

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。


使用特殊值(甚至是NULL)可能会使您的数据变得更加混乱,并且您的代码更加混乱。 要区分QNaN结果和QNaN“特殊”值是不可能的。

您可能更好地维护并行数据结构以跟踪有效性,或者可能将FP数据放在不同(稀疏)数据结构中以仅保留有效数据。

这是相当一般的建议; 特殊值在某些情况下非常有用(例如真正严格的内存或性能限制),但随着上下文变大,它们可能会导致比它们更值得的困难。


据我所知,信号NaN的目的是初始化数据结构,但是,当然C中的运行初始化存在将NaN作为初始化的一部分加载到浮点寄存器中的风险,从而触发信号,因为编译器不是请注意,需要使用整数寄存器复制此浮点值。

我希望你可以使用信号NaN初始化一个static值,但即便如此,编译器也需要进行一些特殊处理,以避免将其转换为安静的NaN。 您可以使用一些魔法来避免在初始化期间将其视为浮点值。

如果您使用ASM编写,这不是问题。 但是在C中,特别是在C ++中,我认为你必须颠覆类型系统才能用NaN初始化变量。 我建议使用memcpy





x87