c++ 随机引擎差异

jqjz2hbq  于 2023-02-17  发布在  其他
关注(0)|答案(7)|浏览(585)

C++11标准为随机数生成指定了许多不同的引擎:linear_congruential_enginemersenne_twister_enginesubtract_with_carry_engine等等,显然,这与std::rand的旧用法相比是一个很大的变化。
显然,这些引擎的主要优点之一(至少是一些)是周期长度的大幅增加(它内置在std::mt19937的名称中)。
然而,引擎之间的区别并不明显。不同引擎的优缺点是什么?何时应该使用一个而不是另一个?是否有一个通常应该首选的合理默认值?

cwxwcias

cwxwcias1#

从下面的解释来看,线性引擎似乎更快,但随机性更低,而梅森扭曲器具有更高的复杂性和随机性。减法与进位随机数引擎是对线性引擎的改进,它肯定更具随机性。在上一篇参考文献中,梅森扭曲器具有比减法与进位随机数引擎更高的复杂性。

    • 线性同余随机数引擎**

产生无符号整数的伪随机数生成器引擎。
这是标准库中最简单的生成器引擎。它的状态是单个整数值,转换算法如下:

x = (ax+c) mod m

其中x是当前状态值,ac是它们各自的模板参数,并且如果m大于0,则m是其各自的模板参数,否则numerics_limits<UIntType>::max() + 1是其各自的模板参数。
其生成算法是状态值的直接拷贝。
这使得它在处理和内存消耗方面成为一个非常高效的生成器,但生成的数字具有不同程度的序列相关性,这取决于所使用的特定参数。
linear_congruential_engine生成的随机数具有m的周期。

    • 梅森扭曲随机数引擎**

一个伪随机数生成器引擎,生成闭区间[0,2^w-1]中的无符号整数。
此引擎使用的算法经过优化,可以计算在范围内几乎均匀分布的大型数字序列(如在蒙特卡罗实验中)。
该引擎具有n整数元素的内部状态序列,该序列由构造时或通过调用成员函数seed生成的伪随机序列填充。
内部状态序列成为n元素的源:当状态前进时(例如,为了生成新的随机数),引擎通过使用异或掩码a扭曲当前值来更改状态序列,其中异或掩码r是由参数r确定的位的混合,这些位来自该值和远离m个元素的值(有关详细信息,请参见operator())。
所产生的随机数是这些扭曲值的回火版本。回火是由应用于所选状态值的参数udsbtcl定义的移位和异或运算序列(参见operator())。
mersenne_twister_engine生成的随机数的周期等于梅森数2^((n-1)*w)-1
∮ ∮ ∮ ∮
产生无符号整数的伪随机数生成器引擎。
此引擎使用的算法是滞后Fibonacci生成器,具有r个整数元素的状态序列加上一个进位值。
如果使用加法或减法,Lagged Fibonacci generators的最大周期为(2k - 1)*^(2M-1)。LFG的初始化是一个非常复杂的问题。LFG的输出对初始条件非常敏感,并且统计缺陷可能在初始时出现,但也可能在输出序列中周期性地出现,除非特别小心。LFG的另一个潜在问题是其背后的数学理论不完整,使得必须依赖于统计测试而不是理论性能。
最后是documentation of random
选择使用哪种引擎涉及许多权衡:线性同余引擎速度适中并且对状态的存储要求非常小。滞后斐波那契生成器即使在没有高级算术指令集的处理器上也非常快,以更大的状态存储和有时不太理想的光谱特性为代价。梅森扭转器速度较慢,有更大的状态存储要求,但有正确的参数有最长的非线性具有最理想光谱特性的重复序列(对于给定的理想定义)。

nvbavucw

nvbavucw2#

我认为关键在于随机生成器有不同的属性,这可能使它们更适合或不适合给定的问题。

*周期长度是属性之一。

  • 随机数的质量也可能是重要的。
  • 发电机的性能也可能是一个问题。

根据您的需要,您可以选择一个生成器或另一个生成器。例如,如果您需要快速随机数,但并不真正关心质量,LCG可能是一个很好的选择。如果您想要质量更好的随机数,Mersenne Twister可能是一个更好的选择。
为了帮助您做出选择,这里有一些标准测试和结果(我非常喜欢this paper的表p.29)。
编辑:从报纸上看,

  1. LCG(本文中为LCG(***))系列是速度最快的生成器,但质量最差。
  2. Mersenne Twister(MT19937)稍慢,但产生的随机数更好。
    1.带进位的减法器(我认为是SWB(***))速度要慢得多,但如果调整得当,可以产生更好的随机特性。
jv2fixgn

jv2fixgn3#

当其他人忘记了ranlux时,这里有一位AMD开发人员最近将其移植到OpenCL的一个小注解:
https://community.amd.com/thread/139236
RANLUX也是极少数(实际上是我所知道的唯一一个)PRNG有一个潜在的理论来解释为什么它会产生“随机”数字,以及为什么它们是好的。事实上,如果理论是正确的(我不知道有谁对此有异议),RANLUX在最高的豪华级别产生完全去相关的数字,直到最后一位,只要我们保持在周期(10^171)以下,就没有长期相关性。大多数其他发生器对它们的质量几乎没有什么可说的(如梅森龙卷风,KISS等)。它们必须依赖于通过统计测试。
“欧洲核子研究中心的物理学家是PRNG的粉丝。”纳夫说。

l3zydbqr

l3zydbqr4#

其他答案中的一些信息与我的发现相冲突。我使用Visual Studio 2013在Windows 8.1上运行了测试,一致发现mersenne_twister_enginelinear_congruential_enginesubtract_with_carry_engine的质量更高,速度也明显更快。这让我相信,当考虑到其他答案中的信息时,发动机的具体实施对性能具有显著影响。
我敢肯定,没有人会对此感到惊讶,但是在其他回答中没有提到mersenne_twister_engine的速度较慢。我没有其他平台和编译器的测试结果,但是根据我的配置,在考虑周期、质量和速度性能时,mersenne_twister_engine显然是上级的选择。我没有分析内存使用情况,所以我不能谈论空间要求属性。
下面是我用来测试的代码(要使其具有可移植性,只需将windows.h QueryPerformanceXxx() API调用替换为适当的计时机制):

// compile with: cl.exe /EHsc
#include <random> 
#include <iostream>
#include <windows.h>

using namespace std;

void test_lc(const int a, const int b, const int s) {
    /*
    typedef linear_congruential_engine<unsigned int, 48271, 0, 2147483647> minstd_rand;
    */
    minstd_rand gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_mt(const int a, const int b, const int s) {
    /*
    typedef mersenne_twister_engine<unsigned int, 32, 624, 397,
    31, 0x9908b0df,
    11, 0xffffffff,
    7, 0x9d2c5680,
    15, 0xefc60000,
    18, 1812433253> mt19937;
    */
    mt19937 gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

void test_swc(const int a, const int b, const int s) {
    /*
    typedef subtract_with_carry_engine<unsigned int, 24, 10, 24> ranlux24_base;
    */
    ranlux24_base gen(1729);

    uniform_int_distribution<> distr(a, b);

    for (int i = 0; i < s; ++i) {
        distr(gen);
    }
}

int main()
{
    int a_dist = 0;
    int b_dist = 1000;

    int samples = 100000000;

    cout << "Testing with " << samples << " samples." << endl;

    LARGE_INTEGER ElapsedTime;
    double        ElapsedSeconds = 0;

    LARGE_INTEGER Frequency;
    QueryPerformanceFrequency(&Frequency);
    double TickInterval = 1.0 / ((double) Frequency.QuadPart);

    LARGE_INTEGER StartingTime;
    LARGE_INTEGER EndingTime;
    QueryPerformanceCounter(&StartingTime);
    test_lc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "linear_congruential_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_mt(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "   mersenne_twister_engine time: " << ElapsedSeconds << endl;

    QueryPerformanceCounter(&StartingTime);
    test_swc(a_dist, b_dist, samples);
    QueryPerformanceCounter(&EndingTime);
    ElapsedTime.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
    ElapsedSeconds = ElapsedTime.QuadPart * TickInterval;
    cout << "subtract_with_carry_engine time: " << ElapsedSeconds << endl;
}

输出:

Testing with 100000000 samples.
linear_congruential_engine time: 10.0821
   mersenne_twister_engine time: 6.11615
subtract_with_carry_engine time: 9.26676
5vf7fwbs

5vf7fwbs5#

我刚看到Marnos的this answer,决定自己测试一下。我用std::chono::high_resolution_clock100000个样本进行100次计时,得出一个平均值。我用std::chrono::nanoseconds测量了所有的东西,但最终得到了不同的结果:
std::minstd_rand具有28991658纳秒的平均值
std::mt19937具有29871710纳秒的平均值
ranlux48_base的平均值为29281677纳秒
这是在Windows 7机器上。编译器是Mingw-Builds 4.8.1 64位。这显然使用了C++11标志,没有优化标志。
当我打开-O3优化时,std::minstd_randranlux48_base实际上运行得比high_precision_clock的实现所能测量的要快;然而,std::mt19937仍然花费730045纳秒或3/4秒。
所以,正如他所说,这是具体实现的,但至少在GCC中,平均时间似乎与公认答案中的描述一致。Mersenne Twister似乎从优化中受益最少,而其他两个在考虑编译器优化后,实际上只是以难以置信的速度抛出随机数。
顺便说一句,我一直在我的噪音生成库中使用Mersenne Twister引擎(它不预先计算梯度),所以我想我会切换到其他引擎之一,以真正看到一些速度上的改进。
代码:

#include <iostream>
#include <chrono>
#include <random>

using namespace std;
using namespace std::chrono;

int main()
{
    minstd_rand linearCongruentialEngine;
    mt19937 mersenneTwister;
    ranlux48_base subtractWithCarry;
    uniform_real_distribution<float> distro;

    int numSamples = 100000;
    int repeats = 100;

    long long int avgL = 0;
    long long int avgM = 0;
    long long int avgS = 0;

    cout << "results:" << endl;

    for(int j = 0; j < repeats; ++j)
    {
        cout << "start of sequence: " << j << endl;

        auto start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(linearCongruentialEngine);
        auto stop = high_resolution_clock::now();
        auto L = duration_cast<nanoseconds>(stop-start).count();
        avgL += L;
        cout << "Linear Congruential:\t" << L << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(mersenneTwister);
        stop = high_resolution_clock::now();
        auto M = duration_cast<nanoseconds>(stop-start).count();
        avgM += M;
        cout << "Mersenne Twister:\t" << M << endl;

        start = high_resolution_clock::now();
        for(int i = 0; i < numSamples; ++i)
            distro(subtractWithCarry);
        stop = high_resolution_clock::now();
        auto S = duration_cast<nanoseconds>(stop-start).count();
        avgS += S;
        cout << "Subtract With Carry:\t" << S << endl;
    }

    cout << setprecision(10) << "\naverage:\nLinear Congruential: " << (long double)(avgL/repeats)
    << "\nMersenne Twister: " << (long double)(avgM/repeats)
    << "\nSubtract with Carry: " << (long double)(avgS/repeats) << endl;
}
qq24tv8q

qq24tv8q6#

这真的是一个权衡。像Mersenne Twister这样的PRNG更好,因为它有非常大的周期和其他良好的统计特性。
但是大周期PRNG占用更多的存储器(用于维持内部状态)并且还花费更多的时间用于生成随机数(由于复杂的转换和后处理)。
根据应用程序的需要选择PNRG。当不确定时,使用Mersenne Twister,它是许多工具的默认设置。

3zwtqj6y

3zwtqj6y7#

一般来说,mersenne twister是最好的(也是最快的)RNG,但是它需要一些空间(大约2.5千字节)。哪一个适合你的需要取决于你需要示例化生成器对象多少次。(如果你只需要示例化它一次,或者几次,那么MT就是你要使用的。如果你需要示例化它数百万次,那么也许是更小的。)
有些人报告说MT比其他的慢。根据我的实验,这在很大程度上取决于你的编译器优化设置。最重要的是-march=native设置可能会产生巨大的差异,这取决于你的主机架构。
我运行了一个小程序来测试不同发电机的速度,以及它们的大小,得到了这个:

std::mt19937 (2504 bytes): 1.4714 s
std::mt19937_64 (2504 bytes): 1.50923 s
std::ranlux24 (120 bytes): 16.4865 s
std::ranlux48 (120 bytes): 57.7741 s
std::minstd_rand (4 bytes): 1.04819 s
std::minstd_rand0 (4 bytes): 1.33398 s
std::knuth_b (1032 bytes): 1.42746 s

相关问题