Яка різниця між тихим NaN і сигнальним NaN?


96

Я читав про плаваючу крапку і розумію, що NaN може бути результатом операцій. Але я не можу зрозуміти, що це саме за поняття. У чому різниця між ними?

Яку можна створити під час програмування на C ++? Як програміст, чи можу я написати програму, яка викликає sNaN?

Відповіді:


68

Коли операція призводить до спокійного NaN, немає ознак того, що нічого незвичайного, поки програма не перевірить результат і не побачить NaN. Тобто, обчислення продовжуються без будь-якого сигналу від блоку з плаваючою комою (FPU) або бібліотеки, якщо плаваюча точка реалізована в програмному забезпеченні. Сигналізація NaN подає сигнал, як правило, у формі виключення з FPU. Чи буде викинуто виняток, залежить від стану ФПУ.

C ++ 11 додає кілька мовних елементів контролю над середовищем з плаваючою комою та надає стандартизовані способи створення та тестування NaN . Однак, чи реалізовано керування недостатньо стандартизовано і винятки з плаваючою комою, як правило, не приймаються так само, як стандартні винятки C ++.

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


34
Додавання до цього: Зазвичай, мета сигналізації NaN (sNaN) - це налагодження. Наприклад, об'єкти з плаваючою комою можуть бути ініціалізовані до sNaN. Потім, якщо програмі не вдасться до одного з них значення перед його використанням, виняток виникне, коли програма використовує sNaN в арифметичній операції. Програма не створить sNaN ненавмисно; жодна нормальна операція не виробляє sNaN. Вони створені спеціально для того, щоб мати сигнальний NaN, а не як результат будь-якої арифметики.
Eric Postpischil

18
На відміну від цього, NaN - це для більш нормального програмування. Вони можуть бути отримані звичайними операціями, коли немає числового результату (наприклад, взяття квадратного кореня від'ємного числа, коли результат повинен бути реальним). Їх призначення, як правило, дозволяють арифметиці протікати дещо нормально. Наприклад, у вас може бути величезний масив номерів, деякі з яких представляють особливі випадки, з якими неможливо нормально працювати. Ви можете викликати складну функцію для обробки цього масиву, і він міг працювати з масивом зі звичайною арифметикою, ігноруючи NaN. Після того, як це закінчиться, ви розділили б особливі справи для більшої роботи.
Eric Postpischil

@wrdieter Дякую, то лише найрізноманітніша різниця генерує вивільнення чи ні.
JalalJaberi

@EricPostpischil Дякую за увагу до другого питання.
JalalJaberi

@JalalJaberi так, виняток становить основна відмінність
wrdieter

34

Як експериментально виглядають 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(), ми робимо висновок, що всі вони є тихими NaN.

    В C11 N1570 проект стандарту підтверджує , що nanf()породжує тихими нехтує малих, бо nanfвперед до strtodі 7.22.1.3 «The strtod, strtof і strtold функцій» , говорить:

    Послідовність символів NAN або NAN (опція n-char-послідовності) інтерпретується як тихий 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-056US вересня 2015 4.8.3.4 «Nans» підтверджує , що x86 слід IEEE 754, розрізняючи NaN і sNaN найвищої фракції біт:

Архітектура IA-32 визначає два класи NaN: тихі NaNs (QNaNs) та сигнальні NaNs (SNaNs). 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":

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 і sNaN по-різному?

Тепер, коли ми знаємо, як виглядають qNaNs і sNaNs, і як ними маніпулювати, ми нарешті готові спробувати змусити sNaNs робити свою справу і підірвати деякі програми!

Тож без зайвих прихисток:

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 обробляло та інформувало додаток через сигнал.

Результатом є те , що відбитки Баш 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 та продовжувати виконання

Які існують реальні програми sNaN?

Чесно кажучи, я досі не зрозумів надзвичайно корисного випадку використання sNaN, про це запитали: Корисність передачі сигналу NaN?

sNaNs відчувають себе особливо марними, оскільки ми можемо виявити початкові недійсні операції ( 0.0f/0.0f), які генерують qNaN за допомогою 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 «Нескінченності, підписані нулі, і NaNs»:

1 Ця специфікація не визначає поведінку сигналізації NaN. Для позначення спокійних NaN зазвичай використовується термін NaN. Макроси NAN та INFINITY та функції nan <math.h>надають позначення для IEC 60559 NaNs та нескінченності.

Випробувано в Ubuntu 18.10, GCC 8.2. GitHub вище за течією:


en.wikipedia.org/wiki/IEEE_754#Interchange_formats вказує, що IEEE-754 просто пропонує 0 для сигналізації NaN - це гарний вибір реалізації, щоб дозволити замовкнути NaN, не ризикуючи зробити його нескінченним (significand = 0). Мабуть, це не стандартизовано, хоча це те, що робить x86. (І той факт, що саме MSB значущості і визначає qNaN проти sNaN, є стандартизованим). en.wikipedia.org/wiki/Single-precision_floating-point_format говорить про те, що x86 і ARM однакові, але PA-RISC зробив протилежний вибір.
Пітер Кордес

@PeterCordes так, я не впевнений, що в "IEEE 754 20at" має бути "=" "повинен" або "кращим" сигнальний рядок біт сигналізації NaN повинен бути закодований, коли перший біт поля значущого сигналу та 0 буде 0 ".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

re: але, схоже, не визначено, який біт слід використовувати для диференціації нескінченності від NaN. Ви писали, що, як ви очікували, буде якийсь певний біт, який стандарт рекомендує встановити, щоб відрізнити sNaN від нескінченності. IDK, чому б ви очікували, що буде такий бит; будь-який ненульовий вибір добре. Просто виберіть щось, що пізніше визначить, звідки взявся sNaN. IDK, це просто звучить як дивне фразування, і моє перше враження, читаючи, було те, що ви говорили, що веб-сторінка не описує те, що відрізняє inf від NaN в кодуванні (значення, що має нуль).
Пітер Кордес

До 2008 року IEEE 754 говорив, що є сигнальним / тихим бітом (біт 22), але не яке значення визначає. Більшість процесорів зійшлися на 1 = тихо, так що це було частиною стандарту у випуску 2008 року. У ньому йдеться "слід", а не "повинен", щоб не робити старіших реалізацій, які зробили той самий вибір невідповідним. Загалом, "слід" у стандартних засобах "повинно бути, якщо ви не маєте дуже вагомих (і бажано, добре задокументованих) причин невідповідності".
Джон Коуан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.