我了解浮点数,并知道 NaN 的出现与操作有关。但是我不太明白这些概念的确切含义。它们之间有什么区别?
在 C++ 编程中,哪种类型的 NaN 可能出现?作为程序员,我能否编写一个会导致 sNaN 的程序?
我了解浮点数,并知道 NaN 的出现与操作有关。但是我不太明白这些概念的确切含义。它们之间有什么区别?
在 C++ 编程中,哪种类型的 NaN 可能出现?作为程序员,我能否编写一个会导致 sNaN 的程序?
当某个操作得到了一个 quiet NaN 时,直到程序检查结果并发现 NaN 时,没有任何异常指示。也就是说,如果浮点运算是通过软件实现的,那么即使不使用浮点单元 (FPU) 或库,计算也会继续进行。产生脉冲 NaN 将产生一个信号,通常以来自 FPU 的异常形式出现。是否抛出异常取决于 FPU 的状态。
C++11 添加了一些语言对浮点环境进行控制,并提供了创建和测试 NaNs 的标准化方法。然而,这些控制是否被实现并不太标准化,并且浮点异常通常无法像标准 C++ 异常一样捕获。
在 POSIX/Unix 系统中,通常使用 SIGFPE 的处理程序来捕获浮点异常。
如何实验性地区分qNaN和sNaN?
首先,让我们学习如何识别sNaN或qNaN。
在本答案中,我将使用C++而不是C,因为它提供了方便的std::numeric_limits::quiet_NaN
和std::numeric_limits::signaling_NaN
,而这些在C中不太容易找到。
但是,我无法找到一个函数来分类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
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 and sNaN seem to be differentiated only by bit 22: 1 means quiet, and 0 means signaling
infinities are also quite similar with exponent == 0xFF, but they have fraction == 0.
For this reason, NaNs must set bit 21 to 1, otherwise it would not be possible to distinguish sNaN from positive infinity!
nanf()
produces several different NaNs, so there must be multiple possible encodings:
7fc00000
7fc00001
7fc00002
Since nan0
is the same as std::numeric_limits<float>::quiet_NaN()
, we deduce that they are all different quiet NaNs.
The C11 N1570 standard draft confirms that nanf()
generates quiet NaNs, because nanf
forwards to strtod
and 7.22.1.3 "The strtod, strtof, and strtold functions" says:
A character sequence NAN or NAN(n-char-sequence opt ) is interpreted as a quiet NaN, if supported in the return type, else like a subject sequence part that does not have the expected form; the meaning of the n-char sequence is implementation-defined. 293)
另请参见:
qNaN和sNaN在手册中是什么样子的?
IEEE 754 2008建议(TODO强制或可选?):
但它似乎没有说明哪个位更适合区分无穷大和NaN。
6.2.1“二进制格式中的NaN编码”说:
本子句进一步指定了NaN的编码方式,当它们是操作结果时,需要将其编码为位字符串。在编码时,所有NaN都有一个符号位和必要的位模式来识别编码为NaN并确定其种类(sNaN vs. qNaN)。剩余的位在尾数字段中,编码有效负载,这可能是诊断信息(见上文)。所有二进制NaN位字符串的偏置指数字段E的所有位都设置为1(见3.4)。安静NaN位字符串应使用尾数字段T的第一位(d1)为1进行编码。信令NaN位字符串应使用尾数字段的第一位为0进行编码。如果尾数字段的第一位为0,则必须有其他位的尾数字段非零以区分NaN和无穷大。在刚才描述的首选编码中,通过将d1设置为1并保持T的其余位不变,可以使信令NaN静音。对于二进制格式,有效负载编码在尾数字段的p-2个最低有效位中。英特尔64和IA-32体系结构软件开发人员手册-卷1基本架构-253665-056US 2015年9月的4.8.3.4“NaNs”证实,x86遵循IEEE754标准,通过区分最高分数位的NaN和sNaN来区分它们:
IA-32架构定义了两类NaN:quiet NaN(QNaNs)和signaling NaN(SNaNs)。 QNaN是最高有效小数位设置为SNaN是最高有效小数位清除的NaN。
ARM架构参考手册-ARMv8,适用于ARMv8-A架构配置文件-DDI 0487C.a的A1.4.3“单精度浮点格式”也是如此:
fraction!=0
:该值为NaN,可以是quiet NaN或signaling NaN。这两种类型的NaN由它们的最高分数位bit [22]区分:
bit[22] == 0
: NaN是一种信号NaN。符号位可以取任何值,其余的分数位可以取任何非零值。bit[22] == 1
: NaN是一种安静NaN。符号位和剩余的分数位可以取任何值。qNaNS和sNaNS是如何生成的?
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()
完全一样。blow_up.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
时发生:使用-O3
,GCC会预先计算并优化所有我们的sNaN操作!我不确定是否有符合标准的方法来防止这种情况发生。snan + 1.0
会导致FE_INVALID
,但qnan + 1.0
则不会。
- 仅当使用feenableexept
启用时,Linux才会生成信号。
这是glibc的扩展,在任何标准中都找不到任何方法来实现它。
当信号发生时,是因为CPU硬件本身引发了异常,Linux内核通过信号处理并告知应用程序。Floating point exception (core dumped)
,退出状态为136
,对应信号136 - 128 == 8
,根据:man 7 signal
是 SIGFPE
信号。
请注意,如果我们试图将整数除以0,我们将得到相同的信号:SIGFPE
。
int main() {
int i = 1 / 0;
}
虽然对于整数:
feenableexcept
如何处理SIGFPE?
如果只创建一个正常返回的处理程序,它会导致无限循环,因为在处理程序返回后,除法会再次发生!可以通过GDB进行验证。
唯一的方法是使用setjmp
和longjmp
跳转到其他地方,如所示:C handle signal SIGFPE and continue execution
sNaNs的一些实际应用是什么?
说实话,我还没有理解sNaN的超级有用的用例,这已经被问过了:Usefulness of signaling NaN?
sNaN感觉特别无用,因为我们可以使用feenableexcept
检测生成qNaN的初始无效操作(0.0f/0.0f
):似乎snan
仅针对qnan
不会引发错误的更多操作引发错误,例如(qnan + 1.0f
)。
E.g.:
main.c
#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
什么是信号标志(signal flags),它们如何操作?
所有内容都是由CPU硬件实现的。
标志位存储在某个寄存器中,一个比特位表示是否应该引发异常/信号。
这些寄存器从大多数架构的用户空间 可以访问。
glibc 2.29代码的这一部分实际上非常容易理解!
例如,fetestexcept
在 x86_64 上实现于 sysdeps/x86_64/fpu/ftestexcept.c:
#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宏以及
<math.h>
中的nan函数提供了IEC 60559 NaN和无穷大的指示。
在Ubuntu 18.10,GCC 8.2中进行测试。GitHub上游: