QNaN'ler ve sNaN'ler deneysel olarak nasıl görünür?
Öncelikle bir sNaN veya qNaN'ye sahip olduğumuzu nasıl belirleyeceğimizi öğrenelim.
Bu cevapta C yerine C ++ kullanacağım çünkü uygun olanı sunuyor std::numeric_limits::quiet_NaN
ve std::numeric_limits::signaling_NaN
C de rahatlıkla bulamıyorum.
Bununla birlikte, bir NaN'nin sNaN veya qNaN olup olmadığını sınıflandıracak bir işlev bulamadım, bu yüzden sadece NaN ham baytlarını yazdıralım:
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));
}
Derleyin ve çalıştırın:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
x86_64 makinemdeki çıktı:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Programı aarch64 üzerinde QEMU kullanıcı modu ile de çalıştırabiliriz:
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
ve bu tam olarak aynı çıktıyı üretir, bu da birden fazla kemerin IEEE 754'ü yakından uyguladığını gösterir.
Bu noktada, IEEE 754 kayan noktalı sayıların yapısına aşina değilseniz, şuna bir göz atın: Normalden düşük kayan nokta sayısı nedir?
İkili olarak yukarıdaki değerlerden bazıları şunlardır:
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
Bu deneyden şunu gözlemliyoruz:
qNaN ve sNaN sadece bit 22 ile ayırt edilmiş gibi görünmektedir: 1 sessiz anlamına gelir ve 0 sinyalleşme anlamına gelir
sonsuzluklar da üs == 0xFF ile oldukça benzerdir, ancak kesir == 0'a sahiptirler.
Bu nedenle, NaN'ler bit 21'i 1'e ayarlamalıdır, aksi takdirde sNaN'yi pozitif sonsuzdan ayırmak mümkün olmazdı!
nanf()
birkaç farklı NaN üretir, bu nedenle birden fazla olası kodlama olması gerekir:
7fc00000
7fc00001
7fc00002
Yana nan0
aynıdırstd::numeric_limits<float>::quiet_NaN()
, biz hepsi farklı sessiz NaN'ler olduklarını anlamak.
C11 N1570 standart taslak onaylar nanf()
, çünkü sessiz NaN'ler üretir nanf
yönlendirme yaptığı strtod
"strtod, strtof ve strtold fonksiyonlar" ve 7.22.1.3 diyor ki:
Bir karakter dizisi NAN veya NAN (n-karakter dizisi tercihi), dönüş türünde destekleniyorsa sessiz bir NaN olarak yorumlanır, aksi takdirde beklenen biçime sahip olmayan bir konu dizisi parçası gibi; n-char dizisinin anlamı uygulama tanımlıdır. 293)
Ayrıca bakınız:
QNaN'ler ve sNaN'ler kılavuzlarda nasıl görünüyor?
IEEE 754 2008 şunları önerir (TODO zorunlu mu yoksa isteğe bağlı mı?):
- üslü == 0xFF ve kesirli! = 0 olan herhangi bir şey bir NaN'dir
- ve en yüksek kesir bitinin qNaN'yi sNaN'den ayırdığını
ancak sonsuzluğu NaN'den ayırmak için hangi bitin tercih edildiğini söylemiyor gibi görünüyor.
6.2.1 "İkili biçimlerde NaN kodlamaları" diyor ki:
Bu alt bölüm ayrıca, işlemlerin sonucu olduklarında NaN'lerin kodlamalarını bit dizileri olarak belirtir. Kodlandığında, tüm NaN'ler bir işaret bitine ve kodlamayı bir NaN olarak tanımlamak için gerekli olan ve türünü (sNaN'e karşı qNaN) belirleyen bir bit modeline sahiptir. Sondaki anlam alanında kalan bitler, tanılama bilgisi olabilen yükü kodlar (yukarıya bakın). 34
Tüm ikili NaN bit dizgileri, 1'e ayarlanmış önyargılı üs alanının tüm bitlerine sahiptir (bkz. 3.4). Sessiz bir NaN bit dizgisi, sondaki anlam ve T alanının ilk biti (d1) 1 olacak şekilde kodlanmalıdır. Bir sinyalleşme NaN bit dizgisi, sondaki anlam ve alanın ilk biti 0 olacak şekilde kodlanmalıdır. sondaki anlamlılık alanı 0'dır, sondaki anlamlılık alanının diğer bir biti, NaN'yi sonsuzdan ayırmak için sıfırdan farklı olmalıdır. Az önce tarif edilen tercih edilen kodlamada, bir sinyalleme NaN'si, d1'i 1'e ayarlayarak susturulacak ve T'nin kalan bitleri değişmeden bırakılacaktır. İkili formatlar için, yük, sondaki anlamlılık ve önem alanının p − 2 en az anlamlı bitinde kodlanır
Intel 64 ve IA-32 Mimarileri Yazılım Geliştirici Kılavuzu - Cilt 1 Temel Mimarlık - 253665-056US Eylül 2015 x86 en yüksek fraksiyon azar NaN ve snan ayırt ederek IEEE 754 aşağıdaki 4.8.3.4 "NaN'ler" dogruluyor:
IA-32 mimarisi iki NaN sınıfını tanımlar: sessiz NaN'ler (QNaN'ler) ve sinyal veren NaN'ler (SNaN'ler). Bir QNaN, en önemli kesir biti setine sahip bir NaN'dir; SNaN, en önemli kesir biti berraklığına sahip bir NaN'dir.
ve ARMv8-A mimari profili - DDI 0487C.a A1.4.3 "Tek duyarlıklı kayan nokta biçimi" için ARM Mimarisi Referans Kılavuzu - ARMv8 :
fraction != 0
: Değer bir NaN'dir ve ya sessiz bir NaN ya da bir sinyal NaN'dir. İki tür NaN, en önemli kesir biti olan bit [22] ile ayırt edilir:
bit[22] == 0
: NaN, sinyal veren bir NaN'dir. İşaret biti herhangi bir değeri alabilir ve kalan kesir bitleri tümü sıfırlar dışında herhangi bir değeri alabilir.
bit[22] == 1
: NaN sessiz bir NaN'dir. İşaret biti ve kalan kesir bitleri herhangi bir değeri alabilir.
QNanS ve sNaN'ler nasıl oluşturulur?
QNaN'ler ve sNaN'ler arasındaki önemli bir fark şudur:
- qNaN, tuhaf değerlere sahip normal yerleşik (yazılım veya donanım) aritmetik işlemlerle oluşturulur
- sNaN asla yerleşik işlemler tarafından oluşturulmaz, yalnızca programcılar tarafından açıkça eklenebilir, örn.
std::numeric_limits::signaling_NaN
Bunun için net IEEE 754 veya C11 teklifleri bulamadım, ancak sNaN'ler oluşturan herhangi bir yerleşik işlem bulamıyorum ;-)
Intel kılavuzu bu prensibi açıkça belirtir ancak 4.8.3.4 "NaNs":
SNaN'ler genellikle bir istisna işleyiciyi yakalamak veya çağırmak için kullanılır. Yazılım tarafından eklenmeleri gerekir; yani işlemci, kayan nokta işleminin sonucu olarak hiçbir zaman bir SNaN üretmez.
Bu, her ikisinin de bulunduğu örneğimizden görülebilir:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
tamamen aynı bitleri üretin std::numeric_limits<float>::quiet_NaN()
.
Bu işlemlerin her ikisi de, qNaN'yi doğrudan donanımda üreten tek bir x86 derleme talimatına derlenir (TODO, GDB ile onaylar).
QNaN'ler ve sNaN'ler neyi farklı yapar?
Artık qNaN'ların ve sNaN'lerin neye benzediğini ve onları nasıl manipüle edeceğimizi bildiğimize göre, nihayet sNaN'ların kendi işlerini yapmasını sağlamaya ve bazı programları havaya uçurmaya hazırız!
Yani daha fazla uzatmadan:
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;
}
Derleyin, çalıştırın ve çıkış durumunu alın:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Çıktı:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Bu davranışın yalnızca -O0
GCC 8.2'de gerçekleştiğini unutmayın:-O3
GCC, tüm sNaN işlemlerimizi önceden hesaplar ve optimize eder! Bunu önlemenin standart ve uyumlu bir yolu olup olmadığından emin değilim.
Bu örnekten şunu çıkardık:
snan + 1.0
neden olur FE_INVALID
ama qnan + 1.0
değil
Linux, yalnızca etkinleştirilmişse bir sinyal üretir feenableexept
.
Bu bir glibc uzantısı, bunu herhangi bir standartta yapmanın bir yolunu bulamadım.
Sinyal gerçekleştiğinde, bunun nedeni CPU donanımının kendisinin bir istisna oluşturmasıdır, Linux çekirdeği bunu sinyal yoluyla işleyip uygulamayı bilgilendirir.
Sonuç bu deneme baskıları Floating point exception (core dumped)
ve çıkış durumu 136
olan, karşılık gelen sinyal 136 - 128 == 8
göre:
man 7 signal
olduğunu SIGFPE
.
Not SIGFPE
biz 0'a tarafından bir tamsayı bölmek çalışırsanız aldığımız aynı sinyaldir:
int main() {
int i = 1 / 0;
}
tamsayılar için olsa da:
- herhangi bir şeyi sıfıra bölmek sinyali yükseltir, çünkü tamsayılarda sonsuzluk gösterimi yoktur
- buna gerek kalmadan varsayılan olarak meydana gelen sinyal
feenableexcept
SIGFPE ile nasıl başa çıkılır?
Normal olarak dönen bir işleyici yaratırsanız, sonsuz bir döngüye yol açar, çünkü işleyici geri döndükten sonra bölme yeniden gerçekleşir! Bu GDB ile doğrulanabilir.
Tek yol kullanmak setjmp
ve longjmp
başka bir yere atlamaktır : C tutma sinyali SIGFPE ve yürütmeye devam edin
SNaN'lerin bazı gerçek dünya uygulamaları nelerdir?
Dürüst olmak gerekirse, sNaN'ler için süper kullanışlı bir kullanım durumunu hala anlamadım, bu soruldu: NaN sinyalinin faydası ?
sNaN'ler özellikle işe yaramaz çünkü 0.0f/0.0f
qNaN'leri üreten ilk geçersiz işlemleri ( ) tespit edebiliyoruz feenableexcept
: Görünüşe göre snan
daha fazla işlem için artmayan hatalar yükseliyor qnan
, örneğin (qnan + 1.0f
).
Örneğin:
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);
}
derleyin:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
sonra:
./main.out
verir:
Floating point exception (core dumped)
ve:
./main.out 1
verir:
f1 -nan
f2 -nan
Ayrıca bkz: C ++ 'da bir NaN izleme
İşaret bayrakları nelerdir ve nasıl manipüle edilir?
Her şey CPU donanımında uygulanmaktadır.
Bayraklar bir kayıtta yaşar ve bir istisna / sinyalin yükseltilmesi gerektiğini söyleyen bit de öyle.
Bu kayıtlara çoğu kemerden kullanıcı alanından erişilebilir .
Glibc 2.29 kodunun bu kısmı aslında anlaşılması çok kolay!
Örneğin, fetestexcept
x86_86 için sysdeps / x86_64 / fpu / ftestexcept.c'de uygulanır :
#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)
bu nedenle, talimatların kullanımının stmxcsr
"MXCSR Kayıt Durumunu Kaydet" anlamına .
Ve feenableexcept
de uygulanmaktadır 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 standardı qNaN ve sNaN hakkında ne diyor?
C11 N1570 standart taslağı açıkça standart F.2.1'e "Sonsuzluklar, imzalı sıfır ve NaN'ler" nde aralarında ayrım yapmaz diyor:
1 Bu belirtim, NaN'lerin sinyalleşme davranışını tanımlamaz. Sessiz NaN'leri belirtmek için genellikle NaN terimini kullanır. NAN ve INFINITY makroları ve içindeki nan fonksiyonları, <math.h>
IEC 60559 NaN'ler ve sonsuzluklar için atamalar sağlar.
Ubuntu 18.10, GCC 8.2'de test edilmiştir. GitHub yukarı akışları: