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

2025-08-30 08:21:30

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

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

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

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

主文件

#include

#include

#include // nanf, isnan

#include

#include // 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::has_quiet_NaN, "");

static_assert(std::numeric_limits::has_signaling_NaN, "");

static_assert(std::numeric_limits::has_infinity, "");

// Generate them.

float qnan = std::numeric_limits::quiet_NaN();

float snan = std::numeric_limits::signaling_NaN();

float inf = std::numeric_limits::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::quiet_NaN(),我们推断它们都是不同的安静 NaN。

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

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

也可以看看:

如何在 c 中产生 NaN 浮点数?

C/C++ NaN 常量(字面量)?

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::quiet_NaN()。

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

qNaNs 和 sNaNs 有什么不同?

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

所以事不宜迟:

爆破.cpp

#include

#include

#include // isnan

#include

#include // std::numeric_limits

#include

#pragma STDC FENV_ACCESS ON

int main() {

float snan = std::numeric_limits::signaling_NaN();

float qnan = std::numeric_limits::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 进行验证。

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

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

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

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

例如:

主程序

#define _GNU_SOURCE

#include

#include

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

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/feenablxcpt.c 实现:

#include

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 函数为 IEC 60559 NaN 和无穷大提供了名称。

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

c/nan.c

cpp/nan.cpp

glibc/interactive/feenableexcept.c