当前位置: 首页 > 知识库问答 >
问题:

为什么新的随机库比std::rand()好?

阳修永
2023-03-14

但是,我想直接看到std::rand()的缺点,所以我做了一个快速的实验:

  1. 基本上,我编写了两个函数getrandnum_old()getrandnum_new(),它们分别使用std::rand()std::mt19937+std::uniform_int_distribution生成0和5之间的随机数。
  2. 然后我用“老”的方式生成了960,000个(可被6整除)随机数,并记录了数字0-5的频率。然后我计算了这些频率的标准差。我所寻找的是一个尽可能低的标准偏差,因为如果分布是真正均匀的,那么就会发生这种情况。
  3. 我运行该模拟1000次,并记录每次模拟的标准偏差。我还记录了以毫秒为单位的时间。
  4. 之后,我又做了一次完全相同的操作,但这次是以“新”的方式生成随机数。
  5. 最后,我计算了新旧方法的标准差列表的平均值和标准差,以及新旧方法的时间列表的平均值和标准差。

结果如下:

[OLD WAY]
Spread
       mean:  346.554406
    std dev:  110.318361
Time Taken (ms)
       mean:  6.662910
    std dev:  0.366301

[NEW WAY]
Spread
       mean:  350.346792
    std dev:  110.449190
Time Taken (ms)
       mean:  28.053907
    std dev:  0.654964

我的实验有什么缺陷吗?还是std::rand()真的没有那么差,甚至可能更好?

这里是我全部使用的代码供参考:

#include <cstdio>
#include <random>
#include <algorithm>
#include <chrono>

int getRandNum_Old() {
    static bool init = false;
    if (!init) {
        std::srand(time(nullptr)); // Seed std::rand
        init = true;
    }

    return std::rand() % 6;
}

int getRandNum_New() {
    static bool init = false;
    static std::random_device rd;
    static std::mt19937 eng;
    static std::uniform_int_distribution<int> dist(0,5);
    if (!init) {
        eng.seed(rd()); // Seed random engine
        init = true;
    }

    return dist(eng);
}

template <typename T>
double mean(T* data, int n) {
    double m = 0;
    std::for_each(data, data+n, [&](T x){ m += x; });
    m /= n;
    return m;
}

template <typename T>
double stdDev(T* data, int n) {
    double m = mean(data, n);
    double sd = 0.0;
    std::for_each(data, data+n, [&](T x){ sd += ((x-m) * (x-m)); });
    sd /= n;
    sd = sqrt(sd);
    return sd;
}

int main() {
    const int N = 960000; // Number of trials
    const int M = 1000;   // Number of simulations
    const int D = 6;      // Num sides on die

    /* Do the things the "old" way (blech) */

    int freqList_Old[D];
    double stdDevList_Old[M];
    double timeTakenList_Old[M];

    for (int j = 0; j < M; j++) {
        auto start = std::chrono::high_resolution_clock::now();
        std::fill_n(freqList_Old, D, 0);
        for (int i = 0; i < N; i++) {
            int roll = getRandNum_Old();
            freqList_Old[roll] += 1;
        }
        stdDevList_Old[j] = stdDev(freqList_Old, D);
        auto end = std::chrono::high_resolution_clock::now();
        auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
        double timeTaken = dur.count() / 1000.0;
        timeTakenList_Old[j] = timeTaken;
    }

    /* Do the things the cool new way! */

    int freqList_New[D];
    double stdDevList_New[M];
    double timeTakenList_New[M];

    for (int j = 0; j < M; j++) {
        auto start = std::chrono::high_resolution_clock::now();
        std::fill_n(freqList_New, D, 0);
        for (int i = 0; i < N; i++) {
            int roll = getRandNum_New();
            freqList_New[roll] += 1;
        }
        stdDevList_New[j] = stdDev(freqList_New, D);
        auto end = std::chrono::high_resolution_clock::now();
        auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
        double timeTaken = dur.count() / 1000.0;
        timeTakenList_New[j] = timeTaken;
    }

    /* Display Results */

    printf("[OLD WAY]\n");
    printf("Spread\n");
    printf("       mean:  %.6f\n", mean(stdDevList_Old, M));
    printf("    std dev:  %.6f\n", stdDev(stdDevList_Old, M));
    printf("Time Taken (ms)\n");
    printf("       mean:  %.6f\n", mean(timeTakenList_Old, M));
    printf("    std dev:  %.6f\n", stdDev(timeTakenList_Old, M));
    printf("\n");
    printf("[NEW WAY]\n");
    printf("Spread\n");
    printf("       mean:  %.6f\n", mean(stdDevList_New, M));
    printf("    std dev:  %.6f\n", stdDev(stdDevList_New, M));
    printf("Time Taken (ms)\n");
    printf("       mean:  %.6f\n", mean(timeTakenList_New, M));
    printf("    std dev:  %.6f\n", stdDev(timeTakenList_New, M));
}

共有1个答案

郎刚捷
2023-03-14

几乎任何“旧”rand()实现都使用LCG;虽然他们通常不是最好的发电机周围,通常你不会看到他们失败这样一个基本的测试-均值和标准差通常是正确的,即使是最坏的PRNGs。

“坏”(但也足够常见)的rand()实现的常见故障是:

  • 低阶位的低随机性;
  • 短周期;
  • 低电平RAND_MAX
  • 连续提取之间的某些相关性(通常,LCG产生的数字位于有限数量的超平面上,尽管这种情况可以得到某种程度的缓解)。

编辑:@R。正确地注意到,rand/srand接口受到以下事实的限制,即srand采用一个无符号的int,因此实现可能放在它们后面的任何生成器本质上都限于uint_max可能的起始种子(以及由此生成的序列)。这确实是正确的,尽管API可以稍加扩展,使srand采用无符号长,或者添加单独的srand(unsigned char*,size_t)重载。

实际上,rand()的实际问题不是原则上的实现问题,而是:

>

  • 向下兼容;许多当前的实现使用次优的发生器,典型地具有错误选择的参数;一个臭名昭著的例子是Visual C++,它运动的rand_max只有32767。但是,这不能轻易地改变,因为这会破坏与过去的兼容性--使用srand并带有固定种子以进行可复制模拟的人不会太高兴(实际上,IIRC前面提到的实现可以追溯到80年代中期的Microsoft C早期版本--甚至是Lattice C);
  • 简化界面;rand()为整个程序提供具有全局状态的单个生成器。虽然这对于许多简单的用例来说是非常好的(实际上也非常方便),但它也带来了一些问题:

    • 使用多线程代码:要修复它,您要么需要一个全局互斥体(这会无缘无故地减慢所有的速度,并扼杀任何可重复性的机会,因为调用序列本身变得随机),要么需要线程本地状态;几个实现(特别是Visual C++)都采用了最后一个;
    • 如果您希望将一个“私有”的、可复制的序列放入程序的特定模块中,并且不影响全局状态。

    最后,rand状态:

    • 没有指定实际的实现(C标准提供的只是示例实现),因此任何旨在跨不同编译器产生可复制输出(或期望具有某种已知质量的PRNG)的程序都必须滚动它自己的生成器;
    • 没有提供任何跨平台方法来获得合适的种子(time(NULL)不是,因为它不够粒度,而且通常--想想没有RTC的嵌入式设备--甚至不够随机)。
      null

    ...以及默认的random_device来种子它们。

    现在,如果你问我,我也希望在这个基础上构建一个简单的API,用于“简单的”、“猜一个数字”的情况(类似于Python提供“复杂的”API,但也可以使用简单的random.randint&Co.为我们这些不复杂的人使用一个全局的、预先播种的PRNG,他们不希望每次我们想要提取宾果牌的数字时都淹没在随机设备/引擎/适配器/任何东西中),但确实,您可以通过当前的工具轻松地构建它(而通过一个简单的工具构建“完整的”API是不可能的)。

    最后,回到您的性能比较:正如其他人所指定的,您正在比较一个快速的LCG和一个较慢的(但通常被认为质量更好的)梅森扭转器;如果您对LCG的质量没有问题,可以使用std::minstd_rand代替std::mt19937

    实际上,在调整函数以使用std::minstd_rand并避免用于初始化的无用静态变量之后

    int getRandNum_New() {
        static std::minstd_rand eng{std::random_device{}()};
        static std::uniform_int_distribution<int> dist{0, 5};
        return dist(eng);
    }
    

    我得到9毫秒(旧)和21毫秒(新);最后,如果我去掉dist(与经典的模运算符相比,它处理的是输出范围的分布偏斜,而不是输入范围的倍数),然后回到您在getrandnum_old()中所做的工作

    int getRandNum_New() {
        static std::minstd_rand eng{std::random_device{}()};
        return eng() % 6;
    }
    

    我将其压缩到6 ms(因此,快了30%),这可能是因为与对rand()调用不同,std::minstd_rand更容易内联。

  •  类似资料:
    • 我想知道为什么c标准要求只接受随机访问迭代器?我不认为这有什么好处,因为std::sort和std::list::sort的复杂性都是。将限制为随机访问迭代器(RAI),似乎需要为具有相同复杂性的列表编写单独的函数。 同样的情况也适用于,其中列表的非RAI计数器部分至今仍然缺失。 这种设计是因为历史上人们使用了的变体来实现? 如果在RAI容器上编写排序算法有好处,那么最好使更通用,并让像这样的RA

    • 本文向大家介绍php中随机函数mt_rand()与rand()性能对比分析,包括了php中随机函数mt_rand()与rand()性能对比分析的使用技巧和注意事项,需要的朋友参考一下 本文实例对比分析了php中随机函数mt_rand()与rand()性能问题。分享给大家供大家参考。具体分析如下: 在php中mt_rand()和rand()函数都是可以随机生成一个纯数字的,他们都是需要我们设置好种子

    • 问题内容: 有没有什么方法可以模拟Collections.shuffle的行为,而比较器不容易受到排序算法实现的影响,从而确保结果安全? 我的意思是不违反可比合同等。 问题答案: 不打破合同就不可能实现真正的“改组比较器”。合同的一个基本方面是,结果是可 重现的, 因此必须确定特定实例的顺序。 当然,您可以使用混洗操作预先初始化该固定顺序,并创建一个比较器来精确地建立此顺序。例如 虽然没有意义。显

    • 是否有任何方法可以模拟Collections.shuffle的行为,而比较器不容易受到排序算法实现的影响,以确保结果安全? 我的意思是不违反类似的合同等..

    • 我正在研究一种算法,它需要以尽可能快的速度生成数百万个数字。实际上,我发现我的算法的rand()函数占用了75%的处理时间。 所以我在找更快的东西。而且我根本不需要大范围。(我只需要1000以下的整数) 你知道我需要什么吗? 谢啦! 编辑: 我使用这个数字来洗牌少于1000个实体的组。 我发现了更多关于“快速兰特”的信息。还有SSE版本,它更快,一次生成4个数字。 https://software