我最近阅读了一些关于IEEE 754和x87架构的资料。我在编写一些数值计算代码时考虑使用NaN作为“缺失值”,并希望使用 signal NaN能够在不希望继续处理“缺失值”的情况下捕获浮点异常。相反的,我会使用quiet NaN来允许“缺失值”通过计算进行传播。然而,根据(非常有限的)已有文献所述,signal NaN 并没有按照我预想的那样工作。
这是我所知道的摘要(所有这些都使用x87和VC++):
- _EM_INVALID(IEEE“invalid”异常)控制x87在遇到NaN时的行为
- 如果屏蔽了_EM_INVALID(禁用异常),则不会生成异常,并且操作可以返回quiet NaN。涉及signal NaN的操作不会引发异常,但会被转换为quiet NaN。
- 如果未屏蔽_EM_INVALID(启用异常),则无效操作(例如sqrt(-1))会导致抛出一个无效异常。
- x87从不生成signal NaN.
- 如果未屏蔽_EM_INVALID,任何对signal 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寄存器中以将其存储到内存地址中)。
您可能像我一样认为以下各项:
- 掩盖_EM_INVALID。
- 使用信号NaN初始化变量。
- 取消掩盖_EM_INVALID。
然而,步骤2会导致信号NaN转换为静默NaN,因此随后对其的使用不会导致抛出异常! 真是什么鬼?!
信号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。