133

我已阅读有关浮点的信息,并且我了解 NaN 可能是由操作产生的。但我不明白这些到底是什么概念。它们之间有什么区别?

在 C++ 编程过程中可以产生哪一个?作为程序员,我可以编写一个导致 sNaN 的程序吗?

4

2 回答 2

91

当操作导致安静的 NaN 时,在程序检查结果并看到 NaN 之前,没有任何迹象表明有任何异常。也就是说,如果浮点是在软件中实现的,那么在没有来自浮点单元 (FPU) 或库的任何信号的情况下继续计算。一个信号 NaN 将产生一个信号,通常以来自 FPU 的异常的形式。是否抛出异常取决于 FPU 的状态。

C++11在浮点环境中添加了一些语言控件,并提供了创建和测试 NaN 的标准化方法。但是,控件是否被实现并没有很好地标准化,并且浮点异常通常不会像标准 C++ 异常那样被捕获。

在 POSIX/Unix 系统中,浮点异常通常使用SIGFPE的处理程序来捕获。

于 2013-08-08T05:41:50.280 回答
63

qNaNs 和 sNaNs 在实验上看起来如何?

让我们首先学习如何识别我们是 sNaN 还是 qNaN。

我将在这个答案中使用 C++ 而不是 C,因为它提供了方便std::numeric_limits::quiet_NaNstd::numeric_limits::signaling_NaN而我在 C 中找不到方便。

但是我找不到一个函数来分类 NaN 是 sNaN 还是 qNaN,所以让我们打印出 NaN 原始字节:

主文件

#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits

#pragma STDC FENV_ACCESS ON

void print_float(float f) {
    std::uint32_t i;
    std::memcpy(&i, &f, sizeof f);
    std::cout << std::hex << i << std::endl;
}

int main() {
    static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
    static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
    static_assert(std::numeric_limits<float>::has_infinity, "");

    // Generate them.
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float snan = std::numeric_limits<float>::signaling_NaN();
    float inf = std::numeric_limits<float>::infinity();
    float nan0 = std::nanf("0");
    float nan1 = std::nanf("1");
    float nan2 = std::nanf("2");
    float div_0_0 = 0.0f / 0.0f;
    float sqrt_negative = std::sqrt(-1.0f);

    // Print their bytes.
    std::cout << "qnan "; print_float(qnan);
    std::cout << "snan "; print_float(snan);
    std::cout << " inf "; print_float(inf);
    std::cout << "-inf "; print_float(-inf);
    std::cout << "nan0 "; print_float(nan0);
    std::cout << "nan1 "; print_float(nan1);
    std::cout << "nan2 "; print_float(nan2);
    std::cout << " 0/0 "; print_float(div_0_0);
    std::cout << "sqrt "; print_float(sqrt_negative);

    // Assert if they are NaN or not.
    assert(std::isnan(qnan));
    assert(std::isnan(snan));
    assert(!std::isnan(inf));
    assert(!std::isnan(-inf));
    assert(std::isnan(nan0));
    assert(std::isnan(nan1));
    assert(std::isnan(nan2));
    assert(std::isnan(div_0_0));
    assert(std::isnan(sqrt_negative));
}

编译并运行:

g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

我的 x86_64 机器上的输出:

qnan 7fc00000
snan 7fa00000
 inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
 0/0 ffc00000
sqrt ffc00000

我们也可以使用 QEMU 用户模式在 aarch64 上执行程序:

aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out

这会产生完全相同的输出,这表明多个架构紧密地实现了 IEEE 754。

至此,如果您不熟悉 IEEE 754 浮点数的结构,请看一看:什么是次正规浮点数?

在二进制中,上面的一些值是:

     31
     |
     | 30    23 22                    0
     | |      | |                     |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
 inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
     | |      | |                     |
     | +------+ +---------------------+
     |    |               |
     |    v               v
     | exponent        fraction
     |
     v
     sign

从这个实验中,我们观察到:

  • qNaN 和 sNaN 似乎只通过第 22 位来区分:1 表示安静,0 表示发信号

  • 无穷大也与指数 == 0xFF 非常相似,但它们的分数 == 0。

    因此,NaN 必须将第 21 位设置为 1,否则无法区分 sNaN 和正无穷大!

  • nanf()产生几个不同的 NaN,所以必须有多种可能的编码:

    7fc00000
    7fc00001
    7fc00002
    

    由于nan0与 相同std::numeric_limits<float>::quiet_NaN(),我们推断它们都是不同的安静 NaN。

    C11 N1570 标准草案确认nanf()生成安静的 NaN,因为转发nanfstrtod7.22.1.3 “strtod、strtof 和 strtold 函数”说:

    如果返回类型支持,则字符序列 NAN 或 NAN(n-char-sequence opt) 被解释为安静的 NaN,否则就像不具有预期形式的主题序列部分;n-char 序列的含义是实现定义的。293)

也可以看看:

qNaN 和 sNaN 在手册中的外观如何?

IEEE 754 2008建议(TODO 是强制性的还是可选的?):

  • 任何具有指数 == 0xFF 和分数 != 0 的东西都是 NaN
  • 并且最高小数位区分 qNaN 和 sNaN

但似乎并没有说明哪个位更适合区分无穷大和 NaN。

6.2.1 “二进制格式的 NaN 编码” 说:

当 NaN 是运算的结果时,本子条款进一步将 NaN 的编码指定为位串。编码时,所有 NaN 都有一个符号位和一个位模式,这些位模式将编码识别为 NaN 并确定其类型(sNaN 与 qNaN)。尾随有效位字段中的剩余位对有效载荷进行编码,这可能是诊断信息(见上文)。34

所有二进制 NaN 位串都将偏置指数字段 E 的所有位设置为 1(参见 3.4)。一个安静的 NaN 位串应该用尾随有效位字段 T 的第一位 (d1) 为 1 进行编码。信令 NaN 位串应该用尾随有效位字段的第一位为 0 进行编码。如果尾随有效位字段为 0,尾随有效位字段的某些其他位必须为非零以区分 NaN 和无穷大。在刚刚描述的优选编码中,信令 NaN 应通过将 d1 设置为 1 来静默,保持 T 的其余位不变。对于二进制格式,有效载荷在尾随有效位字段的 p-2 个最低有效位中编码

英特尔64 和 IA-32 架构软件开发人员手册 - 第 1 卷基本架构 - 253665-056US 2015 年 9 月4.8.3.4 “NaNs”通过最高分数位区分 NaN 和 sNaN 确认 x86 遵循 IEEE 754:

IA-32 架构定义了两类 NaN:安静 NaN (QNaN) 和信令 NaN (SNaN)。QNaN 是设置了最高有效小数位的 NaN,SNaN 是清除了最高有效小数位的 NaN。

ARM 架构参考手册 - ARMv8,针对 ARMv8-A 架构配置文件 - DDI 0487C.a A1.4.3“单精度浮点格式”也是如此:

fraction != 0:该值为 NaN,可以是安静的 NaN,也可以是信号的 NaN。两种类型的 NaN 的区别在于它们的最高有效小数位 bit[22]:

  • bit[22] == 0: NaN 是一个信令 NaN。符号位可以取任何值,其余的小数位可以取除全零以外的任何值。
  • bit[22] == 1: NaN 是安静的 NaN。符号位和剩余的小数位可以取任何值。

qNanS 和 sNaN 是如何生成的?

qNaNs 和 sNaNs 之间的一个主要区别是:

  • qNaN 由具有奇怪值的常规内置(软件或硬件)算术运算生成
  • sNaN 永远不会由内置操作生成,它只能由程序员显式添加,例如std::numeric_limits::signaling_NaN

我找不到明确的 IEEE 754 或 C11 引用,但我也找不到任何生成 sNaN 的内置操作;-)

英特尔手册在 4.8.3.4 “NaNs”中明确说明了这一原则:

SNaN 通常用于捕获或调用异常处理程序。必须通过软件插入;也就是说,处理器永远不会作为浮点运算的结果生成 SNaN。

这可以从我们的示例中看出:

float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);

产生与 完全相同的位std::numeric_limits<float>::quiet_NaN()

这两个操作都编译为一条 x86 汇编指令,该指令直接在硬件中生成 qNaN(TODO 使用 GDB 确认)。

qNaNs 和 sNaNs 有什么不同?

现在我们知道了 qNaN 和 sNaN 的样子,以及如何操作它们,我们终于准备好尝试让 sNaN 做他们的事情并炸毁一些程序!

所以事不宜迟:

爆破.cpp

#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>

#pragma STDC FENV_ACCESS ON

int main() {
    float snan = std::numeric_limits<float>::signaling_NaN();
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float f;

    // No exceptions.
    assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);

    // Still no exceptions because qNaN.
    f = qnan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;

    // Now we can get an exception because sNaN, but signals are disabled.
    f = snan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
    feclearexcept(FE_ALL_EXCEPT);

    // And now we enable signals and blow up with SIGFPE! >:-)
    feenableexcept(FE_INVALID);
    f = qnan + 1.0f;
    std::cout << "feenableexcept qnan + 1.0f" << std::endl;
    f = snan + 1.0f;
    std::cout << "feenableexcept snan + 1.0f" << std::endl;
}

编译、运行并获取退出状态:

g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?

输出:

FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136

请注意,此行为仅-O0在 GCC 8.2 中发生:使用-O3GCC 会预先计算并优化我们所有的 sNaN 操作!我不确定是否有符合标准的方法来防止这种情况。

所以我们从这个例子中推断出:

  • snan + 1.0导致FE_INVALID,但qnan + 1.0不会

  • Linux 仅在使用 启用时才会生成信号feenableexept

    这是一个 glibc 扩展,我在任何标准中都找不到任何方法。

当信号发生时,是因为 CPU 硬件本身引发了异常,Linux 内核处理并通过信号通知应用程序。

结果是 bash 打印Floating point exception (core dumped),退出状态是136对应于signal 136 - 128 == 8,根据:

man 7 signal

SIGFPE

请注意,SIGFPE如果我们尝试将整数除以 0,则会得到相同的信号:

int main() {
    int i = 1 / 0;
}

虽然对于整数:

  • 将任何值除以零会产生信号,因为整数中没有无穷大表示
  • 默认情况下发生的信号,无需feenableexcept

如何处理 SIGFPE?

如果只是创建一个正常返回的处理程序,就会导致无限循环,因为处理程序返回后,除法又发生了!这可以通过 GDB 进行验证。

唯一的方法是使用setjmplongjmp跳转到其他地方,如下所示:C 处理信号 SIGFPE 并继续执行

sNaN 在现实世界中有哪些应用?

老实说,我仍然没有理解 sNaN 的一个超级有用的用例,这已被问到:有用的信号 NaN?

sNaNs 感觉特别无用,因为我们可以检测到0.0f/0.0f生成 qNaNs 的初始无效操作 () feenableexcept: 似乎snan只会引发更多操作的错误,而这些操作qnan不会引发,例如 ( qnan + 1.0f)。

例如:

主程序

#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>

int main(int argc, char **argv) {
    (void)argv;
    float f0 = 0.0;

    if (argc == 1) {
        feenableexcept(FE_INVALID);
    }
    float f1 = 0.0 / f0;
    printf("f1 %f\n", f1);

    feenableexcept(FE_INVALID);
    float f2 = f1 + 1.0;
    printf("f2 %f\n", f2);
}

编译:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm

然后:

./main.out

给出:

Floating point exception (core dumped)

和:

./main.out  1

给出:

f1 -nan
f2 -nan

另请参阅:如何在 C++ 中跟踪 NaN

什么是信号标志以及它们是如何被操纵的?

一切都在 CPU 硬件中实现。

标志存在于某个寄存器中,表示是否应该引发异常/信号的位也是如此。

这些寄存器可以从大多数拱门的用户区访问。

这部分glibc 2.29代码其实很容易理解!

例如,fetestexceptsysdeps/x86_64/fpu/ftestexcept.c 中为 x86_86 实现:

#include <fenv.h>

int
fetestexcept (int excepts)
{
  int temp;
  unsigned int mxscr;

  /* Get current exceptions.  */
  __asm__ ("fnstsw %0\n"
       "stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));

  return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)

所以我们立即看到使用的指令stmxcsr代表“存储 MXCSR 寄存器状态”。

feenableexceptsysdeps/x86_64/fpu/feenablxcpt.c 实现

#include <fenv.h>

int
feenableexcept (int excepts)
{
  unsigned short int new_exc, old_exc;
  unsigned int new;

  excepts &= FE_ALL_EXCEPT;

  /* Get the current control word of the x87 FPU.  */
  __asm__ ("fstcw %0" : "=m" (*&new_exc));

  old_exc = (~new_exc) & FE_ALL_EXCEPT;

  new_exc &= ~excepts;
  __asm__ ("fldcw %0" : : "m" (*&new_exc));

  /* And now the same for the SSE MXCSR register.  */
  __asm__ ("stmxcsr %0" : "=m" (*&new));

  /* The SSE exception masks are shifted by 7 bits.  */
  new &= ~(excepts << 7);
  __asm__ ("ldmxcsr %0" : : "m" (*&new));

  return old_exc;
}

C 标准对 qNaN 与 sNaN 有何不同?

C11 N1570 标准草案明确指出该标准在 F.2.1“无穷大、有符号零和 NaN”中没有区分它们:

1 本规范没有定义信号 NaN 的行为。它通常使用术语 NaN 来表示安静的 NaN。NAN 和 INFINITY 宏以及 nan 函数<math.h>为 IEC 60559 NaN 和无穷大提供了名称。

在 Ubuntu 18.10、GCC 8.2 中测试。GitHub 上游:

于 2019-04-12T09:05:58.297 回答