我已经阅读过关于浮点的内容,我知道NaN可能来自操作。但我无法完全理解这些概念是什么。有什么区别?
在C ++编程期间可以生成哪一个?作为程序员,我可以编写一个程序导致sNaN吗?
答案 0 :(得分:51)
当一个操作产生一个安静的NaN时,在程序检查结果并看到NaN之前,没有迹象表明任何异常。也就是说,如果在软件中实现浮点,则计算继续而没有来自浮点单元(FPU)或库的任何信号。信号NaN将产生信号,通常以FPU的异常形式产生。是否抛出异常取决于FPU的状态。
C ++ 11添加了一些语言controls over the floating-point environment并提供了standardized ways to create and test for NaNs。但是,控件是否实现的标准化程度不高,浮点异常通常不会像标准C ++异常那样被捕获。
在POSIX / Unix系统中,通常使用SIGFPE的处理程序捕获浮点异常。
答案 1 :(得分:4)
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
我们还可以使用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浮点数的结构,请查看:What is a subnormal floating point number?
以二进制形式表示的上述一些值是:
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 standard draft确认nanf()
生成安静的NaN,因为nanf
转发到strtod
和7.22.1.3“ strtod,strtof和strtold函数”说: / p>
字符序列NAN或NAN(n-char-sequence opt)被解释为安静 NaN(如果返回类型受支持),否则类似于主题序列部分,该部分没有 预期形式; n-char序列的含义是实现定义的。 293)
另请参阅:
qNaN和sNaN在手册中的外观如何?
IEEE 754 2008建议(TODO是必需的还是可选的?):
但是似乎并没有说最好使用哪一位来区分无穷大和NaN。
6.2.1“二进制格式的NaN编码”说:
本节进一步将NaN的编码指定为位字符串,当它们是运算结果时。 编码后,所有NaN都有一个符号位和一个位模式,以将编码识别为NaN 并确定其种类(sNaN与qNaN)。其余位,其尾随有效位 字段,对有效载荷进行编码,这可能是诊断信息(请参见上文)。 34
所有二进制NaN位字符串都将偏置指数字段E的所有位都设置为1(请参见3.4)。安静的NaN位 字符串应使用尾随有效位T的第一位(d1)进行编码。信令NaN 位字符串应使用尾随有效位字段的第一位为0进行编码。 尾随有效字段为0,尾随有效字段的其他位必须为非零才能区分 NaN从无穷大开始。在刚刚描述的首选编码中,应通过设置 d1到1,使T的其余位保持不变。 对于二进制格式,有效载荷编码在尾随有效位字段的p-2最低有效位中
Intel 64 and IA-32 Architectures Software Developer’s Manual - Volume 1 Basic Architecture - 253665-056US September 2015 4.8.3.4“ NaNs”通过以最高分数位区分NaN和sNaN来确认x86遵循IEEE 754:
IA-32体系结构定义了两类NaN:静默NaN(QNaN)和信令NaN(SNaN)。 QNaN是设置了最高有效位的NaN SNaN是设置了最高有效位的NaN。
ARM Architecture Reference Manual - ARMv8, for ARMv8-A architecture profile - 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?
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(使用GDB进行TODO确认)。
qNaN和sNaN有什么不同?
现在我们知道qNaN和sNaN是什么样子,以及如何操作它们,我们终于准备好尝试让sNaN做他们的事情并炸毁一些程序!
因此,事不宜迟:
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
请注意,此行为仅在GCC 8.2中的-O0
时发生:在-O3
中,GCC会预先计算并优化所有sNaN操作!我不确定是否有防止这种情况的符合标准的方法。
因此我们从此示例中得出:
snan + 1.0
导致FE_INVALID
,但qnan + 1.0
不会
只有在启用feenableexept
的情况下,Linux才会生成信号。
这是glibc扩展,在任何标准下我都找不到任何方法。
发生信号时,是因为CPU硬件本身引发了异常,Linux内核通过信号处理并通知了应用程序。
结果是bash打印Floating point exception (core dumped)
,退出状态为136
,corresponds to表示136 - 128 == 8
,根据:
man 7 signal
是SIGFPE
。
请注意,SIGFPE
与我们尝试将整数除以0时得到的信号相同:
int main() {
int i = 1 / 0;
}
尽管是整数:
feenableexcept
如何处理SIGFPE?
如果仅创建一个正常返回的处理程序,则会导致无限循环,因为在处理程序返回后,除法再次发生!可以使用GDB进行验证。
唯一的方法是使用setjmp
和longjmp
跳到其他地方,如C handle signal SIGFPE and continue execution
sNaN在现实世界中有哪些应用?
坦白地说,我仍然不了解sNaN的超级有用的用例,对此问题的询问是:Usefulness of signaling NaN?
sNaN感觉特别没用,因为我们可以检测到最初的无效操作(0.0f/0.0f
),该操作会使用feenableexcept
生成qNaN:似乎snan
只会为更多操作带来错误,其中{{1 }}不加注,例如(qnan
。
例如:
main.c
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
另请参阅:How to trace a NaN in C++
什么是信号标志以及如何对其进行操作?
一切都在CPU硬件中实现。
标志位于某个寄存器中,指示是否应引发异常/信号的位也存在。
大多数拱门中的寄存器是accessible from userland。
glibc 2.29代码的这一部分实际上非常容易理解!
例如,在sysdeps/x86_64/fpu/ftestexcept.c上为x86_86实现了f1 -nan
f2 -nan
:
fetestexcept
所以我们立即看到指令的使用是stmxcsr
,代表“存储MXCSR寄存器状态”。
#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)
在sysdeps/x86_64/fpu/feenablxcpt.c处实现:
feenableexcept
C标准关于qNaN与sNaN的说法是什么?
C11 N1570 standard draft明确表示该标准在F.2.1“无穷大,有符号零和NaN”中没有区别:
1本规范未定义NaN信号的行为。它通常使用术语NaN表示安静的NaN。
#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; }
中的NAN和INFINITY宏以及nan函数提供了IEC 60559 NaN和无穷大的指定。
已在Ubuntu 18.10,GCC 8.2中进行了测试。 GitHub上游: