Saya selalu berpikir bilangan acak akan berada di antara nol dan satu, tanpa1
, yaitu nomor dari interval setengah terbuka [0,1). The dokumentasi pada cppreference.com dari std::generate_canonical
menegaskan ini.
Namun, ketika saya menjalankan program berikut:
#include <iostream>
#include <limits>
#include <random>
int main()
{
std::mt19937 rng;
std::seed_seq sequence{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
rng.seed(sequence);
rng.discard(12 * 629143 + 6);
float random = std::generate_canonical<float,
std::numeric_limits<float>::digits>(rng);
if (random == 1.0f)
{
std::cout << "Bug!\n";
}
return 0;
}
Ini memberi saya output berikut:
Bug!
yaitu menghasilkan saya yang sempurna 1
, yang menyebabkan masalah dalam integrasi MC saya. Apakah itu perilaku yang valid atau adakah kesalahan di pihak saya? Ini memberikan keluaran yang sama dengan G ++ 4.7.3
g++ -std=c++11 test.c && ./a.out
dan dentang 3.3
clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out
Jika ini adalah perilaku yang benar, bagaimana saya bisa menghindarinya 1
?
Sunting 1 : G ++ dari git tampaknya mengalami masalah yang sama. aku berada
commit baf369d7a57fb4d0d5897b02549c3517bb8800fd
Date: Mon Sep 1 08:26:51 2014 +0000
dan kompilasi dengan ~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.out
memberikan hasil yang sama, ldd
hasil
linux-vdso.so.1 (0x00007fff39d0d000)
libstdc++.so.6 => /home/cschwan/temp/prefix/lib64/libstdc++.so.6 (0x00007f123d785000)
libm.so.6 => /lib64/libm.so.6 (0x000000317ea00000)
libgcc_s.so.1 => /home/cschwan/temp/prefix/lib64/libgcc_s.so.1 (0x00007f123d54e000)
libc.so.6 => /lib64/libc.so.6 (0x000000317e600000)
/lib64/ld-linux-x86-64.so.2 (0x000000317e200000)
Sunting 2 : Saya melaporkan perilaku di sini: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176
Sunting 3 : Tim dentang tampaknya menyadari masalah ini: http://llvm.org/bugs/show_bug.cgi?id=18767
1.f == 1.f
dalam semua kasus (semua kasus ada di sana? Saya bahkan tidak melihat variabel apa pun1.f == 1.f
; hanya ada satu kasus di sini1.f == 1.f
:, dan itu selalutrue
). Tolong jangan menyebarkan mitos ini lebih jauh. Perbandingan floating point selalu tepat.abs(random - 1.f) < numeric_limits<float>::epsilon
pemeriksaan jika hasilnya mendekati 1.0 , yang benar-benar salah dalam konteks ini: ada angka yang mendekati 1.0 yang merupakan hasil yang valid di sini, yaitu, semua yang kurang dari 1.0.Jawaban:
Masalahnya adalah dalam pemetaan dari codomain dari
std::mt19937
(std::uint_fast32_t
) kefloat
; algoritma yang dijelaskan oleh standar memberikan hasil yang salah (tidak konsisten dengan deskripsi output dari algoritma) ketika kehilangan presisi terjadi jika mode pembulatan IEEE754 saat ini adalah selain round-to-negative-infinity (perhatikan bahwa defaultnya adalah round -ke-terdekat).Output 7549723 dari mt19937 dengan seed Anda adalah 4294967257 (
0xffffffd9u
), yang jika dibulatkan ke float 32-bit menghasilkan0x1p+32
, yang sama dengan nilai maksimal mt19937, 4294967295 (0xffffffffu
) ketika itu juga dibulatkan ke float 32-bit.Standar tersebut dapat memastikan perilaku yang benar jika itu untuk menentukan hal itu ketika mengkonversi dari output URNG ke
RealType
darigenerate_canonical
, pembulatan harus dilakukan menuju tak terhingga negatif; ini akan memberikan hasil yang benar dalam kasus ini. Sebagai QOI, sebaiknya libstdc ++ melakukan perubahan ini.Dengan perubahan ini,
1.0
tidak akan dibuat lagi; sebagai gantinya nilai batas0x1.fffffep-N
untuk0 < N <= 8
akan dibuat lebih sering (kira-kira2^(8 - N - 32)
perN
, tergantung pada distribusi aktual MT19937).Saya akan merekomendasikan untuk tidak menggunakan
float
denganstd::generate_canonical
langsung; lebih baik buat angka dalamdouble
dan kemudian bulatkan menuju tak terhingga negatif:Masalah ini juga bisa terjadi dengan
std::uniform_real_distribution<float>
; solusinya sama, untuk mengkhususkan distribusi terusdouble
dan membulatkan hasil menuju negatif tak terhingga dalamfloat
.sumber
sin(x)
, yang sebenarnya diinginkannya adalah sinus (π / Math.PI) kali x. Orang-orang yang memelihara Java bersikeras bahwa lebih baik memiliki laporan rutin matematika yang lambat bahwa sinus Math.PI adalah perbedaan antara π dan Math.PI daripada membuatnya melaporkan nilai yang sedikit lebih kecil, meskipun dalam 99% aplikasi itu akan lebih baik ...std::uniform_real_distribution<float>
menderita masalah yang sama sebagai akibat dari ini. (Sehingga orang-orang yang mencari uniform_real_distribution akan melihat Q / A ini).generate_canonical
harus menghasilkan angka dalam kisaran[0,1)
, dan kita berbicara tentang kesalahan di mana kadang-kadang menghasilkan 1,0, bukankah pembulatan ke arah nol sama efektifnya?Menurut standar,
1.0
tidak valid.sumber
Saya baru saja menemukan pertanyaan serupa dengan
uniform_real_distribution
, dan inilah cara saya menafsirkan kata-kata pelit Standar tentang masalah ini:Standar selalu mendefinisikan fungsi matematika dalam istilah matematika , tidak pernah dalam istilah titik mengambang IEEE (karena Standar masih menganggap bahwa titik mengambang mungkin tidak berarti titik mengambang IEEE). Jadi, setiap kali Anda melihat susunan kata matematika dalam Standar, itu berbicara tentang matematika nyata , bukan IEEE.
Standar mengatakan bahwa keduanya
uniform_real_distribution<T>(0,1)(g)
dangenerate_canonical<T,1000>(g)
harus mengembalikan nilai dalam rentang setengah terbuka [0,1). Tapi ini adalah nilai matematis . Saat Anda mengambil bilangan real dalam rentang setengah terbuka [0,1) dan merepresentasikannya sebagai titik mengambang IEEE, yah, sebagian besar waktu bilangan tersebut akan dibulatkan ke atasT(1.0)
.When
T
isfloat
(24 mantissa bits), kami berharap untuk melihatuniform_real_distribution<float>(0,1)(g) == 1.0f
sekitar 1 dalam 2 ^ 25 kali. Eksperimen brute-force saya dengan libc ++ menegaskan harapan ini.Contoh keluaran:
When
T
isdouble
(53 mantissa bits), kami berharap untuk melihatuniform_real_distribution<double>(0,1)(g) == 1.0
sekitar 1 dalam 2 ^ 54 kali. Saya tidak memiliki kesabaran untuk menguji harapan ini. :)Pemahaman saya adalah bahwa perilaku ini baik-baik saja. Mungkin menyinggung perasaan kita tentang "setengah-rentang-terbuka" bahwa distribusi yang mengklaim mengembalikan angka "kurang dari 1.0" sebenarnya dapat mengembalikan angka yang sama dengan
1.0
; tapi itu adalah dua arti yang berbeda dari "1.0", paham? Yang pertama adalah matematika 1.0; yang kedua adalah angka floating-point presisi tunggal IEEE1.0
. Dan kami telah diajarkan selama beberapa dekade untuk tidak membandingkan angka floating-point untuk persamaan yang tepat.Apa pun algoritme yang Anda masukkan ke dalam angka acak tidak akan peduli jika terkadang benar
1.0
. Tidak ada yang bisa Anda lakukan lakukan dengan bilangan floating-point kecuali operasi matematika, dan segera setelah Anda melakukan beberapa operasi matematika, kode Anda harus berurusan dengan pembulatan. Bahkan jika Anda dapat secara sah berasumsi demikiangenerate_canonical<float,1000>(g) != 1.0f
, Anda tetap tidak dapat berasumsi demikiangenerate_canonical<float,1000>(g) + 1.0f != 2.0f
- karena pembulatan. Anda tidak bisa lepas darinya; jadi mengapa kita berpura-pura dalam contoh tunggal ini bahwa Anda bisa?sumber
1.0f
tetapi itu tidak dapat dihindari ketika Anda melemparkannya ke float IEEE. Jika Anda menginginkan hasil matematika murni, gunakan sistem komputasi simbolik; jika Anda mencoba menggunakan IEEE floating-point untuk mewakili angka yang berada di dalameps
1, Anda berada dalam status dosa.canonical - 1.0f
. Untuk setiap float yang dapat direpresentasikan[0, 1.0)
,x-1.0f
bukan nol. Dengan tepat 1,0f, Anda bisa mendapatkan pembagian-dengan-nol, bukan hanya pembagi yang sangat kecil.