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


96

我已经读过有关浮点的知识,并且我知道NaN可能是由操作导致的。但是我不明白这些到底是什么概念。它们之间有什么区别?

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

Answers:


68

当操作导致安静的NaN时,直到程序检查结果并看到NaN为止,才表示有任何异常。也就是说,如果在软件中实现浮点,则计算将继续进行,而不会收到来自浮点单元(FPU)或库的任何信号。信号NaN将产生信号,通常以来自FPU的异常形式出现。是否引发异常取决于FPU的状态。

C ++ 11 在浮点环境中添加了一些语言控件,并提供了用于创建和测试NaN的标准化方法。但是,控件的实现是否得到了很好的标准化,并且浮点异常通常无法以与标准C ++异常相同的方式捕获。

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


34
补充说明:通常,信号NaN(sNaN)的目的是用于调试。例如,浮点对象可能被初始化为sNaN。然后,如果程序在使用前无法给其中一个值赋值,则当程序在算术运算中使用sNaN时,将发生异常。程序不会无意间产生sNaN;正常操作不会产生sNaN。它们仅是为了具有信令NaN的目的而专门创建的,而不是任何算术的结果。
Eric Postpischil 2013年

18
相反,NaN用于更常规的编程。当没有数值结果时(例如,当结果必须为实数时,取负数的平方根)可以通过常规运算生成它们。它们的目的通常是允许算术正常进行。例如,您可能有大量数字,其中一些代表无法正常处理的特殊情况。您可以调用一个复杂的函数来处理此数组,并且可以使用常规算法在该数组上操作而忽略NaN。结束后,您将特殊情况分开进行更多工作。
Eric Postpischil 2013年

@wrdieter谢谢,那么只有最主要的不同是产生过异常。
JalalJaberi

@EricPostpischil感谢您关注第二个问题。
JalalJaberi

@JalalJaberi是的,主要的不同是例外
wrdieter

34

qNaN和sNaN在实验中的外观如何?

首先让我们学习如何识别我们是否有sNaN或qNaN。

我将在此答案中使用C ++而不是C,因为它提供了便利std::numeric_limits::quiet_NaNstd::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浮点数的结构,请看一下:什么是次正规浮点数?

以二进制形式,上面的一些值是:

     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是强制性还是可选?):

  • 指数== 0xFF且分数!= 0的任何东西都是NaN
  • 并且最高分数位将qNaN与sNaN区别开

但是似乎并没有说最好使用哪一位来区分无穷大和NaN。

6.2.1“二进制格式的NaN编码”说:

本子节进一步将NaN的编码指定为位字符串,当它们是运算结果时。编码后,所有NaN都有一个符号位和一个位模式,这些位将识别编码为NaN并确定其种类(sNaN与qNaN)是必需的。尾随有效位字段中的其余位对有效载荷进行编码,这可能是诊断信息(请参见上文)。34

所有二进制NaN位串都将偏置指数字段E的所有位都设置为1(请参见3.4)。安静的NaN位串应以尾随有效位T的第一位(d1)为1进行编码。信令NaN位串应以尾随有效域的第一位为0进行编码。尾随有效字段为0,尾随有效字段的其他位必须为非零才能区分NaN和无穷大。在刚刚描述的优选编码中,应通过将d1设置为1来静默信令NaN,而保留T的其余比特不变。对于二进制格式,有效载荷以尾随有效字段的p-2最低有效位进行编码

英特尔64和IA-32架构软件开发人员手册-第1卷基础架构- 253665-056US 2015年9月 4.8.3.4“的NaN”证实,86由最高分数位区分楠SNAN遵循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?

qNaN和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

请注意,此行为仅-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进行验证。

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

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

老实说,我仍然不了解sNaN的超级有用用例,有人问过:用信号通知NaN的有用性?

sNaN感觉特别没用,因为我们可以用来检测0.0f/0.0f生成qNaNs 的初始无效操作()feenableexcept:似乎snan只是对更多的操作产生了错误,而qnan对于(qnan + 1.0f)则没有。

例如:

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

信号标志是什么?如何操作?

一切都在CPU硬件中实现。

这些标志位于某个寄存器中,该位也表示是否应引发异常/信号。

可以从大多数拱门的用户区访问这些寄存器。

glibc 2.29代码的这一部分实际上非常容易理解!

例如,fetestexceptsysdeps / 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寄存器状态”。

feenableexceptsysdeps / 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和Infinities提供名称。

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


en.wikipedia.org/wiki/IEEE_754#Interchange_formats指出,IEEE-754仅建议使用0表示NaN信号是一个不错的实现选择,它可以使NaN静默而不冒无穷大的风险(significand = 0)。显然,这不是标准的,尽管x86是这样做的。(而且,决定qNaN与sNaN的有效位的最高有效位这一事实标准化的)。 en.wikipedia.org/wiki/Single-precision_floating-point_format表示x86和ARM相同,但PA-RISC却选择了相反的选择。
彼得·科德斯

@PeterCordes是的,我不确定IEEE 754 20中的“应该” ==“必须”还是“首选”在“信号NaN位字符串应以尾随有效位字段的第一位为0的情况下进行编码”。
西罗Santilli郝海东冠状病六四事件法轮功

回复:但似乎没有指定应使用哪一位来区分无穷大和NaN。 您写道,就像您期望的那样,标准建议设置一些特定的位以区分sNaN和无穷大。IDK为什么您希望会有这样的事情?任何非零的选择都可以。只需选择以后可以确定sNaN来自何处的东西即可。IDK听起来像是很奇怪的措辞,而我在阅读它时的第一印象是,您说的是网页并没有描述inf和NaN在编码中的区别(全零有效位)。
彼得·科德斯

在2008年之前,IEEE 754表示哪个是信令/安静位(第22位),但没有哪个值指定了什么。大多数处理器都收敛于1 =安静,因此已成为2008年版标准的一部分。它说“应该”而不是“必须”,以避免使较早的实现使同一选择不符合要求。通常,标准中的“应该”表示“必须,除非您有非常令人信服的(并且最好是有据可查的)不遵守的理由”。
John Cowan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.