随机引擎的差异

59

C++11标准规定了许多不同的引擎用于随机数生成:linear_congruential_enginemersenne_twister_enginesubtract_with_carry_engine等。 很明显,这与旧版使用的std::rand相比是一项重大改变。

显然,至少某些引擎的主要优点是其巨大的周期长度(在std::mt19937的名称中已经体现了这一点)。

然而,引擎之间的差异并不那么清晰。 不同引擎的优缺点是什么?应该何时使用其中之一?是否有一个明智的默认值通常应该被采用?


1
好问题。我会说std::mt19937似乎是最常用的通用随机数生成器,但我完全没有事实依据来支持这一点,所以... - Anthony
1
@anthony-arnold 我同意。我看到的几乎所有示例都使用 std::mt19937,现在每当我需要生成随机数时,我似乎都会使用它,但除了习惯之外,我没有特定的原因。 - Yuushi
2
Mersenne Twister 的原始论文可能对其为什么好有详细的解释:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/ARTICLES/mt.pdf - David Brown
@DavidBrown 感谢提供链接。我同意,关于带进位减法的信息似乎是最难获取的。 - Yuushi
7个回答

30
根据下面的解释,线性引擎似乎更快但不太随机,而Mersenne Twister具有更高的复杂性和随机性。Subtract-with-carry随机数引擎是对线性引擎的改进,肯定更随机。在最后一个参考文献中,指出Mersenne Twister比Subtract-with-carry随机数引擎具有更高的复杂性。
线性同余随机数引擎
这是一个产生无符号整数的伪随机数生成器引擎。
这是标准库中最简单的生成器引擎。它的状态是一个单一的整数值,并具有以下转换算法:
x = (ax+c) mod m

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

它的生成算法是直接复制状态值。

这使它成为一个在处理和内存消耗方面非常高效的生成器,但根据所使用的具体参数产生具有不同程度连续性的随机数。

linear_congruential_engine 生成的随机数周期为 m

Mersenne twister 随机数引擎

一种伪随机数生成器引擎,生成闭区间 [0,2^w-1] 中的无符号整数。

该引擎使用的算法被优化为在范围内具有几乎均匀分布的大量数字序列(例如在 Monte Carlo 实验中)。

引擎有一个内部状态序列,包含n个整数元素,构造函数或调用成员函数seed时填充伪随机序列。
当状态被推进(例如为了生成新的随机数),引擎通过在当前值上使用异或掩码a对由参数r决定的位和距离该值m个元素的值的混合进行扭曲,从而改变状态序列。
生成的随机数是这些扭曲值的加工版本。加工是由参数udsbtcl定义的移位和异或操作序列,应用于选定的状态值(参见operator())。 mersenne_twister_engine生成的随机数具有与mersenne数字2^((n-1)*w)-1等效的周期。

减去进位随机数引擎

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

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

滞后斐波那契生成器如果使用加法或减法,则最大周期为(2k-1)*^(2M-1)。LFG的初始化是一个非常复杂的问题。LFG的输出对初始条件非常敏感,并且除非采取极端谨慎的措施,否则输出序列中可能出现统计缺陷。 LFG的另一个潜在问题是其背后的数学理论不完整,因此需要依靠统计检验而不是理论性能。

最后来自random文档:

选择使用哪个引擎涉及到一些权衡:线性同余发生器速度适中,状态存储要求非常小。滞后斐波那契生成器即使在没有高级算术指令集的处理器上也非常快,但代价是需要更大的状态存储和有时不太理想的谱特性。Mersenne Twister 较慢,需要更大的状态存储要求,但通过正确的参数设置可以获得最长的非重复序列和最理想的谱特性(对于给定的定义而言)。

这有点回答了我的问题。LCG的周期长度远远低于mersenne twister,但它并没有真正揭示出“subtract_with_carry_engine”的太多信息。 - Yuushi
2
Boost提供了一些关于周期长度和速度的有用信息,链接 - pyCthon

12

我认为关键在于随机生成器具有不同的属性,这些属性可以使它们更适合或不适合某个问题。

  • 周期长度是其中之一。
  • 随机数的质量也可能很重要。
  • 性能也可能成为问题。

根据您的需要,您可能会选择一个或另一个生成器。例如,如果您需要快速生成随机数但并不真正关心质量,线性同余发生器(LCG)可能是一个不错的选择。如果你想要更好的质量的随机数,梅森旋转算法(Mersenne Twister)可能是一个更好的选择。

为了帮助您做出选择,有一些标准测试和结果(我绝对喜欢这篇论文第29页的表格)。


编辑:从论文中可以看出:

  1. 线性同余发生器(LCG)(LCG(***))族是最快的发生器,但质量最差。
  2. 梅森旋转算法(MT19937)稍慢,但生成更好的随机数。
  3. 带有进位的减法算法( SWB(***), 我想)比较慢,但在良好调整时,可以产生更好的随机属性。

那篇论文正是我所需要的:性能数据。 - jfritz42

6
作为其他答案忘记了ranlux,这里有一个小笔记,由一个最近将其移植到OpenCL的AMD开发人员提供:

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

RANLUX是极少数(实际上是我所知唯一的)具有基础理论解释其生成“随机”数字及其优秀性能的伪随机数生成器之一。如果该理论正确(我不知道是否有人对此提出异议),则在最高豪华级别下,RANLUX产生的数字完全无相关性,直到最后一位也没有长程相关性,只要我们保持远低于周期(10 ^ 171)。大多数其他生成器不能说明其质量(如Mersenne Twister,KISS等),它们必须依靠通过统计测试来验证。

欧洲核子研究组织的物理学家们喜欢这个伪随机数生成器。就这样。

1
嗨,链接已损坏。新的URL是https://community.amd.com/thread/139236。页面标题为“在OpenCL中实现的RANLUX伪随机数生成器”。 - Akira Takahashi

2
一些其他回答中的信息与我的研究结果不一致。我在Windows 8.1上使用Visual Studio 2013进行了测试,一致发现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个样本进行测试。
线性同余发生器时间: 10.0821
梅森旋转演算法发生器时间: 6.11615
带借位发生器时间: 9.26676

1
我刚刚看到Marnos的这个答案,决定自己测试一下。 我使用了std :: chono :: high_resolution_clock计时100000个样本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;
}

3
对于未来的读者:在基准测试中不要使用high_resolution_clock,而应该使用steady_clock。虽然它们很可能是相同的,但由于没有标准化,high_resolution_clock可能是system_clock的别名,具体取决于实现方式。system_clock可能不是单调的,因此不适合用于基准测试。 - paleonix
1
Gcc(11.1)在-O3优化级别下可以优化对distro(linearCongruentialEngine);的调用。因此,这段代码需要加强防御以抵御这种影响。标志march = native对于Mersenne-Twister在计算效率方面具有关键作用。 - zkoza

0

通常情况下,Mersenne Twister是最好(也是最快)的随机数生成器,但它需要一些空间(大约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

0

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

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

根据应用程序的需求选择 PNRG。当存在疑虑时,使用 Mersenne Twister,它是许多工具的默认值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接