Як експериментально виглядають 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 вище за течією: