【问题标题】:18 trillion coin tosses, where did I go wrong? [closed]18 万亿次抛硬币,我哪里做错了? [关闭]
【发布时间】:2016-06-15 17:43:18
【问题描述】:

为什么下面的 C 代码在我的桌面和服务器上给出不同的结果,两者都运行类似的 Linux 版本?

它在 18 万亿次抛硬币中找到连续序列中最长的同一边。 [参见 Iain M. Banks 的科幻小说考虑 Phlebas。]

在服务器上,经过 15.7 万亿次硬币抛掷(它仍在运行),到目前为止,行序列中最长的同侧序列只有 29 个。由于2^44 = 17,592,186,044,416,我希望最长的同侧序列在某处40 年代中期到 40 年代中期,在 18 万亿美元全部完成后可能是 44 年代。

在桌面上仅抛了 47 亿次硬币之后,最长的序列已经是 31,因为2^31 = 2,147,483,648,这听起来很正确。

那么为什么我在抛硬币 15.7 万亿次后在服务器上只有 29 个序列,而在我的桌面上只在 47 亿次后得到了 31 个序列?

模数偏差是我的第一个想法。 RAND_MAX 在桌面和服务器上都是相同的,2,147,483,647(32 位有符号长)。所以rand() 函数会给我一个数字0 <= rand() <= 2,147,483,647。 0 是偶数,2,147,483,647 是奇数,所以除非我错了,否则我的int rand_num = (rand() % 2); 代码行没有引入模偏差。

我知道 C 标准库的伪随机数生成器不适合加密。当然,在生成零和一的序列时,这肯定不是一个因素,诚然确实相当长。可以吗?

来源:

在两台机器上编译使用:gcc -O3 -o 18TCT 18TrillionCoinTosses.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char* argv[])
{
    srand(time(NULL));

    int current_seq = 0;
    int longest_seq = 0;
    int prev_rand_num = -1;

    long long i = 0;
    long long total = 18000000000000;

    // To serve as a rudimentary progress indicator.
    long billion_counter = 0;
    long billion = 1000000000;

    while (i < total)
    {
        int rand_num = (rand() % 2);

        if (rand_num == prev_rand_num)
        {
            current_seq++;

            if (current_seq >= longest_seq)
            {
                longest_seq = current_seq;
                printf("Longest sequence so far: %d (on iteration %lli)\n", longest_seq, i);
            }
        }
        else
            current_seq = 1;

        if (billion_counter == billion)
        {
            billion_counter = 0;
            printf("Progress report, current iteration: %lli\n", i);
        }

        prev_rand_num = rand_num;

        i++;
        billion_counter++;
    }

    printf("\nTotal coins tossed: %lli\n", i);
    printf("Longest sequence: %d\n", longest_seq);
}

【问题讨论】:

  • TL;博士。不要写小说。见How to Ask
  • 看起来问题是“为什么服务器和笔记本电脑之间的输出不同?”其余的 99% 都是绒毛。
  • 老实说,我喜欢阅读。
  • 我也很喜欢它,如果您花时间理解它,那不是“为什么服务器和笔记本电脑之间的输出不同?”。
  • rand() 没有定义的实现规范,因此实现无处不在。许多实现是低性能线性同余生成器。使用rand() 几乎肯定是个问题,我建议尝试使用更好的生成器,例如Mersenne TwisterWELL

标签: c random


【解决方案1】:

正如其他人所指出的,rand 不是随机性的可靠来源。就在the man page

NAME
     rand, rand_r, srand, sranddev -- bad random number generator

...

DESCRIPTION
     These interfaces are obsoleted by arc4random(3).

为了获得良好的随机性,您必须跳出标准 C 库。

请注意,如果您使用的是 Mac,它会抱怨 RAND_bytes() 已被弃用。不用担心,OpenSSL 不会去任何地方并且可以很好地使用。 The deprecation has to do with binary compatibility issues when upgrading Apple products.

【讨论】:

  • 许多rand() 的实现确实很差,而且确实太短的重复周期很可能是OP 的实际问题。但我想指出RAND_MAX 与RNG 的时期无关。任何旧的rand() 实现都可能重复一个比RAND_MAX 长得多的周期。
  • @SteveSummit 确实,在我的回答中,MSVC 的RAND_MAX32767,但序列重复得更慢。
  • @SteveSummit 你是对的!我将返回值的大小与其可能重复的周期混淆了。
  • 我还要指出,从某种意义上说,这里的正确答案也是错误的答案。是的,OP 的 RNG 不足以满足他的需求,因此需要使用更好的 RNG。但是,他不得不更改代码以调用rand() 以外的其他名称,这真是令人遗憾!模块化的全部意义在于,您可以在同一接口后面加入更好的实现!不,C 标准不保证高质量的rand(),但它当然也不要求低质量的!为什么更多的 libc 作者不能写出高质量的 RNG 并称它们为老旧的rand()
  • @SteveSummit 我听到了。感叹语言标准和实现的选择以及它们倾向于支持不良、甚至危险但兼容的行为是一项全职工作。 ;)
【解决方案2】:

您的代码似乎没问题。问题可能出在您使用的 RNG 上。

我不认为 rand() % 2 是统一的。看看这里: Uniformity of random numbers taken modulo N

为什么不用 C++11 随机数生成器?http://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution

最后但并非最不重要的一点是,-O3 会不会搞砸一些事情?

-O3 进一步优化。 -O3 打开 -O2 指定的所有优化,同时打开 -finline-functions、-funswitch-loops、-fpredictive-commoning、-fgcse-after-reload、-ftree-loop-vectorize、-ftree-loop-distribute -patterns、-fsplit-paths -ftree-slp-vectorize、-fvect-cost-model、-ftree-partial-pre 和 -fipa-cp-clone 选项。

【讨论】:

  • 优化设置不会影响随机数生成。
  • rand() % 2 对于所有实际用途来说足够统一。如果RAND_MAX 是偶数,那么rand() % 2 将是完全一致的,否则它将在RAND_MAX 中减少一部分。 (现在,rand() % 2 确实可能存在 distribution 问题,但这似乎也不是问题所在。)
  • @steve:但在这里我们不仅仅关注频率。交替偶数和奇数的 PRNG 在频率上可能完全没有偏差,但如果您计算重复的 r%2 值,它会非常有偏差。这正是主题。
  • @rici:我想我们说的是同一件事。但很明显,OP 没有遇到他的系统的rand() 实现是偶数和奇数交替的问题,因为如果这样做,他将永远不会得到 任何 运行全部正面或全部反面。
  • @steve:没错。问题在于 gnu 三项式生成器与第 31 个先前值有很强的相关性。这个例子只是一个例子;无偏(按频率)rngs 中存在更微妙的序列偏差
【解决方案3】:

您的随机数生成器可能在 2^32 = 4294967296 次调用后重复,因此您并没有真正模拟 18 万亿次试验。您需要一个更好的 RNG,它可以保留超过 32 位的内部状态。在许多系统上,您可以通过简单地调用random() 而不是rand() 来访问更好的RNG。 (在我的系统上,man random 说“随机——更好的随机数生成器”和“这个随机数生成器的周期非常大,大约 16*((2**31)-1)”。虽然那是“只有" 34,359,738,352,还差你的 18 万亿。)

另外,顺便说一句,rand() % 2 是有风险的,尽管现在大多数 RNG 都没有会烧死你的问题(如果你确实有这个问题,你会知道的,因为除其他外无论如何你都会连续得到 0 的东西)。


附录:您可以在 C 常见问题列表中的问题 13.15 中找到对其他一些更好的随机数生成器的引用:http://c-faq.com/lib/rand.html

【讨论】:

    【解决方案4】:

    即使您的“随机”位 0 具有相同的 0 和 1,伪随机生成器函数 rand() 序列也会相对频繁地重复。在我的测试中,它在循环的 2147483648 (2**31) 次迭代后重复。因此,达到 18 万亿是没有意义的。我测试了好几次,结果都是一样的。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(void)
    {
        unsigned long long n = 0;
        int a, b, c, d;
        int e, f, g, h;
    
        srand((unsigned)time(NULL));
        e = a = rand();
        f = b = rand();
        g = c = rand();
        h = d = rand();
        do {
            n++;
            e = f;
            f = g;
            g = h;
            h = rand();
        } while (e != a || f != b || g != c || h != d);
        printf("%llu\n", n);
    }
    

    【讨论】:

    • 没有生成哪些 65,538 个值?
    • @rici 它可能是 65,536,因为我正在测试重复的三个值的序列。当我检查四个重复值的序列时,循环的迭代次数减少了一次。
    • 它仍然很好奇,你不觉得吗?您使用的是 GNU 三项式生成器,还是一些线性同余变体? GNU 生成器有一个相当大的状态,所以它应该有一个比 RANDMAX 大的循环长度,而不是我曾经测试过它。
    • @rici 是的,我没有正确计数,因为当a 匹配时,我在循环中重复了rand()。我已经重写并发布了 FWIW。