Apa perbedaan antara NaN tenang dan NaN pensinyalan?

98

Saya telah membaca tentang floating-point dan saya memahami bahwa NaN dapat dihasilkan dari operasi. Tapi saya tidak bisa mengerti apa sebenarnya konsep ini. Apa perbedaan di antara keduanya?

Manakah yang dapat diproduksi selama pemrograman C ++? Sebagai seorang programmer, dapatkah saya menulis program yang menyebabkan sNaN?

JalalJaberi
sumber

Jawaban:

68

Ketika sebuah operasi menghasilkan NaN yang tenang, tidak ada indikasi bahwa ada sesuatu yang tidak biasa hingga program memeriksa hasilnya dan melihat NaN. Artinya, komputasi berlanjut tanpa sinyal apa pun dari floating point unit (FPU) atau pustaka jika floating-point diimplementasikan dalam perangkat lunak. NaN pensinyalan akan menghasilkan sinyal, biasanya dalam bentuk pengecualian dari FPU. Apakah pengecualian dilempar tergantung pada status FPU.

C ++ 11 menambahkan beberapa kontrol bahasa pada lingkungan floating-point dan menyediakan cara standar untuk membuat dan menguji NaN . Namun, apakah kontrol diimplementasikan tidak terstandarisasi dengan baik dan pengecualian floating-point biasanya tidak ditangkap dengan cara yang sama seperti pengecualian C ++ standar.

Dalam sistem POSIX / Unix, pengecualian floating point biasanya ditangkap dengan menggunakan handler untuk SIGFPE .

wrdieter
sumber
34
Menambahkan ini: Umumnya, tujuan dari pensinyalan NaN (sNaN) adalah untuk debugging. Misalnya, objek floating-point mungkin diinisialisasi ke sNaN. Kemudian, jika program gagal memberikan salah satu nilai sebelum digunakan, pengecualian akan terjadi saat program menggunakan sNaN dalam operasi aritmatika. Sebuah program tidak akan menghasilkan sNaN secara tidak sengaja; tidak ada operasi normal yang menghasilkan sNaN. Mereka hanya dibuat khusus untuk tujuan memiliki NaN pensinyalan, bukan sebagai hasil dari aritmatika apa pun.
Eric Postpischil
18
Sebaliknya, NaN untuk pemrograman yang lebih normal. Mereka dapat diproduksi dengan operasi normal jika tidak ada hasil numerik (misalnya, mengambil akar kuadrat dari angka negatif jika hasilnya harus nyata). Tujuan mereka umumnya untuk memungkinkan aritmatika berjalan agak normal. Misalnya, Anda mungkin memiliki deretan angka yang sangat banyak, beberapa di antaranya mewakili kasus khusus yang tidak dapat ditangani secara normal. Anda dapat memanggil fungsi rumit untuk memproses larik ini, dan itu bisa beroperasi pada larik dengan aritmatika biasa, mengabaikan NaN. Setelah itu berakhir, Anda akan memisahkan kasus khusus untuk lebih banyak pekerjaan.
Eric Postpischil
@wrdieter Terima kasih, maka hanya perbedaan najor yang menghasilkan pengecualian atau tidak.
JalalJaberi
@EricPostpischil Terima kasih atas perhatian Anda pada pertanyaan kedua.
JalalJaberi
@JalalJaberi ya, pengecualiannya adalah perbedaan utama
wrdieter
36

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_NaNdan std::numeric_limits::signaling_NaNyang 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 nan0sama 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 nanfke depan untuk strtoddan 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 -O0di 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.0penyebab FE_INVALID, tetapi qnan + 1.0tidak

  • 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 SIGFPEadalah 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 setjmpdan longjmpmelompat 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 snanhanya menimbulkan kesalahan untuk lebih banyak operasi yang qnantidak 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, fetestexceptdiimplementasikan 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 stmxcsrkependekan dari "Store MXCSR Register State".

Dan feenableexceptdiimplementasikan 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:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
en.wikipedia.org/wiki/IEEE_754#Interchange_formats menunjukkan bahwa IEEE-754 hanya menyarankan bahwa 0 untuk memberi sinyal NaN adalah pilihan implementasi yang baik, untuk memungkinkan mendiamkan NaN tanpa mengambil risiko menjadikannya tak terbatas (signifikan = 0). Rupanya itu tidak terstandarisasi, meskipun itulah yang dilakukan x86. (Dan fakta bahwa itu adalah MSB dari significand yang menentukan qNaN vs Snan adalah standar). en.wikipedia.org/wiki/Single-precision_floating-point_format mengatakan x86 dan ARM adalah sama, tetapi PA-RISC membuat pilihan sebaliknya.
Peter Cordes
@PeterCordes ya, saya tidak yakin apa yang "harus" == "harus" atau "lebih disukai" di IEEE 754 20at "Sebuah string bit NaN pensinyalan harus dikodekan dengan bit pertama dari bidang signifikansi tambahan menjadi 0".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
re: tetapi tampaknya tidak menentukan bit mana yang harus digunakan untuk membedakan infinity dari NaN. Anda menulis bahwa seperti yang Anda harapkan akan ada beberapa bit spesifik yang direkomendasikan oleh standar untuk membedakan sNaN dari tak terhingga. IDK mengapa Anda berharap ada bit seperti itu; pilihan selain nol tidak masalah. Pilih saja sesuatu yang kemudian mengidentifikasi dari mana sNaN itu berasal. IDK, terdengar seperti ungkapan aneh, dan kesan pertama saya saat membacanya adalah Anda mengatakan bahwa halaman web tidak menjelaskan apa yang membedakan inf dari NaN dalam pengkodean (semua-nol signifikan).
Peter Cordes
Sebelum 2008, IEEE 754 mengatakan mana yang memberi sinyal / bit tenang (bit 22) tetapi tidak nilai yang menentukan apa. Sebagian besar prosesor telah berkumpul pada 1 = silent, sehingga dijadikan bagian dari standar pada edisi 2008. Ia mengatakan "harus" daripada "harus" untuk menghindari membuat implementasi lama yang membuat pilihan yang sama menjadi tidak sesuai. Secara umum, "harus" dalam arti standar "harus, kecuali Anda memiliki alasan yang sangat menarik (dan sebaiknya didokumentasikan dengan baik) untuk tidak mematuhi".
John Cowan