Bagaimana qNaNs dan sNaNs terlihat secara eksperimental?
Pertama-tama, mari pelajari cara mengidentifikasi apakah kita memiliki sNaN atau qNaN.
Saya akan menggunakan C ++ dalam jawaban ini daripada C karena ia menawarkan kemudahan std::numeric_limits::quiet_NaN
dan std::numeric_limits::signaling_NaN
yang tidak dapat saya temukan di C dengan nyaman.
Namun saya tidak dapat menemukan fungsi untuk mengklasifikasikan jika NaN adalah sNaN atau qNaN, jadi mari kita mencetak byte mentah 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));
}
Kompilasi dan jalankan:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
keluaran pada mesin x86_64 saya:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Kami juga dapat menjalankan program di aarch64 dengan mode pengguna 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
dan menghasilkan keluaran yang sama persis, menunjukkan bahwa beberapa arch mengimplementasikan IEEE 754 dengan cermat.
Pada titik ini, jika Anda tidak terbiasa dengan struktur bilangan floating point IEEE 754, lihatlah: Apa itu bilangan floating point subnormal?
Dalam biner beberapa nilai di atas adalah:
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
Dari percobaan ini kami mengamati bahwa:
qNaN dan sNaN tampaknya hanya dibedakan dengan bit 22: 1 berarti tenang, dan 0 berarti pensinyalan
infinitas juga sangat mirip dengan eksponen == 0xFF, tetapi memiliki pecahan == 0.
Karena alasan ini, NaN harus menyetel bit 21 ke 1, jika tidak maka tidak mungkin membedakan sNaN dari tak terhingga positif!
nanf()
menghasilkan beberapa NaN berbeda, jadi harus ada beberapa kemungkinan pengkodean:
7fc00000
7fc00001
7fc00002
Karena nan0
sama dengan std::numeric_limits<float>::quiet_NaN()
, kami menyimpulkan bahwa mereka semua adalah NaN pendiam yang berbeda.
The C11 N1570 rancangan standar menegaskan bahwa nanf()
menghasilkan NaN tenang, karena nanf
ke depan untuk strtod
dan 7.22.1.3 "The strtod, strtof, dan fungsi strtold" mengatakan:
Urutan karakter NAN atau NAN (n-char-sequence opt) diinterpretasikan sebagai NaN yang tenang, jika didukung dalam tipe yang dikembalikan, seperti bagian urutan subjek yang tidak memiliki bentuk yang diharapkan; arti dari urutan n-char ditentukan oleh implementasi. 293)
Lihat juga:
Bagaimana qNaNs dan sNaNs terlihat di manual?
IEEE 754 2008 merekomendasikan bahwa (TODO wajib atau opsional?):
- apapun dengan eksponen == 0xFF dan pecahan! = 0 adalah NaN
- dan bahwa bit fraksi tertinggi membedakan qNaN dari sNaN
tetapi tampaknya tidak mengatakan bit mana yang lebih disukai untuk membedakan tak terhingga dari NaN.
6.2.1 "Pengodean NaN dalam format biner" mengatakan:
Subpasal ini selanjutnya menetapkan pengkodean NaN sebagai string bit jika merupakan hasil operasi. Ketika dikodekan, semua NaN memiliki bit tanda dan pola bit yang diperlukan untuk mengidentifikasi pengkodean sebagai NaN dan yang menentukan jenisnya (sNaN vs. qNaN). Bit yang tersisa, yang berada di bidang signifikansi tertinggal, menyandikan muatan, yang mungkin merupakan informasi diagnostik (lihat di atas). 34
Semua string bit NaN biner memiliki semua bit bidang eksponen bias E yang disetel ke 1 (lihat 3.4). String bit NaN yang tenang harus dikodekan dengan bit pertama (d1) dari bidang signifikansi T menjadi 1. String bit NaN pensinyalan harus dikodekan dengan bit pertama bidang signifikansi tertinggal menjadi 0. Jika bit pertama dari trailing bidang signifikan adalah 0, beberapa bit lain dari bidang signifikansi trailing harus bukan nol untuk membedakan NaN dari tak terhingga. Dalam pengkodean yang disukai yang baru saja dijelaskan, NaN pensinyalan harus diheningkan dengan mengatur d1 ke 1, meninggalkan bit T yang tersisa tidak berubah. Untuk format biner, payload dikodekan dalam p − 2 bit paling signifikan dari bidang signifikansi tertinggal
The Intel 64 dan IA-32 Arsitektur Software Developer Manual - Volume 1 Arsitektur Dasar - 253665-056US September 2015 4.8.3.4 "NaN" menegaskan bahwa x86 berikut IEEE 754 dengan membedakan NaN dan Snan oleh fraksi bit tertinggi:
Arsitektur IA-32 mendefinisikan dua kelas NaN: NaN tenang (QNaN) dan NaN pensinyalan (SNaN). QNaN adalah NaN dengan kumpulan bit fraksi paling signifikan. SNaN adalah NaN dengan bit fraksi paling signifikan yang jelas.
begitu pula dengan Manual Referensi Arsitektur ARM - ARMv8, untuk profil arsitektur ARMv8-A - DDI 0487C.a A1.4.3 "Format titik-mengambang presisi tunggal":
fraction != 0
: Nilainya adalah NaN, dan merupakan NaN tenang atau NaN pensinyalan. Dua jenis NaN dibedakan berdasarkan bit fraksi paling signifikannya, bit [22]:
bit[22] == 0
: NaN adalah NaN pensinyalan. Bit tanda dapat mengambil nilai apa pun, dan bit pecahan yang tersisa dapat mengambil nilai apa pun kecuali semua nol.
bit[22] == 1
: NaN adalah NaN yang tenang. Bit tanda dan bit pecahan yang tersisa dapat mengambil nilai apa pun.
Bagaimana qNanS dan sNaNs dibuat?
Satu perbedaan utama antara qNaNs dan sNaNs adalah:
- qNaN dihasilkan oleh operasi aritmatika bawaan (perangkat lunak atau perangkat keras) dengan nilai aneh
- sNaN tidak pernah dibuat oleh operasi built-in, ia hanya dapat ditambahkan secara eksplisit oleh programmer, misalnya dengan
std::numeric_limits::signaling_NaN
Saya tidak dapat menemukan kutipan IEEE 754 atau C11 yang jelas untuk itu, tetapi saya juga tidak dapat menemukan operasi bawaan yang menghasilkan sNaNs ;-)
Manual Intel dengan jelas menyatakan prinsip ini namun pada 4.8.3.4 "NaNs":
SNaN biasanya digunakan untuk menjebak atau memanggil penangan pengecualian. Mereka harus disisipkan oleh perangkat lunak; artinya, prosesor tidak pernah menghasilkan SNaN sebagai hasil dari operasi floating-point.
Ini dapat dilihat dari contoh kami di mana keduanya:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
menghasilkan bit yang sama persis dengan std::numeric_limits<float>::quiet_NaN()
.
Kedua operasi tersebut dikompilasi ke instruksi perakitan x86 tunggal yang menghasilkan qNaN langsung di perangkat keras (TODO konfirmasi dengan GDB).
Apa yang dilakukan qNaNs dan sNaNs secara berbeda?
Sekarang kita tahu seperti apa qNaN dan sNaNs, dan bagaimana memanipulasinya, akhirnya kita siap untuk mencoba dan membuat sNaN melakukan tugasnya dan meledakkan beberapa program!
Jadi tanpa basa-basi lagi:
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;
}
Kompilasi, jalankan dan dapatkan status keluar:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Keluaran:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Perhatikan bahwa perilaku ini hanya terjadi dengan -O0
di GCC 8.2: dengan -O3
, GCC menghitung sebelumnya dan mengoptimalkan semua operasi sNaN kami! Saya tidak yakin apakah ada cara patuh standar untuk mencegahnya.
Jadi kami menyimpulkan dari contoh ini bahwa:
snan + 1.0
penyebab FE_INVALID
, tetapi qnan + 1.0
tidak
Linux hanya menghasilkan sinyal jika diaktifkan dengan feenableexept
.
Ini adalah ekstensi glibc, saya tidak dapat menemukan cara untuk melakukannya dalam standar apa pun.
Ketika sinyal terjadi, itu karena perangkat keras CPU itu sendiri menimbulkan pengecualian, yang ditangani oleh kernel Linux dan menginformasikan aplikasi melalui sinyal.
Hasilnya adalah bash mencetak Floating point exception (core dumped)
, dan status keluarnya adalah 136
, yang sesuai dengan sinyal 136 - 128 == 8
, yang menurut:
man 7 signal
adalah SIGFPE
.
Perhatikan bahwa SIGFPE
adalah sinyal yang sama yang kita dapatkan jika kita mencoba membagi integer dengan 0:
int main() {
int i = 1 / 0;
}
meskipun untuk bilangan bulat:
- membagi apa pun dengan nol akan meningkatkan sinyal, karena tidak ada representasi tak terhingga dalam bilangan bulat
- sinyal itu terjadi secara default, tanpa perlu
feenableexcept
Bagaimana cara menangani SIGFPE?
Jika Anda baru saja membuat sebuah penangan yang mengembalikan secara normal, ini mengarah ke pengulangan tak terbatas, karena setelah penangan kembali, pembagian terjadi lagi! Ini dapat diverifikasi dengan GDB.
Satu-satunya cara adalah menggunakan setjmp
dan longjmp
melompat ke tempat lain seperti yang ditunjukkan di: C menangani sinyal SIGFPE dan melanjutkan eksekusi
Apa sajakah aplikasi sNaN di dunia nyata?
Sejujurnya, saya masih belum memahami kasus penggunaan yang sangat berguna untuk sNaN, hal ini telah ditanyakan di: Kegunaan pensinyalan NaN?
sNaNs terasa sangat tidak berguna karena kita dapat mendeteksi operasi awal yang tidak valid ( 0.0f/0.0f
) yang menghasilkan qNaN dengan feenableexcept
: tampaknya snan
hanya menimbulkan kesalahan untuk lebih banyak operasi yang qnan
tidak memunculkan, misalnya ( qnan + 1.0f
).
Misalnya:
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);
}
menyusun:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
kemudian:
./main.out
memberikan:
Floating point exception (core dumped)
dan:
./main.out 1
memberikan:
f1 -nan
f2 -nan
Lihat juga: Cara melacak NaN di C ++
Apa sajakah tanda-tanda itu dan bagaimana cara memanipulasinya?
Semuanya diimplementasikan di perangkat keras CPU.
Flag-flag tersebut hidup di beberapa register, dan begitu juga dengan bit yang mengatakan jika sebuah pengecualian / sinyal harus dinaikkan.
Register tersebut dapat diakses dari userland dari kebanyakan arch.
Bagian dari kode glibc 2.29 ini sebenarnya sangat mudah dimengerti!
Misalnya, fetestexcept
diimplementasikan untuk x86_86 di 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)
jadi kita langsung bisa melihat bahwa petunjuk pemakaiannya adalah stmxcsr
kependekan dari "Store MXCSR Register State".
Dan feenableexcept
diimplementasikan di 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;
}
Apa yang dikatakan standar C tentang qNaN vs sNaN?
The C11 N1570 rancangan standar tegas mengatakan bahwa standar tidak membedakan antara mereka pada F.2.1 "infinities, nol ditandatangani, dan NaN":
1 Spesifikasi ini tidak menentukan perilaku pensinyalan NaN. Ini umumnya menggunakan istilah NaN untuk menunjukkan NaN tenang. Makro NAN dan INFINITY serta fungsi nan di<math.h>
memberikan sebutan untuk IEC 60559 NaN dan infinitas.
Diuji di Ubuntu 18.10, GCC 8.2. GitHub upstream: