【问题标题】:Usefulness of signaling NaN?信号 NaN 的用处?
【发布时间】:2011-01-15 21:19:48
【问题描述】:

我最近阅读了大量有关 IEEE 754 和 x87 架构的内容。我正在考虑在我正在处理的一些数字计算代码中使用 NaN 作为“缺失值”,我希望使用 signaling NaN 可以让我在这种情况下捕获浮点异常我不想继续“缺失值”。相反,我会使用 quiet 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&lt;double&gt;::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。

【问题讨论】:

  • 好问题。不幸的是,我见过的发送 NaN 信号的唯一用途是在周六晚上 9:30 向我的手机发起呼叫。

标签: c++ visual-c++ floating-point ieee-754 x87


【解决方案1】:

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

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

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

【讨论】:

  • 是的,我认为这可能是一个合理的假设。我认为它需要成为语言的一部分,而不仅仅是库的一部分,它才能以应有的方式工作。
【解决方案2】:

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

您最好维护一个并行数据结构来跟踪有效性,或者将您的 FP 数据放在不同的(稀疏)数据结构中以仅保留有效数据。

这是相当笼统的建议;特殊值在某些情况下非常有用(例如,非常紧张的内存或性能限制),但随着上下文变得越来越大,它们可能会导致比其价值更多的困难。

【讨论】:

  • 这是一个很好的问题。我只向那些认为“这将是一个巧妙的技巧”的经验不足的旅行者提供这个答案作为谦虚的建议! :-)
  • 明白了。显然很难评论我没见过的代码,而且我从痛苦的经验中知道如何在被告知不要更改的大丑代码库上工作,但如果有机会我会尝试将逻辑隔离到检查函数中的值(即使是宏,如果您如此频繁地检查它,非内联函数调用也会减慢调试构建速度)。如果不出意外,它会让一切变得更加清晰,如果你找到更好的解决方案,充其量它会给你一个重新组织它的机会。
  • 好的,现在更有意义了。在您的示例中,使用 QNaN 并避免测试将更加 SIMD 和缓存友好。正如我在原始答案中所说,严格的内存/性能限制是不使用特殊值的一般规则的例外:-) 感谢 STingRaySC。很抱歉,我没有您的问题的答案。
【解决方案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;

【讨论】:

  • 小心,以这种方式重新解释转换会导致未定义的行为,因为在指定的位置从来没有 uint64_t。正确的方法是将字节表示存储到双位置。
  • 你能修复这个代码吗?大错特错。 1)由于大写字母,它不会编译。 2)如果这样做会导致未定义的行为。 3)具有误导性,因为你所说的sNan其实是负无穷大的模式(你可以查看自己的链接)
  • 谢谢@Arnaud 和 Kaznov。我同意你的批评。你们都得到了我的支持,我已经相应地更新了我早已忘记的答案。
  • 感谢您更新您的答案。我删除了我的反对票。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-19
  • 1970-01-01
  • 1970-01-01
  • 2014-07-12
  • 1970-01-01
相关资源
最近更新 更多