55

我最近阅读了大量有关 IEEE 754 和 x87 架构的内容。我正在考虑在我正在处理的一些数字计算代码中使用 NaN 作为“缺失值”,我希望使用信号NaN 可以让我在我不想的情况下捕获浮点异常继续“缺失值”。相反,我会使用安静的 NaN 来允许“缺失值”通过计算传播。但是,信号 NaN 不起作用,因为我认为它们会基于它们上存在的(非常有限的)文档。

这是我所知道的摘要(所有这些都使用 x87 和 VC++):

  • _EM_INVALID(IEEE“无效”异常)控制 x87 在遇到 NaN 时的行为
  • 如果 _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。

4

3 回答 3

12

据我了解,发出信号 NaN 的目的是初始化数据结构,但是,C 中的运行时初始化当然冒着将 NaN 作为初始化的一部分加载到浮点寄存器中的风险,从而触发信号,因为编译器是'不知道这个浮点值需要使用整数寄存器来复制。

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

如果您使用 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 作为更安全的选择。

话虽如此,出于学术目的,或者如果您知道它在预期硬件上是安全的:

从理论上讲,似乎应该只使用一个 const uint64_t,其中的位已设置为信号 nan 的位。只要你把它当作整数类型,信号 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 回答