Trong thực nghiệm, qNaN và sNaN trông như thế nào?
Đầu tiên chúng ta hãy học cách xác định xem chúng ta có sNaN hay qNaN.
Tôi sẽ sử dụng C ++ trong câu trả lời này thay vì C vì nó mang lại sự thuận tiện std::numeric_limits::quiet_NaN
và std::numeric_limits::signaling_NaN
điều mà tôi không thể tìm thấy trong C một cách thuận tiện.
Tuy nhiên, tôi không thể tìm thấy một hàm để phân loại nếu NaN là sNaN hay qNaN, vì vậy chúng ta chỉ cần in ra các byte thô 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));
}
Biên dịch và chạy:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
đầu ra trên máy x86_64 của tôi:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Chúng tôi cũng có thể thực thi chương trình trên aarch64 với chế độ người dùng 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
và điều đó tạo ra cùng một đầu ra, cho thấy rằng nhiều vòm thực hiện chặt chẽ IEEE 754.
Tại thời điểm này, nếu bạn chưa quen với cấu trúc của số dấu phẩy động IEEE 754, hãy xem: Số dấu phẩy động siêu thường là gì?
Trong hệ nhị phân, một số giá trị ở trên là:
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
Từ thí nghiệm này, chúng tôi nhận thấy rằng:
qNaN và sNaN dường như chỉ được phân biệt bởi bit 22: 1 nghĩa là yên tĩnh và 0 có nghĩa là báo hiệu
số nguyên cũng khá tương tự với số mũ == 0xFF, nhưng chúng có phân số == 0.
Vì lý do này, các NaN phải đặt bit 21 thành 1, nếu không sẽ không thể phân biệt sNaN với dương vô cực!
nanf()
tạo ra một số NaN khác nhau, vì vậy phải có nhiều mã hóa có thể có:
7fc00000
7fc00001
7fc00002
Vì nan0
giống nhau std::numeric_limits<float>::quiet_NaN()
nên chúng tôi suy ra rằng chúng đều là những NaN yên tĩnh khác nhau.
Bản nháp tiêu chuẩn C11 N1570 xác nhận rằng nanf()
tạo ra các NaN yên tĩnh, vì nanf
chuyển tiếp đến strtod
và 7.22.1.3 "Các hàm strtod, strtof và strtold" cho biết:
Chuỗi ký tự NAN hoặc NAN (n-char-serial opt) được hiểu là NaN yên tĩnh, nếu được hỗ trợ trong kiểu trả về, khác giống như phần chuỗi chủ đề không có dạng mong đợi; ý nghĩa của dãy n-char được xác định bằng cách triển khai. 293)
Xem thêm:
QNaNs và sNaNs trông như thế nào trong sách hướng dẫn?
IEEE 754 2008 khuyến nghị rằng (CẦN LÀM bắt buộc hay tùy chọn?):
- bất kỳ thứ gì có số mũ == 0xFF và phân số! = 0 là một NaN
- và bit phân số cao nhất phân biệt qNaN với sNaN
nhưng nó dường như không nói rằng bit nào được ưu tiên để phân biệt vô cực với NaN.
6.2.1 "Mã hóa NaN ở định dạng nhị phân" cho biết:
Điều khoản phụ này chỉ rõ thêm các mã hóa của NaN dưới dạng chuỗi bit khi chúng là kết quả của các phép toán. Khi được mã hóa, tất cả các NaN đều có một bit dấu và một mẫu bit cần thiết để xác định mã hóa là NaN và xác định loại của nó (sNaN so với qNaN). Các bit còn lại, nằm trong trường ý nghĩa và dấu sau, mã hóa tải trọng, có thể là thông tin chẩn đoán (xem ở trên). 34
Tất cả các chuỗi bit NaN nhị phân có tất cả các bit của trường số mũ thiên vị E được đặt thành 1 (xem 3.4). Một chuỗi bit NaN yên lặng nên được mã hóa với bit đầu tiên (d1) của trường dấu và T là 1. Một chuỗi bit NaN báo hiệu phải được mã hóa với bit đầu tiên của trường dấu và bằng 0. Nếu bit đầu tiên của trường có dấu và bằng 0, một số bit khác của trường dấu và phải khác 0 để phân biệt NaN với vô cùng. Trong cách mã hóa ưu tiên vừa mô tả, NaN báo hiệu sẽ được làm yên bằng cách đặt d1 thành 1, giữ nguyên các bit còn lại của T không đổi. Đối với các định dạng nhị phân, trọng tải được mã hóa trong p-2 bit có ý nghĩa nhỏ nhất của trường dấu và
Các tay Intel 64 và IA-32 Kiến trúc phát triển phần mềm của - Tập 1 Kiến trúc cơ bản - 253665-056US tháng 9 năm 2015 4.8.3.4 "Nans" xác nhận rằng x86 sau IEEE 754 bằng cách phân biệt NaN và SNaN bởi các bit phần cao nhất:
Kiến trúc IA-32 xác định hai lớp NaN: NaN yên tĩnh (QNaN) và NaN báo hiệu (SNaN). QNaN là NaN với bit phần quan trọng nhất được thiết lập. SNaN là NaN với bit phần quan trọng nhất rõ ràng.
và Hướng dẫn Tham khảo Kiến trúc ARM - ARMv8, dành cho cấu hình kiến trúc ARMv8-A - DDI 0487C.a A1.4.3 "Định dạng dấu chấm động chính xác đơn":
fraction != 0
: Giá trị là NaN và là NaN yên tĩnh hoặc NaN báo hiệu. Hai loại NaN được phân biệt bằng bit phần quan trọng nhất của chúng, bit [22]:
bit[22] == 0
: NaN là một NaN báo hiệu. Bit dấu có thể nhận bất kỳ giá trị nào và các bit phân số còn lại có thể nhận bất kỳ giá trị nào ngoại trừ tất cả các số không.
bit[22] == 1
: NaN là một NaN trầm lặng. Bit dấu và các bit phân số còn lại có thể nhận bất kỳ giá trị nào.
QNanS và sNaNs được tạo ra như thế nào?
Một điểm khác biệt chính giữa qNaNs và sNaNs là:
- qNaN được tạo ra bởi các phép toán số học (phần mềm hoặc phần cứng) tích hợp sẵn thông thường với các giá trị kỳ lạ
- sNaN không bao giờ được tạo bởi các hoạt động tích hợp, nó chỉ có thể được thêm vào bởi các lập trình viên một cách rõ ràng, ví dụ: với
std::numeric_limits::signaling_NaN
Tôi không thể tìm thấy dấu ngoặc kép IEEE 754 hoặc C11 rõ ràng cho điều đó, nhưng tôi cũng không thể tìm thấy bất kỳ hoạt động tích hợp nào tạo sNaNs ;-)
Tuy nhiên, hướng dẫn sử dụng Intel nêu rõ nguyên tắc này tại 4.8.3.4 "NaNs":
SNaN thường được sử dụng để bẫy hoặc gọi một trình xử lý ngoại lệ. Chúng phải được chèn bằng phần mềm; nghĩa là bộ xử lý không bao giờ tạo SNaN do hoạt động dấu phẩy động.
Điều này có thể được nhìn thấy từ ví dụ của chúng tôi, nơi cả hai:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
tạo ra chính xác các bit giống như std::numeric_limits<float>::quiet_NaN()
.
Cả hai hoạt động đó đều biên dịch thành một lệnh hợp ngữ x86 duy nhất tạo qNaN trực tiếp trong phần cứng (TODO xác nhận với GDB).
QNaNs và sNaNs làm gì khác nhau?
Bây giờ chúng ta đã biết qNaN và sNaN trông như thế nào và cách sử dụng chúng, chúng tôi cuối cùng đã sẵn sàng để thử và làm cho sNaN thực hiện công việc của họ và làm nổ tung một số chương trình!
Vì vậy, không cần quảng cáo thêm:
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;
}
Biên dịch, chạy và nhận trạng thái thoát:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Đầu ra:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Lưu ý rằng hành vi này chỉ xảy ra với -O0
GCC 8.2: với -O3
, GCC tính toán trước và tối ưu hóa tất cả các hoạt động sNaN của chúng tôi! Tôi không chắc liệu có cách tuân thủ tiêu chuẩn nào để ngăn chặn điều đó hay không.
Vì vậy, chúng tôi suy ra từ ví dụ này rằng:
snan + 1.0
nguyên nhân FE_INVALID
, nhưng qnan + 1.0
không
Linux chỉ tạo ra một tín hiệu nếu nó được bật với feenableexept
.
Đây là một phần mở rộng glibc, tôi không thể tìm thấy bất kỳ cách nào để làm điều đó trong bất kỳ tiêu chuẩn nào.
Khi tín hiệu xảy ra, đó là do bản thân phần cứng CPU tạo ra một ngoại lệ, mà nhân Linux xử lý và thông báo cho ứng dụng thông qua tín hiệu.
Kết quả là bản in bash Floating point exception (core dumped)
và trạng thái thoát là 136
, tương ứng với tín hiệu 136 - 128 == 8
, theo:
man 7 signal
là SIGFPE
.
Lưu ý rằng đó SIGFPE
là tín hiệu tương tự mà chúng ta nhận được nếu chúng ta cố gắng chia một số nguyên cho 0:
int main() {
int i = 1 / 0;
}
mặc dù đối với số nguyên:
- chia bất kỳ thứ gì cho không sẽ làm tăng tín hiệu, vì không có biểu diễn vô cực trong số nguyên
- tín hiệu mà nó xảy ra theo mặc định, không cần
feenableexcept
Làm thế nào để xử lý SIGFPE?
Nếu bạn chỉ tạo một trình xử lý trả về bình thường, nó sẽ dẫn đến một vòng lặp vô hạn, vì sau khi trình xử lý trả về, phép chia lại xảy ra! Điều này có thể được xác minh với GDB.
Cách duy nhất là sử dụng setjmp
và longjmp
chuyển sang một nơi khác như được hiển thị tại: C xử lý tín hiệu SIGFPE và tiếp tục thực hiện
Một số ứng dụng trong thế giới thực của sNaN là gì?
Thành thật mà nói, tôi vẫn chưa hiểu một trường hợp sử dụng siêu hữu ích cho sNaN, điều này đã được hỏi tại: Tính hữu ích của việc báo hiệu NaN?
sNaNs cảm thấy đặc biệt vô dụng vì chúng tôi có thể phát hiện các hoạt động không hợp lệ ban đầu ( 0.0f/0.0f
) tạo ra qNaN với feenableexcept
: nó dường như snan
chỉ làm tăng lỗi cho nhiều hoạt động hơn mà qnan
không phát sinh, ví dụ: ( qnan + 1.0f
).
Ví dụ:
C chính
#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);
}
biên dịch:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
sau đó:
./main.out
cho:
Floating point exception (core dumped)
và:
./main.out 1
cho:
f1 -nan
f2 -nan
Xem thêm: Cách theo dõi NaN trong C ++
Cờ tín hiệu là gì và chúng được thao tác như thế nào?
Mọi thứ đều được thực hiện trong phần cứng CPU.
Các cờ nằm trong một số thanh ghi, và bit cho biết liệu một ngoại lệ / tín hiệu có nên được nâng lên hay không.
Những đăng ký đó có thể truy cập từ userland từ hầu hết các cổng.
Phần này của mã glibc 2.29 thực sự rất dễ hiểu!
Ví dụ: fetestexcept
được triển khai cho x86_86 tại 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)
vì vậy chúng tôi ngay lập tức thấy rằng hướng dẫn sử dụng là stmxcsr
viết tắt của "Lưu trữ trạng thái đăng ký MXCSR".
Và feenableexcept
được triển khai tại 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;
}
Tiêu chuẩn C nói gì về qNaN và sNaN?
Các C11 N1570 tiêu chuẩn dự thảo rõ ràng nói rằng tiêu chuẩn không phân biệt giữa họ tại F.2.1 "infinities, số không có chữ ký, và Nans":
1 Đặc điểm kỹ thuật này không xác định hành vi báo hiệu các NaN. Nó thường sử dụng thuật ngữ NaN để biểu thị các NaN yên tĩnh. Các macro NAN và INFINITY và các hàm nan trong <math.h>
cung cấp các ký hiệu cho IEC 60559 NaN và các vô hạn.
Đã thử nghiệm trong Ubuntu 18.10, GCC 8.2. GitHub ngược dòng: