我最近阅读了大量有关 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 寄存器中以将其存储到内存地址)。
您可能会像我一样认为以下内容:
- 掩码 _EM_INVALID。
- 用信号 NaN 初始化变量。
- 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。