随机引擎差异

Random Engine Differences

本文关键字:引擎 随机      更新时间:2023-10-16

c++ 11标准规定了许多不同的随机数生成引擎:linear_congruential_engine, mersenne_twister_engine, subtract_with_carry_engine等。显然,这与std::rand的旧用法相比是一个很大的变化。

显然,这些引擎的主要好处之一(至少是一些)是大幅增加周期长度(这是std::mt19937的名称)。

然而,发动机之间的差异不太清楚。不同引擎的优缺点是什么?什么时候应该使用其中一个而不是另一个?是否存在一种明智的、通常应该被偏爱的违约?

从下面的解释来看,线性引擎似乎更快,但随机性更少,而Mersenne Twister具有更高的复杂性和随机性。进位减法随机数引擎是对线性引擎的改进,它的随机性更强。在最后的参考文献中,我们指出了Mersenne Twister比减法进位随机数引擎具有更高的复杂度。

线性同余随机数引擎

生成无符号整数的伪随机数生成器引擎。

是标准库中最简单的生成器引擎。其状态为单个整数值,使用以下转换算法:

x = (ax+c) mod m

其中x为当前状态值,ac为各自的模板参数,如果大于0,则m为各自的模板参数,否则为numerics_limits<UIntType>::max() + 1

它的生成算法是状态值的直接拷贝。

这使得它在处理和内存消耗方面成为一个非常有效的生成器,但是根据使用的特定参数,生成具有不同程度串行相关的数字。

linear_congruential_engine生成的随机数周期为m

梅森捻线机随机数引擎

伪随机数生成引擎,在闭合区间[0,2^w-1]内生成无符号整数。

该引擎所使用的算法经过优化,可以计算在范围内几乎均匀分布的大数列(如蒙特卡罗实验)。

引擎有一个由n整数元素组成的内部状态序列,该序列由构造或调用成员函数seed生成的伪随机序列填充。

内部状态序列成为n元素的来源:当状态高级时(例如,为了产生一个新的随机数),引擎通过使用xor掩码a对由参数r决定的来自该值和m元素的值的混合来改变状态序列(参见operator())。

产生的随机数是这些扭曲值的调和版本。回火是由参数udsbtcl定义的一系列移位和异或操作,作用于选定的状态值(参见operator())。

mersenne_twister_engine生成的随机数的周期等于梅森数2^((n-1)*w)-1

减法进位随机数引擎

生成无符号整数的伪随机数生成器引擎。

该引擎使用的算法是一个滞后的斐波那契生成器,具有r整数元素的状态序列,加上一个进位值。

如果使用加法或减法,

滞后斐波那契生成器的最大周期为(2k - 1)*^(2M-1)。lfg的初始化是一个非常复杂的问题。lfg的输出对初始条件非常敏感,除非非常小心,否则统计缺陷可能在初始时出现,但也会周期性地出现在输出序列中。lfg的另一个潜在问题是,其背后的数学理论是不完整的,这使得它必须依赖统计测试,而不是理论性能。

最后从random的文档中:

选择使用哪个引擎涉及许多权衡:线性同余引擎速度适中,并且对状态的存储需求非常小。滞后的斐波那契生成器即使在没有高级算术指令集的处理器上也非常快,但代价是更大的状态存储和有时不太理想的频谱特性。梅森扭扭更慢,有更大的状态存储要求,但在正确的参数下,具有最长的非重复序列,具有最理想的光谱特性(对于给定的理想定义)。

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

    周期长度是属性之一。
  • 随机数的质量也很重要。
  • 生成器的性能也可能是一个问题。

根据您的需要,您可以使用一个或另一个生成器。例如,如果你需要快速随机数,但并不真正关心质量,LCG可能是一个不错的选择。如果你想要质量更好的随机数,梅森扭扭机可能是一个更好的选择。

为了帮助你做出选择,这里有一些标准测试和结果(我非常喜欢这篇论文的表p.29)。


编辑:来自论文,

  1. LCG(论文中的LCG(***))家族是最快的发电机,但质量最差。
  2. Mersenne Twister (MT19937)稍微慢一点,但产生更好的随机数。
  3. 带进位的减法(SWB(***),我认为)要慢得多,但在调整好后可以产生更好的随机属性。

由于其他答案都忘记了ranlux,这里有一个AMD开发人员最近将其移植到OpenCL的小提示:

https://community.amd.com/thread/139236

RANLUX也是为数不多的(实际上是我所知道的唯一一个)prng之一,它有一个潜在的理论来解释为什么它会生成"随机"数字,以及为什么它们很好。事实上,如果这个理论是正确的(我不知道有谁对它有异议),最高奢侈品水平的RANLUX会产生完全去相关的数字,直到最后一位,只要我们保持在周期(10^171)以下,就没有长期相关性。大多数其他生成器对其质量知之甚少(如Mersenne Twister, KISS等),它们必须依赖于通过统计测试。

欧洲核子研究中心的物理学家是这种PRNG的粉丝。足够地说。

这些其他答案中的一些信息与我的发现相冲突。我使用Visual Studio 2013在Windows 8.1上运行测试,我一直发现mersenne_twister_engine质量更高,速度也比linear_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;
}
输出:

<>之前100000000个样品测试。Linear_congruential_engine time: 10.0821Mersenne_twister_engine时间:6.11615Subtract_with_carry_engine时间:9.26676

我刚从Marnos那里看到这个答案,决定自己测试一下。我使用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;
}

这是一种权衡。像Mersenne Twister这样的PRNG更好,因为它具有极大的周期和其他良好的统计特性。

但是大周期的PRNG占用更多的内存(用于维护内部状态),并且还需要更多的时间来生成随机数(由于复杂的转换和后处理)。

根据应用程序的需要选择PNRG。如果有疑问,请使用Mersenne Twister,这是许多工具的默认设置。

一般来说,mersenne twister是最好的(也是最快的)RNG,但它需要一些空间(大约2.5 kb)。哪一个适合您的需要取决于您需要实例化生成器对象的次数。(如果您只需要实例化它一次或几次,那么可以使用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