1.0是从std :: generate_canonical有效的输出?

我一直认为随机数在0和1之间, 没有1 ,即它们是半开区间[0,1)的数字。 std::generate_canonical cppreference.com上的文档证实了这一点。

但是,当我运行以下程序:

 #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; } 

它给了我以下输出:

 Bug! 

即它产生了一个完美的1 ,这导致了我的MC集成中的问题。 这是有效的行为还是在我身边有一个错误? 这给与G ++ 4.7.3相同的输出

 g++ -std=c++11 test.c && ./a.out 

和铛3.3

 clang++ -stdlib=libc++ -std=c++11 test.c && ./a.out 

如果这是正确的行为,我该如何避免1

编辑1 :从GIT G ++似乎遭受同样的问题。 我在

 commit baf369d7a57fb4d0d5897b02549c3517bb8800fd Date: Mon Sep 1 08:26:51 2014 +0000 

并编译~/temp/prefix/bin/c++ -std=c++11 -Wl,-rpath,/home/cschwan/temp/prefix/lib64 test.c && ./a.out给出相同的输出, ldd产量

 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) 

编辑2 :我在这里报告的行为: https : //gcc.gnu.org/bugzilla/show_bug.cgi?id = 63176

编辑3 :叮当队似乎意识到这个问题: http : //llvm.org/bugs/show_bug.cgi?id=18767

问题是从std::mt19937std::uint_fast32_t )的std::uint_fast32_tfloat ; 如果当前的IEEE754舍入模式不是圆到负无穷大,则标准描述的algorithm会产生错误的结果(与algorithm输出的描述不一致),如果当前的IEEE754舍入模式不是圆到负无穷大(注意缺省值是圆的-to-最近)。

mt19937与你的种子的输出是4294967257( 0xffffffd9u ),当四舍五入为32位浮点时给出0x1p+32 ,这等于mt19937,4294967295( 0xffffffffu )的最大值时,也被舍入为32位浮动。

标准可以确保正确的行为,如果它指定从URNG的输出转换为Real_canonical的RealType时,要对负无穷进行舍入; 这将在这种情况下给出正确的结果。 作为QOI,libstdc ++可以做出这样的改变。

随着这个变化, 1.0将不再生成; 取而代之,对于0 < N <= 8的边界值0x1.fffffep-N将更经常地生成(每N 2^(8 - N - 32)大约2^(8 - N - 32) ,这取决于MT19937的实际分布)。

我build议不要直接使用floatstd::generate_canonical ; 而是产生double的数字,然后向负的无穷大方向发展:

  double rd = std::generate_canonical<double, std::numeric_limits<float>::digits>(rng); float rf = rd; if (rf > rd) { rf = std::nextafter(rf, -std::numeric_limits<float>::infinity()); } 

这个问题也可以发生在std::uniform_real_distribution<float> ; 解决scheme是一样的,专门分配在double和四舍五入的结果对负无穷在float

根据标准, 1.0是无效的。

C ++ 11§26.5.7.2函数模板generate_canonical

从26.5.7.2节描述的模板实例化的每个函数将提供的统一随机数发生器g的一个或多个调用的结果映射到指定RealType的一个成员,使得如果由g产生的g i是均匀分布的,实例化结果t j0≤tj <1 ,按照以下规定尽可能均匀分布。

我刚刚遇到了与uniform_real_distribution类似的问题,下面是我如何解释标准对这个问题的简短措辞:

标准总是以math术语来定义math函数, 而不是以IEEE浮点(因为标准仍然假设浮点可能不意味着IEEE浮点)而言。 所以,任何时候你看标准中的math措辞,都是在谈论真正的math ,而不是IEEE。

标准说uniform_real_distribution<T>(0,1)(g)generate_canonical<T,1000>(g)应该返回半开范围[0,1)的值。 但是这些都是math价值。 当你在半开范围[0,1)中取一个实数并将其表示为IEEE浮点时,很大一部分时间将会到达T(1.0)

Tfloat (24个尾数位)时,我们期望看到uniform_real_distribution<float>(0,1)(g) == 1.0f大约在2 ^ 25次。 我用libc ++的powershell实validation实了这个期望。

 template<class F> void test(long long N, const F& get_a_float) { int count = 0; for (long long i = 0; i < N; ++i) { float f = get_a_float(); if (f == 1.0f) { ++count; } } printf("Expected %d '1.0' results; got %d in practice\n", (int)(N >> 25), count); } int main() { std::mt19937 g(std::random_device{}()); auto N = (1uLL << 29); test(N, [&g]() { return std::uniform_real_distribution<float>(0,1)(g); }); test(N, [&g]() { return std::generate_canonical<float, 32>(g); }); } 

示例输出:

 Expected 16 '1.0' results; got 19 in practice Expected 16 '1.0' results; got 11 in practice 

Tdouble (53个尾数位)时,我们期望在2 ^ 54次中看到uniform_real_distribution<double>(0,1)(g) == 1.0约1。 我没有耐心来testing这个期望。 🙂

我的理解是,这种行为是好的。 它可能会冒犯我们的“半开放性”的意识,即声称返回“小于1.0”的分布实际上可以返回等于 1.0数字; 但这些是“1.0”的两个不同含义,看? 首先是math 1.0; 第二个是IEEE单精度浮点数1.0 。 我们已经教了几十年不去比较浮点数的确切的平等。

无论您将随机数送入哪种algorithm都不会在意它是否恰好为1.0 。 除了math运算以外,你不能用浮点数来做任何事情 ,只要你做了一些math运算,你的代码将不得不处理舍入。 即使你可以合理地假设generate_canonical<float,1000>(g) != 1.0f ,你仍然不能假设generate_canonical<float,1000>(g) + 1.0f != 2.0f – 因为四舍五入。 你不能摆脱它; 那么为什么我们会在这个单一的例子中假装你可以?