Hasil floating point berbeda dengan pengoptimalan diaktifkan - bug kompilator?

109

Kode di bawah ini berfungsi pada Visual Studio 2008 dengan dan tanpa pengoptimalan. Tetapi ini hanya berfungsi pada g ++ tanpa pengoptimalan (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Outputnya harus:

4.5
4.6

Tapi g ++ dengan pengoptimalan ( O1- O3) akan menghasilkan:

4.5
4.5

Jika saya menambahkan volatilekata kunci sebelum t, itu berfungsi, jadi mungkinkah ada semacam bug pengoptimalan?

Uji pada g ++ 4.1.2, dan 4.4.4.

Berikut adalah hasil di ideone: http://ideone.com/Rz937

Dan opsi yang saya uji di g ++ sederhana:

g++ -O2 round.cpp

Makin menarik hasilnya, meski saya mengaktifkan /fp:fastopsi Visual Studio 2008, hasilnya tetap benar.

Pertanyaan lebih lanjut:

Saya bertanya-tanya, haruskah saya selalu mengaktifkan -ffloat-storeopsi?

Karena versi g ++ yang saya uji dikirimkan dengan CentOS / Red Hat Linux 5 dan CentOS / Redhat 6 .

Saya mengumpulkan banyak program saya di bawah platform ini, dan saya khawatir itu akan menyebabkan bug yang tidak terduga di dalam program saya. Tampaknya agak sulit untuk menyelidiki semua kode C ++ saya dan pustaka yang digunakan apakah mereka memiliki masalah seperti itu. Ada saran?

Adakah yang tertarik mengapa bahkan /fp:fastdihidupkan, Visual Studio 2008 masih berfungsi? Sepertinya Visual Studio 2008 lebih dapat diandalkan dalam masalah ini daripada g ++?

Beruang
sumber
51
Untuk semua pengguna baru SO: INI adalah cara Anda mengajukan pertanyaan. +1
tenfour
1
FWIW, saya mendapatkan hasil yang benar dengan g ++ 4.5.0 menggunakan MinGW.
Steve Blackwell
2
ideone menggunakan 4.3.4 ideone.com/b8VXg
Daniel A. White
5
Anda harus ingat bahwa rutinitas Anda tidak mungkin bekerja dengan andal dengan semua jenis hasil. Berbeda dengan pembulatan ganda menjadi bilangan bulat, ini rentan terhadap fakta bahwa tidak semua bilangan real dapat direpresentasikan sehingga Anda akan mendapatkan lebih banyak bug seperti ini.
Jakub Wieczorek
2
Untuk mereka yang tidak dapat mereproduksi bug: jangan hapus komentar pada debug stmts, mereka mempengaruhi hasil.
n. 'kata ganti' m.

Jawaban:

91

Prosesor Intel x86 menggunakan presisi diperpanjang 80-bit secara internal, sedangkan doublebiasanya lebar 64-bit. Tingkat pengoptimalan yang berbeda memengaruhi seberapa sering nilai floating point dari CPU disimpan ke dalam memori dan dengan demikian dibulatkan dari presisi 80-bit ke presisi 64-bit.

Gunakan -ffloat-storeopsi gcc untuk mendapatkan hasil titik mengambang yang sama dengan tingkat pengoptimalan yang berbeda.

Alternatifnya, gunakan long doubletipe, yang biasanya lebar 80-bit pada gcc untuk menghindari pembulatan dari presisi 80-bit ke 64-bit.

man gcc mengatakan semuanya:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

Di kompiler build x86_64 menggunakan register SSE untuk floatdan doublesecara default, sehingga presisi yang diperpanjang tidak digunakan dan masalah ini tidak terjadi.

gccopsi kompilator-mfpmath mengontrolnya.

Maxim Egorushkin
sumber
20
Saya rasa inilah jawabannya. Konstanta 4,55 diubah menjadi 4,54999999999999 yang merupakan representasi biner terdekat dalam 64 bit; kalikan dengan 10 dan bulatkan lagi menjadi 64 bit dan Anda mendapatkan 45,5. Jika Anda melewatkan langkah pembulatan dengan menyimpannya dalam register 80-bit, Anda akan mendapatkan 45.4999999999999.
Mark Ransom
Terima kasih, saya bahkan tidak tahu opsi ini. Tapi saya bertanya-tanya, haruskah saya selalu mengaktifkan opsi -ffloat-store? Karena versi g ++ yang saya uji dikirimkan dengan CentOS / Redhat 5 dan CentOS / Redhat 6. Saya mengkompilasi banyak program saya di bawah platform ini, saya khawatir itu akan menyebabkan bug yang tidak terduga di dalam program saya.
Beruang
5
@ Bear, pernyataan debug mungkin menyebabkan nilai di-flush dari register ke memori.
Mark Ransom
2
@ Bear, biasanya aplikasi Anda akan mendapatkan keuntungan dari presisi yang diperpanjang, kecuali jika beroperasi pada nilai yang sangat kecil atau sangat besar saat float 64-bit diperkirakan akan mengalami kelebihan atau kekurangan dan produksi inf. Tidak ada aturan praktis yang baik, pengujian unit dapat memberi Anda jawaban yang pasti.
Maxim Egorushkin
2
@bear Sebagai aturan umum jika Anda membutuhkan hasil yang dapat diprediksi dengan sempurna dan / atau persis seperti apa yang akan didapatkan manusia dalam melakukan penjumlahan di atas kertas, maka Anda harus menghindari floating point. -ffloat-store menghilangkan satu sumber ketidakpastian tapi itu bukan peluru ajaib.
plugwash
10

Output seharusnya: 4.5 4.6 Itulah output jika Anda memiliki presisi tak terbatas, atau jika Anda bekerja dengan perangkat yang menggunakan representasi floating point berbasis desimal daripada berbasis biner. Tapi, kamu tidak. Sebagian besar komputer menggunakan standar floating point IEEE biner.

Seperti yang telah dicatat oleh Maxim Yegorushkin dalam jawabannya, sebagian masalahnya adalah bahwa secara internal komputer Anda menggunakan representasi titik mengambang 80 bit. Ini hanya sebagian dari masalah. Dasar dari masalah ini adalah bahwa bilangan apapun dalam bentuk n.nn5 tidak memiliki representasi mengambang biner yang tepat. Kasus sudut itu selalu angka yang tidak pasti.

Jika Anda benar-benar ingin pembulatan Anda dapat diandalkan untuk kasus sudut ini, Anda memerlukan algoritma pembulatan yang membahas fakta bahwa n.n5, n.nn5, atau n.nnn5, dll. (Tetapi tidak n.5) selalu tdk tepat. Temukan kapitalisasi sudut yang menentukan apakah beberapa nilai input membulatkan ke atas atau ke bawah dan mengembalikan nilai dibulatkan ke atas atau ke bawah berdasarkan perbandingan dengan kapitalisasi sudut ini. Dan Anda perlu berhati-hati bahwa kompiler pengoptimalan tidak akan meletakkan kasus sudut yang ditemukan itu dalam register presisi yang diperluas.

Lihat Bagaimana Excel berhasil Membulatkan angka Mengambang meskipun tidak tepat? untuk algoritma seperti itu.

Atau Anda bisa hidup dengan fakta bahwa kasing sudut terkadang membulat secara keliru.

David Hammen
sumber
6

Penyusun yang berbeda memiliki pengaturan pengoptimalan yang berbeda. Beberapa dari pengaturan pengoptimalan yang lebih cepat tersebut tidak mempertahankan aturan floating-point yang ketat sesuai dengan IEEE 754 . Visual Studio memiliki pengaturan khusus, /fp:strict, /fp:precise, /fp:fast, di mana /fp:fastmelanggar standar pada apa yang bisa dilakukan. Anda mungkin menemukan bahwa ini bendera adalah apa yang mengontrol optimasi dalam pengaturan tersebut. Anda juga dapat menemukan setelan serupa di GCC yang mengubah perilaku.

Jika ini masalahnya, satu-satunya hal yang berbeda di antara compiler adalah GCC akan mencari perilaku floating point tercepat secara default pada pengoptimalan yang lebih tinggi, sedangkan Visual Studio tidak mengubah perilaku floating point dengan tingkat pengoptimalan yang lebih tinggi. Oleh karena itu, ini mungkin bukan bug yang sebenarnya, tetapi perilaku yang diinginkan dari opsi yang Anda tidak tahu sedang Anda aktifkan.

Anak anjing
sumber
4
Ada -ffast-mathtombol untuk GCC itu, dan itu tidak diaktifkan oleh -Otingkat pengoptimalan mana pun sejak kutipan: "ini dapat menghasilkan keluaran yang salah untuk program yang bergantung pada implementasi yang tepat dari aturan / spesifikasi IEEE atau ISO untuk fungsi matematika."
Mat
@ Mat: Saya sudah mencoba -ffast-mathdan beberapa hal lain pada saya g++ 4.4.3dan saya masih tidak dapat mereproduksi masalah tersebut.
NPE
Bagus: dengan -ffast-mathsaya mendapatkan 4.5dalam kedua kasus untuk tingkat pengoptimalan lebih besar dari 0.
Kerrek SB
(Koreksi: Saya mendapatkan 4.5dengan -O1dan -O2, tetapi tidak dengan -O0dan -O3di GCC 4.4.3, tetapi dengan -O1,2,3di GCC 4.6.1.)
Kerrek SB
4

Untuk mereka yang tidak dapat mereproduksi bug: jangan hapus komentar pada debug stmts, mereka mempengaruhi hasil.

Ini menyiratkan bahwa masalahnya terkait dengan pernyataan debug. Dan sepertinya ada kesalahan pembulatan yang disebabkan oleh pemuatan nilai ke dalam register selama pernyataan keluaran, itulah sebabnya orang lain menemukan bahwa Anda dapat memperbaikinya dengan-ffloat-store

Pertanyaan lebih lanjut:

Saya bertanya-tanya, haruskah saya selalu mengaktifkan -ffloat-storeopsi?

Untuk menjadi kurang ajar, pasti ada alasan mengapa beberapa pemrogram tidak aktif -ffloat-store, jika tidak opsi tersebut tidak akan ada (juga, pasti ada alasan mengapa beberapa pemrogram benar-benar aktif -ffloat-store). Saya tidak akan merekomendasikan untuk selalu menyalakan atau mematikannya. Mengaktifkannya mencegah beberapa pengoptimalan, tetapi mematikannya memungkinkan untuk jenis perilaku yang Anda dapatkan.

Namun, secara umum, ada beberapa ketidakcocokan antara bilangan floating point biner (seperti yang digunakan komputer) dan bilangan floating point desimal (yang sudah dikenal orang), dan ketidakcocokan itu dapat menyebabkan perilaku yang serupa dengan apa yang Anda dapatkan (untuk lebih jelasnya, perilaku Anda mendapatkan bukan disebabkan oleh ketidakcocokan ini, tetapi perilaku serupa dapat terjadi). Masalahnya, karena Anda sudah memiliki beberapa ketidakjelasan saat berurusan dengan floating point, saya tidak bisa mengatakan itu -ffloat-storemembuatnya lebih baik atau lebih buruk.

Sebaliknya, Anda mungkin ingin mencari solusi lain untuk masalah yang Anda coba selesaikan (sayangnya, Koenig tidak menunjuk ke makalah yang sebenarnya, dan saya tidak dapat benar-benar menemukan tempat "kanonik" yang jelas untuk itu, jadi saya Anda harus mengirim Anda ke Google ).


Jika Anda tidak membulatkan untuk tujuan keluaran, saya mungkin akan melihat std::modf()(in cmath) dan std::numeric_limits<double>::epsilon()(in limits). Memikirkan round()fungsi aslinya , saya yakin akan lebih bersih untuk mengganti panggilan ke std::floor(d + .5)dengan panggilan ke fungsi ini:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Saya pikir itu menunjukkan peningkatan berikut:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Catatan sederhana: std::numeric_limits<T>::epsilon()didefinisikan sebagai "angka terkecil yang ditambahkan ke 1 yang membuat angka tidak sama dengan 1." Anda biasanya perlu menggunakan epsilon relatif (yaitu, skala epsilon entah bagaimana untuk menjelaskan fakta bahwa Anda bekerja dengan angka selain "1"). Jumlah d, .5dan std::numeric_limits<double>::epsilon()harus mendekati 1, jadi mengelompokkan penambahan tersebut berarti std::numeric_limits<double>::epsilon()ukurannya akan sesuai untuk apa yang kita lakukan. Jika ada, std::numeric_limits<double>::epsilon()akan terlalu besar (bila jumlah ketiganya kurang dari satu) dan dapat menyebabkan kita membulatkan beberapa angka padahal seharusnya tidak.


Saat ini, Anda harus mempertimbangkan std::nearbyint().

Max Lybbert
sumber
Sebuah "epsilon relatif" disebut 1 ulp (1 unit di tempat terakhir). x - nextafter(x, INFINITY)terkait dengan 1 ulp untuk x (tapi jangan gunakan itu; saya yakin ada kasus sudut dan saya baru saja mengarangnya). Contoh cppreference for epsilon() memiliki contoh penskalaan untuk mendapatkan error relatif berbasis ULP .
Peter Cordes
2
BTW, jawaban 2016 -ffloat-storeadalah: jangan gunakan x87 di tempat pertama. Gunakan matematika SSE2 (biner 64-bit, atau -mfpmath=sse -msse2untuk membuat biner 32-bit yang lama), karena SSE / SSE2 memiliki temporer tanpa presisi ekstra. doubledan floatvars di register XMM benar-benar dalam format IEEE 64-bit atau 32-bit. (Tidak seperti x87, di mana register selalu 80-bit, dan menyimpan ke memori putaran ke 32 atau 64 bit.)
Peter Cordes
3

Jawaban yang diterima benar jika Anda mengompilasi ke target x86 yang tidak menyertakan SSE2. Semua prosesor x86 modern mendukung SSE2, jadi jika Anda dapat memanfaatkannya, Anda harus:

-mfpmath=sse -msse2 -ffp-contract=off

Mari kita uraikan ini.

-mfpmath=sse -msse2. Ini melakukan pembulatan dengan menggunakan register SSE2, yang jauh lebih cepat daripada menyimpan setiap hasil antara ke memori. Perhatikan bahwa ini sudah menjadi default di GCC untuk x86-64. Dari wiki GCC :

Pada prosesor x86 yang lebih modern yang mendukung SSE2, menentukan opsi compiler -mfpmath=sse -msse2memastikan semua operasi float dan double dilakukan dalam register SSE dan dibulatkan dengan benar. Opsi ini tidak memengaruhi ABI dan oleh karena itu harus digunakan jika memungkinkan untuk hasil numerik yang dapat diprediksi.

-ffp-contract=off. Namun, mengontrol pembulatan tidak cukup untuk pencocokan persis. Instruksi FMA (fused multiply-add) dapat mengubah perilaku pembulatan versus rekan non-fusi, jadi kita perlu menonaktifkannya. Ini adalah default di Clang, bukan GCC. Seperti yang dijelaskan oleh jawaban ini :

FMA hanya memiliki satu pembulatan (ini secara efektif menjaga ketepatan tak terbatas untuk hasil perkalian sementara internal), sedangkan ADD + MUL memiliki dua.

Dengan menonaktifkan FMA, kami mendapatkan hasil yang sama persis dengan debug dan rilis, dengan mengorbankan beberapa performa (dan akurasi). Kami masih dapat memanfaatkan keunggulan kinerja SSE dan AVX lainnya.

tmandry
sumber
1

Saya menggali lebih dalam masalah ini dan saya dapat memberikan lebih banyak ketepatan. Pertama, representasi tepat 4,45 dan 4,55 menurut gcc pada x84_64 adalah sebagai berikut (dengan libquadmath untuk mencetak presisi terakhir):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Seperti yang dikatakan Maxim di atas, masalahnya adalah karena ukuran register FPU 80 bit.

Tetapi mengapa masalah tersebut tidak pernah terjadi pada Windows? pada IA-32, FPU x87 dikonfigurasi untuk menggunakan presisi internal untuk mantissa 53 bit (setara dengan ukuran total 64 bit :) double. Untuk Linux dan Mac OS, presisi default 64 bit digunakan (setara dengan ukuran total 80 bit :) long double. Jadi masalahnya harus mungkin, atau tidak, pada platform yang berbeda ini dengan mengubah kata kontrol FPU (dengan asumsi urutan instruksi akan memicu bug). Masalah ini dilaporkan ke gcc sebagai bug 323 (baca setidaknya komentar 92!).

Untuk menunjukkan presisi mantissa di Windows, Anda dapat mengkompilasinya dalam 32 bit dengan VC ++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

dan di Linux / Cygwin:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Perhatikan bahwa dengan gcc Anda dapat menyetel presisi FPU dengan -mpc32/64/80, meskipun itu diabaikan di Cygwin. Tetapi perlu diingat bahwa itu akan mengubah ukuran mantissa, tetapi bukan eksponen, membiarkan pintu terbuka untuk jenis lain dari perilaku yang berbeda.

Pada arsitektur x86_64, SSE digunakan seperti yang dikatakan oleh tmandry , jadi masalah tidak akan terjadi kecuali Anda memaksa FPU x87 lama untuk komputasi FP dengan -mfpmath=387, atau kecuali Anda mengkompilasi dalam mode 32 bit dengan -m32(Anda memerlukan paket multilib). Saya dapat mereproduksi masalah di Linux dengan berbagai kombinasi flag dan versi gcc:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Saya mencoba beberapa kombinasi pada Windows atau Cygwin dengan VC ++ / gcc / tcc tetapi bug tidak pernah muncul. Saya kira urutan instruksi yang dihasilkan tidak sama.

Terakhir, perhatikan bahwa cara eksotis untuk mencegah masalah ini dengan 4.45 atau 4.55 akan menggunakan _Decimal32/64/128, tetapi dukungan sangat langka ... Saya menghabiskan banyak waktu hanya untuk dapat melakukan printf libdfp!

calandoa
sumber
0

Secara pribadi, saya mengalami masalah yang sama dengan cara lain - dari gcc ke VS. Dalam kebanyakan kasus, saya pikir lebih baik menghindari pengoptimalan. Satu-satunya saat yang berharga adalah ketika Anda berurusan dengan metode numerik yang melibatkan array besar data titik mengambang. Bahkan setelah pembongkaran, saya sering merasa kurang puas dengan pilihan kompiler. Seringkali lebih mudah menggunakan intrinsik kompilator atau hanya menulis rakitan sendiri.

cdcdcd
sumber