1.0是来自 std: : create_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 : 我在这里报告了这个行为: < a href = “ https://gcc.gnu.org/bugzilla/show _ bug.cgi? id = 63176”> https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63176

编辑3 : clang 团队似乎意识到了这个问题: < a href = “ http://llvm.org/bug/show _ bug.cgi? id = 18767”> http://llvm.org/bugs/show_bug.cgi?id=18767

6538 次浏览

根据标准,1.0是无效的。

C + + 1126.5.7.2函数模板 create _ canonical

Each function instantiated from the template described in this section 26.5.7.2 maps the result of one or more invocations of a supplied uniform random number generator g to one member of the specified RealType such that, if the values g produced by g are uniformly distributed, the instantiation’s results tJ , 0 ≤ tJ < 1, are distributed as uniformly as possible as specified below.

问题在于从 std::mt19937(std::uint_fast32_t)的余域映射到 float; 当当前的 IEEE754四舍五入模式不是四舍五入到负无穷大(注意默认是四舍五入到最近)时,标准描述的算法给出了不正确的结果(与其对算法输出的描述不一致)。

带有种子的 mt19937的第7549723个输出是4294967257(0xffffffd9u) ,当四舍五入为32位浮点数时,得到的 0x1p+32等于 mt19937的最大值,当四舍五入为32位浮点数时,得到的最大值是4294967295(0xffffffffu)。

该标准可以确保正确的行为,如果它指定,当从 URNG 的输出转换到 generate_canonicalRealType时,舍入将被执行到负无穷大; 这将在这种情况下给出一个正确的结果。作为 QOI,对于 libstdc + + 来说,进行这种更改是有好处的。

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

我建议不要直接使用 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>上,解决方案也是一样的,专门化 double上的分布,使结果在 float上趋于负无穷大。

我刚刚在 uniform_real_distribution上遇到了一个类似的问题,下面是我如何解读标准在这个问题上简约的措辞:

标准总是根据 数学来定义数学函数,而不是根据 IEEE 浮点(因为标准仍然假定浮点 也许不会意味着 IEEE 浮点)。因此,任何时候你在标准中看到的数学词汇,都是在谈论 real math,而不是 IEEE。

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

Tfloat(24尾数位)时,我们期望在2 ^ 25次中看到 uniform_real_distribution<float>(0,1)(g) == 1.0f约1

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。我没有耐心去测试这个期望。:)

我的理解是这种行为是正常的。声称返回数字“小于1.0”的分布实际上可以返回数字 平等1.0,这可能会冒犯我们的“半开放范围”的感觉; 但这是“1.0”的两种不同含义,明白吗?第一个是 数学1.0; 第二个是 IEEE 单精度浮点数 1.0。几十年来,我们一直被教导不要为了精确的相等而去比较浮点数。

无论你把随机数输入哪个算法,它都不会关心它是否有时会得到正确的 1.0。除了数学运算,没有什么东西可以用浮点数来表示 ,一旦你做了一些数学运算,你的代码将不得不处理舍入。即使你合理地假设 可以generate_canonical<float,1000>(g) != 1.0f,你的 还是也不能假设是 generate_canonical<float,1000>(g) + 1.0f != 2.0fーー因为四舍五入。你就是摆脱不了它,那么我们为什么要在这个单一的情况下假装你可以呢?