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 volatile
kata 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:fast
opsi Visual Studio 2008, hasilnya tetap benar.
Pertanyaan lebih lanjut:
Saya bertanya-tanya, haruskah saya selalu mengaktifkan -ffloat-store
opsi?
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:fast
dihidupkan, Visual Studio 2008 masih berfungsi? Sepertinya Visual Studio 2008 lebih dapat diandalkan dalam masalah ini daripada g ++?
sumber
Jawaban:
Prosesor Intel x86 menggunakan presisi diperpanjang 80-bit secara internal, sedangkan
double
biasanya 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-store
opsi gcc untuk mendapatkan hasil titik mengambang yang sama dengan tingkat pengoptimalan yang berbeda.Alternatifnya, gunakan
long double
tipe, yang biasanya lebar 80-bit pada gcc untuk menghindari pembulatan dari presisi 80-bit ke 64-bit.man gcc
mengatakan semuanya:Di kompiler build x86_64 menggunakan register SSE untuk
float
dandouble
secara default, sehingga presisi yang diperpanjang tidak digunakan dan masalah ini tidak terjadi.gcc
opsi kompilator-mfpmath
mengontrolnya.sumber
inf
. Tidak ada aturan praktis yang baik, pengujian unit dapat memberi Anda jawaban yang pasti.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.
sumber
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:fast
melanggar 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.
sumber
-ffast-math
tombol untuk GCC itu, dan itu tidak diaktifkan oleh-O
tingkat 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."-ffast-math
dan beberapa hal lain pada sayag++ 4.4.3
dan saya masih tidak dapat mereproduksi masalah tersebut.-ffast-math
saya mendapatkan4.5
dalam kedua kasus untuk tingkat pengoptimalan lebih besar dari0
.4.5
dengan-O1
dan-O2
, tetapi tidak dengan-O0
dan-O3
di GCC 4.4.3, tetapi dengan-O1,2,3
di GCC 4.6.1.)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
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-store
membuatnya 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()
(incmath
) danstd::numeric_limits<double>::epsilon()
(inlimits
). Memikirkanround()
fungsi aslinya , saya yakin akan lebih bersih untuk mengganti panggilan kestd::floor(d + .5)
dengan panggilan ke fungsi ini:Saya pikir itu menunjukkan peningkatan berikut:
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"). Jumlahd
,.5
danstd::numeric_limits<double>::epsilon()
harus mendekati 1, jadi mengelompokkan penambahan tersebut berartistd::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()
.sumber
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 forepsilon()
memiliki contoh penskalaan untuk mendapatkan error relatif berbasis ULP .-ffloat-store
adalah: jangan gunakan x87 di tempat pertama. Gunakan matematika SSE2 (biner 64-bit, atau-mfpmath=sse -msse2
untuk membuat biner 32-bit yang lama), karena SSE / SSE2 memiliki temporer tanpa presisi ekstra.double
danfloat
vars 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.)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:
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 :-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 :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.
sumber
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):
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 ++:
dan di Linux / Cygwin:
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: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 printflibdfp
!sumber
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.
sumber