C++ 11標準では、乱数生成用にいくつかの異なるエンジンを指定しています:linear_congruential_engine
、mersenne_twister_engine
、subtract_with_carry_engine
など。明らかに、これはstd::Rand
の古い使用法からの大きな変更です。
明らかに、これらのエンジンの(少なくとも一部の)主な利点の1つは、期間の長さが大幅に増加していることです(std::mt19937
の名前に組み込まれています)。
ただし、エンジン間の違いはあまり明確ではありません。さまざまなエンジンの長所と短所は何ですか?どちらをもう一方に使用すべきか?一般的に推奨される賢明なデフォルトはありますか?
以下の説明から、Marsenne Twisterの方が複雑性とランダム性が高いのに対し、線形エンジンは高速ですがランダム性は低いようです。キャリー付き減算乱数エンジンは、線形エンジンの改良版であり、間違いなくよりランダムです。最後の参考文献では、メルセンヌツイスターはキャリー付き減算減算エンジンよりも複雑であると述べられています。
線形合同乱数エンジン
符号なし整数を生成する疑似乱数ジェネレータエンジン。
これは、標準ライブラリで最も単純なジェネレータエンジンです。その状態は単一の整数値であり、次の遷移アルゴリズムがあります。
x =(ax + c)mod m
ここで、xは現在の状態値、aとcはそれぞれのテンプレートパラメーター、mはそれぞれのテンプレートパラメーターで、これが0より大きい場合、またはnumerics_limits :: max()に1を加えた場合、それ以外の場合。
その生成アルゴリズムは、状態値の直接コピーです。
これにより、処理とメモリ消費の点で非常に効率的なジェネレーターになりますが、使用される特定のパラメーターに応じて、シリアル相関の程度が異なる数値が生成されます。
Linear_congruential_engineによって生成される乱数の周期はmです。 http://www.cplusplus.com/reference/random/linear_congruential_engine/
メルセンヌツイスター乱数エンジン
閉区間[0,2 ^ w-1]で符号なし整数を生成する疑似乱数ジェネレータエンジン。
このエンジンで使用されるアルゴリズムは、範囲内でほぼ均一な分布を持つ(モンテカルロ実験などで)大規模な一連の数値を計算するように最適化されています。
エンジンには、n個の整数要素の内部状態シーケンスがあり、構築時またはメンバー関数シードを呼び出すことによって生成された疑似ランダム系列で満たされます。
内部状態シーケンスはn個の要素のソースになります:状態が進むと(たとえば、新しい乱数を生成するために)、エンジンはxorマスクaを使用して現在の値をビットの組み合わせにねじることによって状態シーケンスを変更しますその値とm要素離れた値から来るパラメーターrによって決定されます(詳細については、operator()を参照してください)。
生成される乱数は、これらのねじれた値の調整されたバージョンです。テンパリングは、選択した状態値に適用されるパラメーターu、d、s、b、t、c、lによって定義される一連のシフトおよびxor操作です(operator()を参照)。
Mersenne_twister_engineによって生成される乱数には、メルセンヌ数2 ^((n-1)* w)-1と同等の周期があります。 http://www.cplusplus.com/reference/random/mersenne_twister_engine/ =
減算付きキャリー乱数エンジン
符号なし整数を生成する疑似乱数ジェネレータエンジン。
このエンジンで使用されるアルゴリズムは、遅延整数フィボナッチジェネレーターであり、r個の整数要素の状態シーケンスと1つのキャリー値を備えています。 http://www.cplusplus.com/reference/random/subtract_with_carry_engine/
加算または減算が使用される場合、遅延フィボナッチジェネレーターの最大周期は(2k-1)* ^(2M-1)です。 LFGの初期化は非常に複雑な問題です。 LFGの出力は初期条件に非常に敏感であり、特に注意を払わない限り、統計的な欠陥が最初だけでなく定期的に出力シーケンスに現れることがあります。 LFGのもう1つの潜在的な問題は、それらの背後にある数学的理論が不完全であり、理論的パフォーマンスではなく統計的検定に依存する必要があることです。 http://en.wikipedia.org/wiki/Lagged_Fibonacci_generator
そして最後に:使用するエンジンの選択にはいくつかのトレードオフが伴います。線形合同エンジンは適度に高速で、状態のストレージ要件が非常に小さいです。遅延フィボナッチジェネレーターは、高度な算術命令セットを備えていないプロセッサーでも非常に高速ですが、状態の保存が多くなり、スペクトル特性が低下する場合があります。メルセンヌツイスターは低速で、状態の保存要件が大きくなりますが、適切なパラメーターを使用すると、最も望ましいスペクトル特性を備えた最も長い非反復シーケンスが得られます(特定の望ましい定義に対して)。 in http://en.cppreference.com/w/cpp/numeric/random
ポイントは、ランダムジェネレーターにはさまざまなプロパティがあり、特定の問題に対してそれらをより適切に、または適切でなくすることができるということです。
必要に応じて、1つのジェネレーターまたは別のジェネレーターを使用できます。たとえば、高速な乱数が必要だが品質をあまり気にしない場合は、LCGが適切なオプションになる可能性があります。より良い品質の乱数が必要な場合は、Mersenne Twisterがおそらくより良いオプションです。
選択を助けるために、いくつかの標準テストと結果があります(私は間違いなく の表p.29がこれです)論文 )。
編集:紙から、
LCG(***)
)ファミリーは最速のジェネレーターですが、品質は最も劣ります。MT19937
_)は少し遅いですが、より良い乱数を生成します。SWB(***)
、だと思います)は非常に低速ですが、適切に調整すると、より良いランダムプロパティを生成できます。他の回答がranluxについて忘れているので、最近OpenCLに移植したAMD開発者による小さなメモがあります。
https://community.AMD.com/thread/139236
RANLUXは、「乱数」を生成する理由と、それらが優れている理由を説明する根本的な理論を持つ非常に少数の(私が実際に知っている唯一の)PRNGの1つでもあります。実際、理論が正しければ(そして私がそれを論争した人を知らないなら)、最高の贅沢レベルのRANLUXは、完全に無相関の数を最後のビットまで生成します。ピリオド(10 ^ 171)の下。他のほとんどのジェネレーターは、その品質についてほとんど何も言えません(メルセンヌツイスター、KISSなど)。)統計テストに合格する必要があります。
CERNの物理学者はこのPRNGのファンです。 '言っ途切れる。
これらの他の回答の一部の情報は、私の調査結果と矛盾します。 Visual Studio 2013を使用してWindows 8.1でテストを実行しましたが、_mersenne_twister_engine
_は_linear_congruential_engine
_または_subtract_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時間:10.0821 mersenne_twister_engine時間:6.11615 subtract_with_carry_engine時間:9.26676
私はちょうどマルノスから この答え を見て、自分でテストすることにしました。 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_Rand
およびranlux48_base
は、実際にhigh_precision_clock
の実装が測定できる速度よりも速く実行されます。ただし、std::mt19937
は730045
ナノ秒、つまり3/4秒かかります。
だから、彼が言ったように、それは実装固有ですが、少なくともGCCでは、平均時間は受け入れられた回答の説明が言うことに固執しているようです。 Mersenne Twisterは最適化の恩恵が最も少ないようですが、他の2つは、コンパイラーの最適化を考慮に入れると、信じられないほど高速に乱数をスローするだけです。
余談ですが、私はノイズ生成ライブラリで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;
}
そのトレードオフは本当に。 A PRNG like Mersenne Twister
は、期間が非常に長く、他の優れた統計特性があるため、より優れています。
ただし、長い期間PRNG=は(内部状態を維持するために)より多くのメモリを消費し、(複雑な遷移と後処理のために)乱数の生成により多くの時間を要します。
アプリケーションのニーズに応じてPNRGを選択します。疑わしい場合はMersenne Twister
、多くのツールのデフォルトです。
一般に、メルセンヌツイスターは最高の(そして最速の)RNGですが、ある程度のスペース(約2.5キロバイト)が必要です。どちらがニーズに適しているかは、ジェネレーターオブジェクトをインスタンス化する必要がある回数によって異なります。 (1回だけ、または数回インスタンス化する必要がある場合は、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