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
และทำให้เกิดเอาต์พุตที่เหมือนกันโดยบอกว่าหลาย arch ใช้ 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 หมายถึงการส่งสัญญาณ
infinities ก็ค่อนข้างใกล้เคียงกับเลขชี้กำลัง == 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 "การ strtod, strtof และฟังก์ชั่น strtold" พูดว่า:
ลำดับอักขระ NAN หรือ NAN (ตัวเลือกลำดับ n-char) ถูกตีความว่าเป็น NaN แบบเงียบหากได้รับการสนับสนุนในประเภทการส่งคืนอื่นเช่นส่วนลำดับหัวเรื่องที่ไม่มีรูปแบบที่คาดไว้ ความหมายของลำดับ n-char คือการกำหนดการนำไปใช้งาน 293)
ดูสิ่งนี้ด้วย:
qNaNs และ sNaNs มีลักษณะอย่างไรในคู่มือ?
IEEE 754 2008แนะนำว่า (จำเป็นต้องทำหรือไม่บังคับ?):
- อะไรก็ตามที่มีเลขชี้กำลัง == 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 "แก่นแก้ว" ยืนยันว่า x86 ดังนี้ IEEE 754 โดยแยกความแตกต่างน่านและสนานโดยบิตส่วนสูงสุด:
สถาปัตยกรรม IA-32 กำหนด NaN สองคลาส ได้แก่ NaN ที่เงียบ (QNaNs) และ NaN การส่งสัญญาณ (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 และ sNaNs สร้างขึ้นได้อย่างไร
ความแตกต่างที่สำคัญอย่างหนึ่งระหว่าง 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 โดยตรงในฮาร์ดแวร์ (สิ่งที่ต้องทำยืนยันกับ GDB)
qNaNs และ sNaNs ต่างกันอย่างไร
ตอนนี้เรารู้แล้วว่า qNaN และ sNaN มีลักษณะอย่างไรและจะจัดการอย่างไรในที่สุดเราก็พร้อมที่จะลองและทำให้ 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 ฉันไม่สามารถหาวิธีทำได้ในมาตรฐานใด ๆ
เมื่อสัญญาณเกิดขึ้นเนื่องจากฮาร์ดแวร์ของ CPU ทำให้เกิดข้อยกเว้นซึ่งเคอร์เนล 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?
sNaN รู้สึกไร้ประโยชน์อย่างยิ่งเพราะเราสามารถตรวจพบการดำเนินการที่ไม่ถูกต้องเริ่มต้น ( 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 ++
ธงสัญญาณคืออะไรและมีการจัดการอย่างไร?
ทุกอย่างถูกนำไปใช้ในฮาร์ดแวร์ของ CPU
แฟล็กอาศัยอยู่ในรีจิสเตอร์บางตัวและบิตที่ระบุว่าควรเพิ่มข้อยกเว้น / สัญญาณ
การลงทะเบียนเหล่านั้นสามารถเข้าถึงได้จาก userlandจาก archs ส่วนใหญ่
โค้ด 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
ย่อมาจาก "Store MXCSR Register State"
และ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 vs sNaN อย่างไร
ร่างมาตรฐาน C11 N1570อย่างชัดเจนกล่าวว่ามาตรฐานไม่แตกต่างระหว่างพวกเขาที่ F.2.1 "อนันต์, ศูนย์มีเครื่องหมายและแก่นแก้ว":
1 ข้อกำหนดนี้ไม่ได้กำหนดลักษณะการทำงานของการส่งสัญญาณ NaN โดยทั่วไปใช้คำว่า NaN เพื่อแสดงถึง NaN ที่เงียบสงบ มาโคร NAN และ INFINITY และฟังก์ชัน nan ในการ<math.h>
กำหนดสำหรับ IEC 60559 NaNs และ infinities
ทดสอบใน Ubuntu 18.10, GCC 8.2 GitHub อัปสตรีม: