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


124

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

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

#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


21
@David Lively 1.f == 1.f在所有情况下(所有情况都存在吗?我什至没有看到任何变量1.f == 1.f;这里只有一种情况:1.f == 1.f,并且总是如此true)。请不要进一步传播这个神话。浮点比较始终是精确的。
R. Martinho Fernandes

15
@DavidLively:不,不是。比较总是精确的。如果您的操作数是经过计算的而不是文字,那么它们可能并不准确。

2
@Galik任何低于1.0的正数都是有效的结果。1.0不是。就这么简单。舍入无关紧要:代码获得一个随机数,并且不对其进行任何舍入。
R. Martinho Fernandes 2014年

7
@DavidLively他说只有一个值等于1.0。该值为1.0。接近1.0的值不等于1.0。生成函数的功能无关紧要:如果返回1.0,则它等于1.0。如果不返回1.0,则它将不等于1.0。您使用的示例abs(random - 1.f) < numeric_limits<float>::epsilon检查结果是否接近1.0,在这种情况下这是完全错误的:这里有接近1.0的数字是有效的结果,即所有小于1.0的数字。
R. Martinho Fernandes 2014年

4
@Galik是的,实现它会遇到麻烦。但是那麻烦是实现者要处理的。用户永远不能看到1.0,并且用户必须始终看到所有结果的均等分布。
R. Martinho Fernandes 2014年

Answers:


121

问题在于从std::mt19937std::uint_fast32_t)的共域到float;的映射。如果当前IEEE754舍入模式不是round-to-negative-infinity(除了取整到负无穷大)之外的其他任何值(请注意默认值为round),则当精度下降时,标准描述的算法会给出错误的结果(与算法输出的描述不一致)。 -最接近)。

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

该标准可以确保正确的行为,如果它是指定从革联的输出转换到时RealTypegenerate_canonical,舍入将被向负无穷执行; 在这种情况下,这将给出正确的结果。作为QOI,libstdc ++进行此更改将是很好的。

进行此更改后,1.0将不再生成;代替边界值0x1.fffffep-N0 < N <= 8将被更频繁地产生(大约2^(8 - N - 32)N取决于MT19937的实际分布)。

我会建议不要使用floatstd::generate_canonical直接; 而是生成in的数字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


2
@user的实现质量-使一个一致性实现比另一个一致性更好的所有方面,例如性能,边缘情况下的行为,错误消息的帮助。
ecatmur 2014年

2
@supercat:有点离题,实际上,有充分的理由尝试使小角度的正弦函数尽可能准确,例如,因为sin(x)中的小误差会变成sin(x)/ x中的大误差(当x接近零时,在实际计算中会经常发生。π倍数附近的“超精确度”通常只是其副作用。
Ilmari Karonen 2014年

1
@IlmariKaronen:对于足够小的角度,sin(x)就是x。我对Java正弦函数的抱怨是角度接近pi的倍数。我会假设,有99%的时间,当代码要求时sin(x),它真正想要的是(π/ Math.PI)乘以x的正弦值。维护Java的人坚持认为,慢速数学例程报告Math.PI的正弦值是π与Math.PI之间的差,而不是让它报告一个稍小的值,尽管在99%的应用程序中会更好...
supercat

3
@ecatmur建议;更新此帖子以提及std::uniform_real_distribution<float>由于此而遭受相同问题的问题。(这样,搜索“ uniform_real_distribution”的人员就会收到此“问题/答案”)。
MM

1
@ecatmur,我不确定为什么要舍入为负无穷大。既然generate_canonical应该生成一个range范围内的数字[0,1),而我们正在谈论的是它偶尔会生成1.0的错误,那么四舍五入是否会同样有效呢?
马歇尔(Marshall)Clow 2015年

39

根据标准,1.0无效。

C ++ 11§26.5.7.2函数模板generate_canonical

从本节26.5.7.2中描述的模板实例化的每个函数都将提供的统一随机数生成器的一个或多个调用的结果映射g到指定RealType的一个成员,这样,如果由产生的值g ig被均匀分布,则实例化的结果t j0≤t j <1,如下所述尽可能均匀地分布。


25
+1我在OP的程序中看不到任何缺陷,因此我将其称为libstdc ++和libc ++错误……这本身似乎不太可能,但是我们可以了。
Lightness Races in Orbit

-2

我只是与遇到了类似的问题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.0f2 ^ 25倍中的1倍。我对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 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个尾数位)时,我们期望看到uniform_real_distribution<double>(0,1)(g) == 1.02 ^ 54次中的大约1倍。我没有耐心来检验这种期望。:)

我的理解是这种行为很好。它可能会得罪我们的“半开放式rangeness”意义上,一个自称分配“小于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。您只是无法摆脱它;那么为什么我们会在这个单一实例中假装您可以呢?


2
我非常不同意这种观点。如果标准规定了半开间隔的值,并且实现违反了该规则,则说明实现错误。不幸的是,正如ecatmur在其答案中正确指出的那样,该标准还规定了存在错误的算法。这在这里也得到正式认可:open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
cschwan

@cschwan:我的解释是实现没有违反规则。该标准规定了[0,1)中的值;实现返回[0,1)中的值;这些值中的一些恰好符合IEEE,1.0f但是当您将它们强制转换为IEEE浮点数时,这是不可避免的。如果需要纯数学结果,请使用符号计算系统。如果您试图使用IEEE浮点数表示eps1 之内的数字,则您处于犯罪状态。
Quuxplusone

可能会被该错误打破的假想示例:将某物除以canonical - 1.0f。对于中的每个可表示的浮点数[0, 1.0)x-1.0f都为非零。精确地使用1.0f,您可以得到除以零的结果,而不仅仅是很小的除数。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.