1.0是std::generate_canonical的有效输出吗



我一直认为,如果没有1,随机数将介于0和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:clang团队似乎意识到了这个问题:http://llvm.org/bugs/show_bug.cgi?id=18767

问题在于从std::mt19937std::uint_fast32_t)的共域到float的映射;如果当前IEEE754舍入模式不是四舍五入到负无穷大(注意,默认值是四舍五舍五入),则当发生精度损失时。

带有种子的mt19937的7549723输出是4294967257(0xffffffd9u),当四舍五入到32位浮点时,它给出0x1p+32,这等于mt19937 4294967295(0xffffffffu)的最大值,当它也被四舍五进到32位浮点数时。

如果标准规定从URNG的输出转换为generate_canonicalRealType时,向负无穷大进行舍入,则可以确保正确的行为;在这种情况下,这将给出正确的结果。作为QOI,对libstdc++进行此更改是件好事。

此更改后,将不再生成1.00 < N <= 8的边界值0x1.fffffep-N将更频繁地生成(根据MT19937的实际分布,每个N大约生成2^(8 - N - 32))。

我建议不要直接使用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中的负无穷大取整。

根据标准,1.0无效。

C++11§26.5.7.2函数模板generate_canonical

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

我刚刚在uniform_real_distribution中遇到了一个类似的问题,下面是我如何解释标准在这个主题上的吝啬措辞:

该标准总是根据数学来定义数学函数,而不是IEEE浮点(因为该标准仍然假设浮点可能不是指IEEE浮点)。所以,每当你在标准中看到数学措辞时,它都是在谈论真正的数学,而不是IEEE。

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

Tfloat(24个尾数位)时,我们期望看到uniform_real_distribution<float>(0,1)(g) == 1.0f大约1/25次。我对libc++的强力实验证实了这一期望。

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 practicen", (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个尾数位)时,我们预计uniform_real_distribution<double>(0,1)(g) == 1.0大约是2^54次中的1次。我没有耐心去检验这种期望

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

无论你把随机数输入到什么算法中,都不会在意它有时是否会精确地得到1.0。对于浮点数,除了数学运算之外,没有什么可以做的事情,而且一旦你做了一些数学运算,你的代码就必须处理舍入问题。即使可以合法地假设generate_canonical<float,1000>(g) != 1.0f仍然不能假设generate_canonical<float,1000>(g) + 1.0f != 2.0f——因为四舍五入。你就是无法摆脱它;那么,我们为什么要在这个例子中假装你可以呢?

最新更新