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

问题描述 投票:72回答:2

我读过关于浮点的内容,我知道NaN可能来自操作。但我无法完全理解这些概念是什么。有什么区别?

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

floating-point nan ieee-754
2个回答
53
投票

当操作导致安静的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的处理程序捕获。


8
投票

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

让我们首先学习如何识别我们是否有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

并且产生完全相同的输出,表明多个arch紧密地实现了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表示信号
  • 无穷大也与exponent == 0xFF非常相似,但它们的分数== 0。 因此,NaN必须将第21位设为1,否则无法将sNaN与正无穷大区分开来!
  • nanf()生成几种不同的NaN,因此必须有多种可能的编码: 7fc00000 7fc00001 7fc00002 由于nan0std::numeric_limits<float>::quiet_NaN()相同,我们推断它们都是不同的安静NaN。 C11 N1570 standard draft证实nanf()产生安静的NaNs,因为nanf转发到strtod和7.22.1.3“strtod,strtof和strtold函数”说: 如果在返回类型中支持,则字符序列NAN或NAN(n-char-sequence opt)被解释为安静的NaN,否则类似于不具有预期形式的主题序列部分; n-char序列的含义是实现定义的。 293)

也可以看看:

qNaNs和sNaNs在手册中的外观如何?

IEEE 754 2008建议(TODO强制性或可选?):

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

但似乎没有说哪个位更适合区分无穷大和NaN。

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

该子条款进一步规定了NaNs作为位串的编码,当它们是操作结果时。编码时,所有NaN都有一个符号位和一个比特模式,用于将编码标识为NaN并确定其类型(sNaN与qNaN)。尾随有效数字段中的其余位编码有效载荷,这可能是诊断信息(见上文)。 34

所有二进制NaN位串都将偏置指数字段E的所有位设置为1(见3.4)。应该用尾随有效位字段T的第一位(d1)编码安静的NaN位串。应当用尾随有效位字段的第一位为0来编码信令NaN位串。如果第一位尾随有效位字段为0,尾随有效位字段的其他位必须为非零以区分NaN和无穷大。在刚才描述的优选编码中,通过将d1设置为1来保持信令NaN,使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以其最重要的分数位,位[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报价,但我也找不到任何产生sNaNs的内置操作;-)

英特尔手册明确说明了这一原则,但是在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做他们的事情并吹嘘一些程序了!

所以不用多说:

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,则Linux仅生成信号。 这是一个glibc扩展,在任何标准中都找不到任何方法。

当信号发生时,它是因为CPU硬件本身引发了异常,Linux内核处理该异常并通过信号通知应用程序。

结果是bash打印Floating point exception (core dumped),退出状态是136corresponds to信号136 - 128 == 8,根据:

man 7 signal

SIGFPE

请注意,如果我们尝试将整数除以0,SIGFPE与我们得到的信号相同:

int main() {
    int i = 1 / 0;
}

虽然对于整数:

  • 将任何东西除以零会提升信号,因为整数中没有无穷大表示
  • 它默认发生的信号,不需要feenableexcept

如何处理SIGFPE?

如果你只是创建一个正常返回的处理程序,它会导致无限循环,因为在处理程序返回之后,除法再次发生!这可以通过GDB验证。

唯一的方法是使用setjmplongjmp跳到其他地方,如下所示:C handle signal SIGFPE and continue execution

什么是sNaNs的真实世界应用?

老实说,我仍然没有理解sNaNs的一个超级有用的用例,这已被问到:Usefulness of signaling NaN?

sNaN感觉特别无用,因为我们可以检测到使用0.0f/0.0f生成qNaNs的初始无效操作(feenableexcept):看来snan只是为qnan没有提高的更多操作引发错误,例如: (qnan + 1.0f)。

Ef。:

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

另见:How to trace a NaN in C++

什么是信号标志以及它们是如何被操纵的?

一切都在CPU硬件中实现。

标志位于某个寄存器中,如果应该引发异常/信号,则该位也是如此。

这些登记册是来自大多数拱门的accessible from userland

这部分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,它代表“Store MXCSR Register State”。

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;
}

关于qNaN和sNaN,C标准有什么看法?

C11 N1570 standard draft明确表示标准不区分F.2.1“Infinities,signed zero和NaNs”:

1本规范未定义信令NaN的行为。它通常使用术语NaN来表示安静的NaN。 NAN和INFINITY宏以及<math.h>中的nan函数为IEC 60559 NaN和无穷大提供了名称。

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

© www.soinside.com 2019 - 2024. All rights reserved.