【问题标题】:Is 1.0 a valid output from std::generate_canonical?1.0 是 std::generate_canonical 的有效输出吗?
【发布时间】:2014-10-29 09:17:48
【问题描述】:

我一直认为随机数会介于零和一之间,没有1,即它们是来自半开区间 [0,1) 的数字。 std::generate_canonicaldocumention on 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

和clang 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 &amp;&amp; ./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

【问题讨论】:

  • @David Lively 1.f == 1.f 在所有情况下(所有情况下都有什么?我什至没有在1.f == 1.f 中看到任何变量;这里只有一个情况:1.f == 1.f,那就是总是true)。请不要进一步传播这个神话。浮点比较总是精确的。
  • @DavidLively:不,不是。比较总是准确的。您的操作数可能不准确如果它们是计算出来的而不是文字。
  • @Galik 任何低于 1.0 的正数都是有效结果。 1.0 不是。就这么简单。舍入无关:代码得到一个随机数并且不对它执行任何舍入。
  • @DavidLively 他说只有一个值比较等于 1.0。该值为 1.0。接近 1.0 的值不等于 1.0。生成函数的作用无关紧要:如果它返回 1.0,它将比较等于 1.0。如果它不返回 1.0,它不会比较等于 1.0。您使用 abs(random - 1.f) &lt; numeric_limits&lt;float&gt;::epsilon 的示例检查结果是否接近 1.0,这在这种情况下是完全错误的:这里有接近 1.0 的数字是有效结果,即所有小于1.0.
  • @Galik 是的,实现起来会有问题。但是这个麻烦是由实施者来处理的。用户决不能看到 1.0,而且用户必须始终看到所有结果的均等分布。

标签: c++ c++11 random


【解决方案1】:

问题在于从std::mt19937 (std::uint_fast32_t) 的共域映射到float;如果当前 IEEE754 舍入模式不是舍入到负无穷大(注意默认值是舍入- 到最近)。

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

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

进行此更改后,1.0 将不再生成;相反,0 &lt; N &lt;= 8 的边界值 0x1.fffffep-N 将更频繁地生成(大约为 2^(8 - N - 32)N,取决于 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&lt;float&gt; 也可能出现此问题;解决方案是相同的,在double 上专门分配分布并将结果向float 中的负无穷大舍入。

【讨论】:

  • @user 实施质量 - 使一种符合要求的实施比另一种更好的所有因素,例如性能、边缘情况下的行为、错误消息的有用性。
  • @supercat:离题一点,实际上有充分的理由尝试使正弦函数在小角度上尽可能准确,例如因为当 x 接近于零时,sin(x) 中的小错误可能会变成 sin(x)/x 中的大错误(occurs quite often 在实际计算中)。 π 倍数附近的“额外精度”通常只是其副作用。
  • @IlmariKaronen:对于足够小的角度,sin(x) 就是 x。我对 Java 的正弦函数的抱怨与角度接近 pi 的倍数有关。我假设 99% 的情况下,当代码请求 sin(x) 时,它真正想要的是 (π/Math.PI) 乘以 x 的正弦值。维护 Java 的人坚持认为,最好有一个缓慢的数学例程报告 Math.PI 的正弦是 π 和 Math.PI 之间的差,而不是让它报告一个略小的值,尽管在 99% 的应用程序中它会更好...
  • @ecatmur 建议;更新此帖子以提及 std::uniform_real_distribution&lt;float&gt; 也因此而遭受同样的问题。 (这样搜索 uniform_real_distribution 的人就会看到这个 Q/A)。
  • @ecatmur,我不确定你为什么要向负无穷大取整。由于generate_canonical 应该在[0,1) 范围内生成一个数字,而我们正在讨论它偶尔会生成 1.0 的错误,所以向零舍入不是同样有效吗?
【解决方案2】:

根据标准,1.0 无效。

C++11 §26.5.7.2 函数模板 generate_canonical

从本节 26.5.7.2 中描述的模板实例化的每个函数都将提供的统一随机数生成器g 的一次或多次调用的结果映射到指定 RealType 的一个成员,这样,如果值 gg 产生的 >i 是均匀分布的,实例化的结果 tj , 0 ≤ tj 是尽可能均匀分布,如下所示。

【讨论】:

  • +1 我在 OP 的程序中看不到任何缺陷,所以我将其称为 libstdc++ 和 libc++ 错误……这本身似乎不太可能,但我们开始了。跨度>
【解决方案3】:

我刚刚遇到了与uniform_real_distribution 类似的问题,以下是我对标准关于该主题的简洁措辞的解释:

标准总是根据 math 定义数学函数,从不根据 IEEE 浮点(因为标准仍然假装浮点 可能不 表示 IEEE浮点)。因此,每当您在标准中看到数学用语时,它都是在谈论真正的数学,而不是 IEEE。

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

Tfloat(24 个尾数位)时,我们预计会看到uniform_real_distribution&lt;float&gt;(0,1)(g) == 1.0f 大约2^25 次中的1。 My brute-force experimentation with libc++ confirms this expectation.

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&lt;double&gt;(0,1)(g) == 1.0 大约2^54 次中的1 个。我没有耐心去检验这个期望。 :)

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

无论您将随机数输入何种算法,都不会在意它是否有时会恰好得到1.0。除了数学运算之外,您无法执行浮点数,并且一旦您执行一些数学运算,您的代码将不得不处理舍入。即使您可以合理地假设generate_canonical&lt;float,1000&gt;(g) != 1.0f,您仍然不能假设generate_canonical&lt;float,1000&gt;(g) + 1.0f != 2.0f——因为四舍五入。你就是无法摆脱它;那么我们为什么要在这个单一的例子中假装你可以呢?

【讨论】:

  • 我强烈反对这种观点。如果标准规定了半开区间的值并且实施违反了此规则,则实施是错误的。不幸的是,正如 ecatmur 在他的回答中正确指出的那样,该标准还规定了存在错误的算法。这也是官方认可的:open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2524
  • @cschwan:我的解释是实现没有违反规则。该标准规定了 [0,1) 中的值;实现从 [0,1) 中返回值;其中一些值恰好符合 IEEE 1.0f,但是当您将它们转换为 IEEE 浮点数时,这是不可避免的。如果您想要纯数学结果,请使用符号计算系统;如果您尝试使用 IEEE 浮点数来表示在 1 的 eps 范围内的数字,那么您就处于罪恶状态。
  • 会被此错误破坏的假设示例:除以 canonical - 1.0f。对于[0, 1.0) 中的每个可表示浮点数,x-1.0f 都是非零的。使用恰好 1.0f,您可以获得被零除,而不仅仅是一个非常小的除数。
猜你喜欢
  • 1970-01-01
  • 2011-03-04
  • 2018-08-25
  • 2020-07-29
  • 2019-08-04
  • 2017-01-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多