そこで私は Rand()Considered Harmful と呼ばれる講演を見ました。そしてそれは単純なstd::Rand()
と法のパラダイムの上に乱数生成のエンジン分配パラダイムを使うことを提唱しました。
しかし、私はstd::Rand()
の失敗を直接目にしたかったので、簡単な実験をしました。
getRandNum_Old()
とstd::mt19937
+ std::uniform_int_distribution
を使って0から5までの乱数を生成する2つの関数getRandNum_New()
とstd::Rand()
を書きました。結果は次のとおりです。
[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::mt19937
some_code __ + std::uniform_int_distribution
は、単純なstd::Rand()
+ %
よりも「均一」ではありませんでした。私がしたもう一つの観察は、新しいものが古い方法より約4倍遅いということでした。全体的に見て、私は品質をほとんど向上させることなくスピードに多大なコストをかけていたようでした。
私の実験には何らかの欠陥がありますか?それとも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));
}
"古い" Rand()
のほとんどすべての実装は _ lcg _ ;を使います。それらは一般的には最良のジェネレータではありませんが、通常、あなたはそれらがそのような基本的なテストで失敗するのを見ることはないでしょう - 平均と標準偏差は一般に最悪のPRNGでも正しく得られます。
"悪い"という一般的な失敗 - しかし一般的に十分な - Rand()
実装は次のとおりです。
Rand_MAX
;それでも、これらのどれもRand()
のAPIに特定ではありません。特定の実装はxorshift-familyジェネレータをsrand
/Rand
の後ろに置き、アルゴリズム的に言えば最先端のPRNGを得ることができるので、あなたがしたようなテストは弱点を示すことはないでしょう。出力。
編集: @R。はRand
/srand
インターフェースはsrand
がunsigned int
を取るという事実によって制限されることを正しく注意しているので、実装がそれらの後ろに置くことができるジェネレータは本質的にUINT_MAX
に制限されます可能性のある開始シード(そしてこうして生成されたシーケンス)。 APIはsrand
がunsigned long long
を取るように、または別のsrand(unsigned char *, size_t)
オーバーロードを追加するように自明に拡張することができますが、これは確かに本当です。
実際のところ、Rand()
の実際の問題は実装の大部分ではありません原則的にですが、
Rand_MAX
を使用します。ただし、これは過去との互換性を壊すので簡単には変更できません。再現性のあるシミュレーションに固定シードを付けてsrand
を使用する人々はうんざりしません。 IIRC、前述の実装は、80年代半ばからMicrosoft Cの初期バージョン、さらにはLattice Cまでさかのぼります。単純なインタフェース。 Rand()
はプログラム全体のグローバルな状態を持つ単一のジェネレータを提供します。これは多くの単純なユースケースでは完全に問題ありませんが(そして実際にはかなり便利ですが)、問題を引き起こします。
最後に、Rand
の業務状況は次のとおりです。
time(NULL)
は十分ではないため、十分ではありません。多くの場合、RTCがない組み込みデバイスを考える - 十分にランダムではありません)。それゆえ、新しい<random>
ヘッダは、この混乱を修正しようとしており、以下のアルゴリズムを提供します。
...そしてそれらをシードするためのデフォルトのrandom_device
。
さて、あなたが私に尋ねるなら、私はもを "簡単な"、 "多くのことを推測する"ケースのために構築されたシンプルなAPIが好きだったでしょう。ランダムなデバイス/エンジン/アダプタ/に没頭したくない場合は何でも、グローバルな事前シードPRNGを使用している些細なrandom.randint
&Co.も、ビンゴカード)、しかし、それはあなたが現在の機能の上に自分でそれを簡単に構築できることは事実です(単純なものの上に「完全な」APIを構築することはできませんが)。
最後に、パフォーマンスの比較に戻るために、他の人が指定したように、速いLCGと遅い(ただし一般的にはより良いと考えられる)Mersenne Twisterを比較しています。 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 ms(古い)と21 ms(新しい)が表示されます。最後に、(従来のモジュロ演算子と比較して、入力範囲の倍数ではない出力範囲の分布の偏りを処理する)dist
を取り除き、getRandNum_Old()
で行っていることに戻ります。
int getRandNum_New() {
static std::minstd_Rand eng{std::random_device{}()};
return eng() % 6;
}
Rand()
の呼び出しとは異なり、std::minstd_Rand
のインライン化が簡単だからでしょう。
ちなみに、私はハンドロール(しかし標準ライブラリインターフェースにほぼ準拠しています)XorShift64*
を使って同じテストをしました、そしてそれはRand()
より2.3倍速いです(3.68 ms対8.61 ms)。 Mersenne Twisterや提供されているさまざまなLCGとは異なり、 現在のランダム性テストスイートを飛んでいる色で通過させますandこれは驚くほど高速です。まだ標準ライブラリ。
5より大きい範囲で実験を繰り返すと、おそらく異なる結果が表示されます。あなたの範囲がRand_MAX
よりかなり小さい場合、ほとんどのアプリケーションにとって問題はありません。
たとえば、Rand_MAX
が25の場合、Rand() % 5
は次の頻度で数値を生成します。
0: 6
1: 5
2: 5
3: 5
4: 5
Rand_MAX
は32767を超えることが保証されており、頻度の最も低い可能性が高いと最も高い可能性が高いのは1にすぎません。少数の場合、分布はほとんどのユースケースで十分にランダムです。
まず、驚くべきことに、答えは乱数を使っているものによって変わります。例えば、ランダムな背景色チェンジャーを動かすのであれば、Rand()を使用しても問題ありません。乱数を使ってランダムなポーカーハンドや暗号化された安全な鍵を作成しているのであれば、それは問題ありません。
予測可能性:順序012345012345012345012345 ...は、サンプル内の各数値の均等分布を示しますが、明らかにランダムではありません。シーケンスがランダムであるためには、nの値(またはn、n-1、n-2、n-3などの値でさえ)でn + 1の値を簡単に予測することはできません。同じ数字の縮退は縮退した場合ですが、任意の線形合同生成元を使用して生成されたシーケンスを分析することができます。一般的なライブラリからの一般的なLCGのデフォルトのデフォルト設定を使用する場合、悪意のある人はまったく努力せずに「シーケンスを壊す」ことができます。過去には、いくつかのオンラインカジノ(およびいくつかの実店舗用のカジノ)が、貧弱な乱数発生器を使用している機械で損失を被っていました。もっとよく知っておくべき人でさえ巻き込まれています。いくつかの製造業者からのTPMチップは、鍵生成パラメータによる選択が不十分であるために、鍵のビット長が予測したものよりも壊れやすいことが実証されています。
分布:ビデオで触れたように、100のモジュロ(またはシーケンスの長さに均等に分割できない任意の値)を使用すると、一部の結果が他の結果よりもわずかに高くなる可能性があります。 100を法とする32767の可能な開始値の範囲では、0から66までの数値は、67から99までの値よりも328/327(0.3%)多く出現します。攻撃者に利点をもたらす可能性がある要因。
正しい答えは次のとおりです。それは、「より良い」という意味によって異なります。
"新しい" <random>
エンジンは13年以上前にC++に導入されたので、本当に新しいものではありません。 CライブラリRand()
は数十年前に導入され、その当時非常に有用でした。
C++標準ライブラリは、3つのクラスの乱数ジェネレータエンジンを提供します。線形合同(そのうちのRand()
がその例です)、Lagged Fibonacci、およびMersenne Twisterです。各クラスにはトレードオフがあり、各クラスはある意味で「最善」です。たとえば、LCGの状態は非常に小さく、正しいパラメータが選択されていれば、最近のデスクトッププロセッサではかなり高速です。 LFGは状態が大きく、メモリのフェッチと加算操作のみを使用するため、組み込みシステムや特殊な演算ハードウェアがないマイクロコントローラでは非常に高速です。 MTGは巨大な状態を持ち、遅いですが、優れたスペクトル特性を持つ非常に大きな非繰り返しシーケンスを持つことができます。
提供されているジェネレータがどれもあなたの特定の用途に十分に適していない場合、C++標準ライブラリはハードウェアジェネレータまたはあなた自身のカスタムエンジンのいずれかのためのインターフェースも提供します。ジェネレータのどれもスタンドアロンで使用されることを意図していません:それらの意図された使用は特定の確率分布関数でランダムなシーケンスを提供する分布オブジェクトを通してです。
Rand()
に対する<random>
のもう1つの利点は、Rand()
がグローバル状態を使用し、再入可能またはスレッドセーフではなく、プロセスごとに単一のインスタンスを許可することです。きめ細かい制御や予測可能性(つまり、RNGシード状態でのバグを再現できること)が必要な場合は、Rand()
は役に立ちません。 <random>
ジェネレータはローカルにインスタンス化され、シリアライズ可能な(そして復元可能な)状態を持ちます。