安静 NaN 和信号 NaN 有什么区别?

我读过关于浮点的文章,我知道 NaN 可能是操作的结果。但我不能理解这些到底是什么概念。他们之间有什么区别?

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

44723 次浏览

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

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

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

QNaN 和 sNaN 在实验中看起来如何?

让我们首先了解如何识别 sNaN 还是 qNaN。

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

但是,如果 NaN 是 sNaN 或 qNaN,我无法找到一个函数进行分类,所以让我们打印出 NaN 原始字节:

Main.cpp

#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

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

此时,如果您不熟悉 IEEE754浮点数的结构,请看一下: 什么是次正常浮点数?

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

     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
    

    由于 nan0std::numeric_limits<float>::quiet_NaN()是一样的,我们推断它们都是不同的安静 NaN。

    C11 N1570标准稿确认 nanf()生成了安静的 NaNs,因为 nanf转发到 strtod和7.22.1.3“ strtod、 strtof 和 strtold 函数”说:

    字符序列 NAN 或 NAN (n- 字符序列 opt)被解释为静音 如果返回类型支持 NaN,则表示主题序列部分没有 期望的形式; n-char 序列的含义是实现定义的

参见:

手册中的 qNaN 和 sNaN 是什么样子的?

IEEE 7542008 建议(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)进行编码,字段 T 为1 位字符串应该以后跟有效字段的第一个位为0进行编码。如果 尾随有效字段为0,其他一些位的尾随有效字段必须是非零才能区分 在刚才描述的首选编码中,信令 NaN 应通过设置 D1到1,保持 T 的剩余位不变。 对于二进制格式,有效负载编码在后跟有效字段的 p-2最低有效位中

Intel 64和 IA-32架构软件开发人员手册-第1卷基本架构-253665-056US September 20154.8.3.4“ NaNs”确认 x86遵循 IEEE 754,通过最高分数位来区分 NaN 和 sNaN:

IA-32架构定义了两类 NaNs: 静态 NaNs (QNaNs)和信令 NaNs (SNNs)。QNaN 是具有最高有效分数位集的 NaN,SNaN 是具有最高有效分数位清晰的 NaN。

ARM 架构参考手册-ARMv8,用于 ARMv8-一个架构配置文件-DDI 0487C. aa1.4.3“单精确浮点数”也是如此:

fraction != 0: 该值是 NaN,它可以是一个安静的 NaN,也可以是一个发信号的 NaN。这两种 NaN 的区别在于它们最重要的分数位,位[22] :

  • bit[22] == 0: NaN 是一个发信号的 NaN。符号位可以接受任何值,其余的分数位可以接受除了所有零以外的任何值。
  • NaN 是一个安静的 NaN。符号位和剩余的分数位可以取任何值。

如何生成 qNanS 和 sNaN?

QNaNs 和 sNaNs 的一个主要区别是:

  • QNaN 是通过具有奇怪值的常规内置(软件或硬件)算术操作生成的
  • SNaN 从来不是由内置操作生成的,它只能由程序员显式地添加,例如使用 std::numeric_limits::signaling_NaN

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

然而,英特尔手册在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 (用 GDB 确认 TODO)。

QNaN 和 sNaN 有什么不同?

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

言归正传:

爆炸

#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

注意,这种行为只发生在 GCC 8.2中的 -O0中: 对于 -O3,GCC 预先计算并优化所有的 sNaN 操作!我不确定是否有一个标准的顺从的方法来防止这种情况的发生。

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

  • snan + 1.0引起 FE_INVALID,但 qnan + 1.0不会

  • Linux 只有在使用 feenableexept启用时才会生成信号。

    这是一个 glibc 扩展,我找不到任何标准的方法来做到这一点。

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

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

man 7 signal

SIGFPE

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

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

不过对于整数:

  • 因为整数中没有无穷大的表示,所以除以零就产生了信号
  • 默认情况下发生的信号,而不需要 feenableexcept

如何处理 SIGFPE?

如果您只是创建一个正常返回的处理程序,那么它将导致一个无限循环,因为在处理程序返回之后,除法将再次发生!这可以用 GDB 进行验证。

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

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

老实说,我还没有理解一个超级有用的 sNaNs 用例,这个问题已经在: 信号 NaN 的有用性?提出

SNaNs 感觉特别无用,因为我们可以检测到用 feenableexcept生成 qNaNs 的初始无效操作(0.0f/0.0f) : 似乎 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代码的这一部分实际上非常容易理解!

例如,fetestexcept是在 Sysdeps/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 寄存器状态”。

feenableexcept是在 Sysdeps/x86 _ 64/fpu/feablelxcpt.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;
}

关于 qNaN 和 sNaN,C 标准说了什么?

C11 N1570标准稿明确表示,该标准在 F.2.1“无穷大、有符号的零和 NaNs”不区分它们:

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

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