Подтвердить что ты не робот

В чем разница между тихим NaN и сигнализацией NaN?

Я читал о плавающей точке, и я понимаю, что NaN может быть результатом операций. но я не могу понять, что это за понятия. В чем разница?

Какой из них можно создать во время программирования на C++? Как программист, могу ли я написать программу, вызвав sNaN?

4b9b3361

Ответ 1

Когда операция приводит к тихим NaN, нет никаких признаков того, что что-то необычно, пока программа не проверит результат и не увидит NaN. То есть вычисления продолжаются без какого-либо сигнала от блока с плавающей точкой (FPU) или библиотеки, если с плавающей точкой реализовано программное обеспечение. Сигнал NaN будет выдавать сигнал, обычно в виде исключения из FPU. Исключено ли исключение, зависит от состояния FPU.

С++ 11 добавляет несколько языков контроль над средой с плавающей запятой и предоставляет стандартизованные способы создания и тестирования NaNs. Однако не реализованы ли стандартные средства управления, а исключения с плавающей запятой обычно не пойманы так же, как стандартные исключения С++.

В системах POSIX/Unix исключения с плавающей запятой обычно захватываются с помощью обработчика для SIGFPE.

Ответ 2

Как qNaNs и sNaNs выглядят экспериментально?

Давайте сначала узнаем, как определить, есть ли у нас 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 в пользовательском режиме QEMU:

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(), мы 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)

Смотрите также:

Как выглядят qNaNs и sNaNs в руководствах?

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 должна быть закодирована с первым битом (d1) завершающего значения и поля T, равным 1. Строка сигнального бита NaN должна быть закодирована с первым битом поля завершающего значения и, равным 0. Если первый бит конечное значение и поле равно 0, другой бит конечного значения и поле должно быть ненулевым, чтобы отличать NaN от бесконечности. В только что описанном предпочтительном кодировании сигнализирующий NaN должен быть отключен установкой d1 в 1, оставляя оставшиеся биты T неизменными. Для двоичных форматов полезная нагрузка кодируется в p-2 младших значащих битах поля завершающего значения и

Руководство для разработчиков программного обеспечения для архитектуры Intel 64 и IA-32 - том 1, базовая архитектура - 253665-056RU сентябрь 2015 г. 4.8.3.4 "NaNs" подтверждает, что x86 следует IEEE 754, различая NaN и sNaN по биту с наибольшей долей:

Архитектура 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 различаются по их наиболее значимой доле бит, бит [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 ;-)

Однако в руководстве Intel этот принцип четко изложен в разделе 4.8.3.4 "NaNs":

SNaNs обычно используются для перехвата или вызова обработчика исключений. Они должны быть вставлены программным обеспечением; то есть процессор никогда не генерирует 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 по-разному?

Теперь, когда мы знаем, как выглядят qNaNs и sNaNs и как ими манипулировать, мы наконец-то готовы попытаться заставить 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, я не мог найти способ сделать это ни в одном стандарте.

Когда происходит сигнал, это происходит потому, что само оборудование ЦП вызывает исключение, которое ядро Linux обрабатывает и сообщает приложению через сигнал.

В результате bash печатает Floating point exception (core dumped), а состояние выхода равно 136, что соответствует сигналу 136 - 128 == 8, что в соответствии с:

man 7 signal

это SIGFPE.

Обратите внимание, что SIGFPE - это тот же сигнал, который мы получаем, если пытаемся разделить целое число на 0:

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

хотя для целых чисел:

  • деление чего-либо на ноль поднимает сигнал, так как в целых числах нет представления бесконечности
  • сигнал это происходит по умолчанию, без необходимости в feenableexcept

Как обращаться с SIGFPE?

Если вы просто создаете обработчик, который возвращает нормально, это приводит к бесконечному циклу, потому что после того, как обработчик возвращается, деление происходит снова! Это можно проверить с помощью GDB.

Единственный способ - использовать setjmp и longjmp для перехода в другое место, как показано на: C обрабатывает сигнал SIGFPE и продолжает выполнение

Каковы некоторые реальные применения sNaNs?

Честно говоря, я до сих пор не понял супер полезного варианта использования для sNaNs, об этом спрашивали: Полезность сигнализации 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

Смотрите также: Как отследить NaN в C++

Что такое сигнальные флаги и как ими манипулируют?

Все реализовано в оборудовании процессора.

Флаги живут в некотором регистре, как и бит, который сообщает, должно ли возникать исключение/сигнал.

Эти регистры доступны из пользовательского пространства из большинства архивов.

Эта часть кода glibc 2.29 на самом деле очень проста для понимания!

Например, fetestexcept реализован для x86_86 в 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 и функции nan в <math.h> предоставляют обозначения для NaN в IEC 60559 и бесконечностей.

Протестировано в Ubuntu 18.10, GCC 8.2. GitHub upstreams: