我已阅读有关浮点的信息,并且我了解 NaN 可能是由操作产生的。但我不明白这些到底是什么概念。它们之间有什么区别?
在 C++ 编程过程中可以产生哪一个?作为程序员,我可以编写一个导致 sNaN 的程序吗?
我已阅读有关浮点的信息,并且我了解 NaN 可能是由操作产生的。但我不明白这些到底是什么概念。它们之间有什么区别?
在 C++ 编程过程中可以产生哪一个?作为程序员,我可以编写一个导致 sNaN 的程序吗?
当操作导致安静的 NaN 时,在程序检查结果并看到 NaN 之前,没有任何迹象表明有任何异常。也就是说,如果浮点是在软件中实现的,那么在没有来自浮点单元 (FPU) 或库的任何信号的情况下继续计算。一个信号 NaN 将产生一个信号,通常以来自 FPU 的异常的形式。是否抛出异常取决于 FPU 的状态。
C++11在浮点环境中添加了一些语言控件,并提供了创建和测试 NaN 的标准化方法。但是,控件是否被实现并没有很好地标准化,并且浮点异常通常不会像标准 C++ 异常那样被捕获。
在 POSIX/Unix 系统中,浮点异常通常使用SIGFPE的处理程序来捕获。
qNaNs 和 sNaNs 在实验上看起来如何?
让我们首先学习如何识别我们是 sNaN 还是 qNaN。
我将在这个答案中使用 C++ 而不是 C,因为它提供了方便std::numeric_limits::quiet_NaN
,std::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,因为转发nanf
到strtod
7.22.1.3 “strtod、strtof 和 strtold 函数”说:
如果返回类型支持,则字符序列 NAN 或 NAN(n-char-sequence opt) 被解释为安静的 NaN,否则就像不具有预期形式的主题序列部分;n-char 序列的含义是实现定义的。293)
也可以看看:
qNaN 和 sNaN 在手册中的外观如何?
IEEE 754 2008建议(TODO 是强制性的还是可选的?):
但似乎并没有说明哪个位更适合区分无穷大和 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 之间的一个主要区别是:
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 中发生:使用-O3
GCC 会预先计算并优化我们所有的 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 <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/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 上游: