信号NaN的有用性?

时间:2010-02-11 20:19:44

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

我最近在IEEE 754和x87架构上阅读了很多内容。我正在考虑在我正在研究的一些数值计算代码中使用NaN作为“缺失值”,我希望使用信令 NaN将允许我在案例中捕获浮点异常在那里我不想继续“缺失值”。相反,我会使用 quiet 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。
  4. 但是,步骤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。

3 个答案:

答案 0 :(得分:9)

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

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

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

答案 1 :(得分:2)

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

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

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

答案 2 :(得分:2)

你是不是只有一个const uint64_t,其中的位已被设置为信令nan的位?只要将其视为整数类型,信令nan与其他整数没有区别。您可以通过指针转换将它写在您想要的位置:

Const uint64_t sNan = 0xfff0000000000000;
Double[] myData;
...
Uint64* copier = (uint64_t*) &myData[index];
*copier=sNan | myErrorFlags;

有关要设置的位的信息: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html