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::mt19937
( std::uint_fast32_t
)的std::uint_fast32_t
到float
; 如果当前的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议不要直接使用float
与std::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 j , 0≤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)
。
当T
是float
(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
当T
是double
(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
– 因为四舍五入。 你不能摆脱它; 那么为什么我们会在这个单一的例子中假装你可以?