Sessiz NaN ile sinyal veren NaN arasındaki fark nedir?


108

Kayan nokta hakkında bir şeyler okudum ve NaN'nin işlemlerden kaynaklanabileceğini anladım. Ama bunların tam olarak ne kavramlar olduğunu anlayamıyorum. Onların arasındaki fark ne?

C ++ programlama sırasında hangisi üretilebilir? Bir programcı olarak sNaN'ye neden olan bir program yazabilir miyim?

Yanıtlar:


73

Bir işlem sessiz bir NaN ile sonuçlandığında, program sonucu kontrol edip bir NaN görene kadar herhangi bir şeyin olağandışı olduğuna dair bir gösterge yoktur. Yani, yazılımda kayan nokta uygulanıyorsa hesaplama, kayan nokta biriminden (FPU) veya kitaplıktan herhangi bir sinyal olmadan devam eder. Bir sinyal NaN, genellikle FPU'dan istisna şeklinde bir sinyal üretecektir. İstisnanın atılıp atılmayacağı FPU'nun durumuna bağlıdır.

C ++ 11 , kayan noktalı ortam üzerine birkaç dil denetimi ekler ve NaN'ler oluşturmak ve bunları test etmek için standartlaştırılmış yollar sağlar . Ancak, kontrollerin uygulanıp uygulanmadığı iyi bir şekilde standartlaştırılmamıştır ve kayan nokta istisnaları tipik olarak standart C ++ istisnaları ile aynı şekilde yakalanmaz.

POSIX / Unix sistemlerinde, kayan nokta istisnaları tipik olarak SIGFPE için bir işleyici kullanılarak yakalanır .


37
Buna ek olarak: Genellikle, bir NaN (sNaN) sinyalizasyonunun amacı hata ayıklamak içindir. Örneğin, kayan nokta nesneleri sNaN olarak başlatılabilir. Daha sonra, program kullanmadan önce bunlardan birinde bir değerde başarısız olursa, program sNaN'yi aritmetik bir işlemde kullandığında bir istisna meydana gelir. Bir program yanlışlıkla sNaN üretmez; hiçbir normal işlem sNaN üretmez. Herhangi bir aritmetiğin sonucu olarak değil, yalnızca bir sinyal NaN'ye sahip olmak amacıyla özel olarak oluşturulurlar.
Eric Postpischil

19
Buna karşılık, NaN'ler daha normal programlama içindir. Sayısal bir sonuç olmadığında normal işlemlerle üretilebilirler (örneğin, sonucun gerçek olması gerektiğinde negatif bir sayının karekökünü almak). Amaçları genellikle aritmetiğin normal bir şekilde ilerlemesine izin vermektir. Örneğin, bazıları normal olarak ele alınamayan özel durumları temsil eden çok sayıda sayı dizisine sahip olabilirsiniz. Bu diziyi işlemek için karmaşık bir işlev çağırabilirsiniz ve dizi üzerinde NaN'leri yok sayarak olağan aritmetik ile çalışabilir. Bittikten sonra, daha fazla iş için özel davaları ayırırsınız.
Eric Postpischil

@wrdieter Teşekkürler, o zaman sadece najor farkı muafiyet üretiyor veya üretmiyor.
JalalJaberi

@EricPostpischil İkinci soruya gösterdiğiniz ilgiden dolayı teşekkür ederiz.
JalalJaberi

@JalalJaberi evet, ana fark istisnadır
wrdieter

43

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_NaNve std::numeric_limits::signaling_NaNC 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 nan0aynı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 nanfyö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 -O0GCC 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.0neden olur FE_INVALIDama qnan + 1.0değ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 136olan, karşılık gelen sinyal 136 - 128 == 8göre:

man 7 signal

olduğunu SIGFPE.

Not SIGFPEbiz 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 setjmpve longjmpbaş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.0fqNaN'leri üreten ilk geçersiz işlemleri ( ) tespit edebiliyoruz feenableexcept: Görünüşe göre snandaha 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, fetestexceptx86_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 feenableexceptde 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ı:


en.wikipedia.org/wiki/IEEE_754#Interchange_formats , IEEE-754'ün yalnızca NaN'leri işaretlemek için 0'ın, bir NaN'yi sonsuz yapma riski olmadan susturmaya izin vermek için iyi bir uygulama seçeneği olduğunu öne sürdüğüne işaret eder (anlamlı = 0). Görünüşe göre bu standartlaştırılmamış, ancak x86'nın yaptığı bu. (Ve qNaN ve sNaN'yi belirleyen anlamın MSB'si olduğu gerçeği standartlaştırılmıştır). en.wikipedia.org/wiki/Single-precision_floating-point_format , x86 ve ARM'ün aynı olduğunu söylüyor, ancak PA-RISC bunun tersini yaptı.
Peter Cordes

@PeterCordes evet, IEEE 754 20at'ta "gerekir" == "gerekir" veya "tercih edilir" ne olduğundan emin değilim "Bir sinyalleme NaN bit dizgisi, sondaki anlamlı ve 0" değerinin ilk bitiyle kodlanmalıdır.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

re: ancak sonsuzluğu NaN'den ayırmak için hangi bitin kullanılması gerektiğini belirtmiyor gibi görünüyor. Beklediğiniz gibi, standardın sNaN'yi sonsuzdan ayırmak için ayarlamayı önerdiği belirli bir bit olmasını beklediğiniz gibi yazdınız. IDK neden böyle bir bit olmasını beklediğiniz; sıfır olmayan herhangi bir seçim iyidir. Daha sonra sNaN'in nereden geldiğini tanımlayan bir şey seçin. IDK, kulağa garip bir ifade gibi geliyor ve bunu okurken ilk izlenimim, web sayfasının kodlamada inf'yi NaN'den ayıran şeyi tanımlamadığını söylüyordunuz (tamamen sıfır anlam).
Peter Cordes

2008'den önce IEEE 754, hangisinin sinyalleşme / sessiz bit (bit 22) olduğunu, ancak hangi değerin ne olduğunu belirtmediğini söyledi. Çoğu işlemci 1 = sessizde yakınsamıştı, bu nedenle bu, 2008 sürümünde standardın bir parçası haline getirildi. Aynı seçimi uyumsuz yapan eski uygulamaları yapmaktan kaçınmak için "gerekir" yerine "gerekir" diyor. Genel olarak, standart bir şekilde "olmalı", "uymamak için çok zorlayıcı (ve tercihen iyi belgelenmiş) nedenleriniz yoksa" gerekir ".
John Cowan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.