조용한 NaN과 신호 NaN의 차이점은 무엇입니까?


96

부동 소수점에 대해 읽었으며 NaN이 작업에서 발생할 수 있음을 이해합니다. 그러나 나는 이것이 개념이 정확히 무엇인지 이해할 수 없습니다. 그들 사이의 차이점은 무엇입니까?

C ++ 프로그래밍 중에 어떤 것이 생성 될 수 있습니까? 프로그래머로서 sNaN을 유발하는 프로그램을 작성할 수 있습니까?

답변:


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-08-08

18
반대로 NaN은보다 일반적인 프로그래밍을위한 것입니다. 수치 결과가없는 경우 (예 : 결과가 실수 여야 할 때 음수의 제곱근을 취함) 정상적인 연산으로 생성 할 수 있습니다. 그들의 목적은 일반적으로 산술이 어느 정도 정상적으로 진행되도록하는 것입니다. 예를 들어, 엄청난 숫자의 배열이있을 수 있으며, 그중 일부는 정상적으로 처리 할 수없는 특수한 경우를 나타냅니다. 복잡한 함수를 호출하여이 배열을 처리 할 수 ​​있으며 NaN을 무시하고 일반적인 산술로 배열에서 작동 할 수 있습니다. 종료 후 더 많은 작업을 위해 특수 사례를 분리합니다.
Eric Postpischil 2013 년

@wrdieter 감사합니다, 그러면 najor 차이 만 예외를 생성하는지 여부입니다.
JalalJaberi

@EricPostpischil 두 번째 질문에 관심을 가져 주셔서 감사합니다.
JalalJaberi

@JalalJaberi 예, 예외는 주요 차이점입니다
wrdieter 2013-08-10

34

qNaN 및 sNaN은 실험적으로 어떻게 생겼습니까?

먼저 sNaN 또는 qNaN이 있는지 식별하는 방법을 알아 보겠습니다.

편리 std::numeric_limits::quiet_NaN하고 std::numeric_limits::signaling_NaNC에서 편리하게 찾을 수 없었기 때문에 C 대신이 답변에서 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는 "의 strtod, strtof 및 strtold 기능"및 7.22.1.3는 말합니다 :

    문자 시퀀스 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)를 결정합니다. 후행 significand 필드에있는 나머지 비트는 진단 정보 일 수있는 페이로드를 인코딩합니다 (위 참조). 34

모든 이진 NaN 비트 문자열에는 편향 지수 필드 E의 모든 비트가 1로 설정됩니다 (3.4 참조). 조용한 NaN 비트 문자열은 후행 유효 필드 T의 첫 번째 비트 (d1)가 1이되도록 인코딩되어야합니다. 신호 NaN 비트 문자열은 후행 유효 필드의 첫 번째 비트가 0이되도록 인코딩되어야합니다. 후행 유효 필드는 0이고, 후행 유효 필드의 다른 비트는 NaN과 무한대를 구별하기 위해 0이 아니어야합니다. 방금 설명한 선호 인코딩에서, 신호 NaN은 d1을 1로 설정하여 조용히되어야하며 T의 나머지 비트는 변경되지 않습니다. 바이너리 형식의 경우 페이로드는 후행 유효 필드의 p-2 최하위 비트로 인코딩됩니다.

인텔 64 및 IA-32 아키텍처 소프트웨어 개발자 설명서 - 제 1 권 기본 아키텍처 - 253665-056US 2015년 9월 86이 가장 높은 분수 비트에 의해 NaN이와 sNaN을 구분하여 IEEE 754를 다음과 4.8.3.4 "NaN이"확인한다 것을 :

IA-32 아키텍처는 조용한 NaN (QNaN)과 신호 NaN (SNaN)의 두 가지 NaN 클래스를 정의합니다. QNaN은 최상위 비율 비트가 설정된 NaN이고 SNaN은 최상위 비율 비트가 명확한 NaN입니다.

그래서 않습니다 ARMv8, ARMv8-A 아키텍처 프로파일 - - 수동 ARM 아키텍처 참조 DDI 0487C.a A1.4.3 "단일 정밀도 부동 소수점 형식"

fraction != 0: 값은 NaN이고 조용한 NaN 또는 신호 NaN입니다. 두 가지 유형의 NaN은 최상위 분수 비트 인 비트 [22]로 구분됩니다.

  • bit[22] == 0: NaN은 신호 NaN입니다. 부호 비트는 모든 값을 가질 수 있으며 나머지 분수 비트는 모두 0을 제외한 모든 값을 가질 수 있습니다.
  • 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().

두 작업 모두 하드웨어에서 직접 qNaN을 생성하는 단일 x86 어셈블리 명령어로 컴파일됩니다 (TODO는 GDB로 확인).

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는 .NET과 함께 활성화 된 경우에만 신호를 생성합니다 feenableexept.

    이것은 glibc 확장이며 어떤 표준에서도 그렇게 할 수있는 방법을 찾을 수 없습니다.

신호가 발생하면 CPU 하드웨어 자체에서 예외가 발생하여 Linux 커널이 신호를 통해 애플리케이션에이를 처리하고 알립니다.

결과는 그 배시 인쇄물이며 Floating point exception (core dumped), 상기 종료 상태는 136어느 에 대응하는 신호 136 - 128 == 8에있어서, :

man 7 signal

입니다 SIGFPE.

SIGFPE우리가 0으로 정수를 분할하려는 경우 우리가 얻을 것과 같은 신호 :

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

정수의 경우 :

  • 정수에 무한대 표현이 없기 때문에 0으로 나누면 신호가 발생합니다.
  • 기본적으로 발생하는 신호입니다. feenableexcept

SIGFPE를 처리하는 방법?

정상적으로 반환되는 핸들러를 생성하면 핸들러가 반환 된 후 다시 분할이 발생하기 때문에 무한 루프가 발생합니다! 이것은 GDB로 확인할 수 있습니다.

유일한 방법은 다음 과 같이 사용 setjmp하고 longjmp다른 곳으로 이동하는 것입니다. C는 SIGFPE 신호를 처리하고 실행을 계속합니다.

sNaN의 실제 응용 프로그램은 무엇입니까?

솔직히, 나는 여전히 sNaN에 대한 매우 유용한 사용 사례를 이해하지 못했습니다. 이것은 다음에서 질문되었습니다. NaN 신호의 유용성?

우리는 초기 유효하지 않은 작업 (검색 할 수 있기 때문에 sNaNs 특히 쓸모 느낌 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 코드의이 부분은 실제로 이해하기 매우 쉽습니다!

예를 들어 sysdeps / x86_64 / fpu / ftestexcept.cfetestexcept 에서 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"를 나타내는 지침 사용이 즉시 표시 됩니다.

그리고 sysdeps / x86_64 / fpu / feenablxcpt.cfeenableexcept 에서 구현됩니다 .

#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 업스트림 :


en.wikipedia.org/wiki/IEEE_754#Interchange_formats 는 IEEE-754 가 NaN을 신호하는 데 0이 좋은 구현 선택 이라고 제안하기 만하면 NaN을 무한대 (유효 값 = 0)로 만들 위험없이 조용히 할 수 있습니다. x86이하는 일이지만 분명히 표준화되지 않았습니다. (그리고 qNaN 대 sNaN을 결정하는 것이 significand의 MSB라는 사실 표준화되었습니다). en.wikipedia.org/wiki/Single-precision_floating-point_format 은 x86과 ARM이 동일하다고 말하지만 PA-RISC는 반대 선택을했습니다.
Peter Cordes

@PeterCordes 예, IEEE 754 20at "신호 NaN 비트 문자열은 후행 유효 필드의 첫 번째 비트가 0이되도록 인코딩해야합니다"에서 "반드시"== "필수"또는 "선호"가 무엇인지 잘 모르겠습니다.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

re : 그러나 NaN과 무한대를 구별하는 데 사용할 비트를 지정하지 않는 것 같습니다. 표준에서 sNaN을 무한대와 구별하기 위해 설정하도록 권장하는 특정 비트가있을 것으로 예상 한대로 작성했습니다. IDK는 왜 그런 비트가있을 것으로 예상하는지; 0이 아닌 선택은 괜찮습니다. 나중에 sNaN의 출처를 식별하는 것을 선택하십시오. IDK, 그냥 이상한 문구처럼 들리는데,이 책을 읽었을 때 첫인상은 웹 페이지가 인코딩에서 inf와 NaN을 구별하는 것을 설명하지 않는다는 것입니다 (모두 0의 의미).
Peter Cordes

2008 년 이전에 IEEE 754는 어떤 값이 시그널링 / 조용한 비트 (비트 22)인지 명시했지만 어떤 값이 무엇을 지정했는지는 밝히지 않았습니다. 대부분의 프로세서는 1 = 조용함으로 수렴되었으므로 2008 년 버전의 표준의 일부가되었습니다. 동일한 선택을 부적합하게 만든 이전 구현을 피하기 위해 "반드시"보다는 "해야한다"고 말합니다. 일반적으로 표준에서 "해야한다"는 것은 "준수하지 않는 매우 설득력있는 (그리고 바람직하게는 잘 문서화 된) 이유가없는 한 반드시"를 의미합니다.
John Cowan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.