新しいランダムライブラリがstd :: rand()よりも優れているのはなぜですか?


82

そこで、rand()は有害見なされるという話を見て、単純なstd::rand()プラスモジュラスパラダイムよりも乱数生成のエンジン分布パラダイムを使用することを提唱しました。

しかし、私はstd::rand()直接の失敗を見たかったので、簡単な実験をしました:

  1. 基本的に、私は2つの機能を書いたgetRandNum_Old()getRandNum_New()それが使用して0から5までの間の乱数を生成std::rand()し、std::mt19937+std::uniform_int_distributionそれぞれ。
  2. 次に、「古い」方法を使用して960,000(6で割り切れる)の乱数を生成し、0〜5の数値の頻度を記録しました。次に、これらの周波数の標準偏差を計算しました。私が探しているのは、分布が本当に均一である場合に起こることになるので、可能な限り低い標準偏差です。
  3. そのシミュレーションを1000回実行し、各シミュレーションの標準偏差を記録しました。また、ミリ秒単位でかかった時間を記録しました。
  4. その後、まったく同じことを繰り返しましたが、今回は「新しい」方法で乱数を生成しました。
  5. 最後に、新旧両方の方法の標準偏差のリストの平均と標準偏差、および新旧両方の方法の時間のリストの平均と標準偏差を計算しました。

結果は次のとおりです。

[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+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));
}

32
これが、このアドバイスが存在する理由です。RNGのエントロピーが十分かどうかをテストする方法がわからない場合、またはプログラムにとって重要かどうかがわからない場合は、std :: rand()では不十分であると想定する必要があります。 )en.wikipedia.org/wiki/Entropy_(computing
ハンスアンパッサン

4
rand()十分かどうかの結論は、乱数のコレクションを何に使用しているかに大きく依存します。特定の種類のランダムな分布が必要な場合は、もちろん、ライブラリの実装の方が優れています。単に乱数が必要で、「ランダム性」や生成される分布のタイプを気にしない場合は、問題ありませんrand()。適切なツールを目前の仕事に合わせてください。
デビッドC.ランキン

2
重複の可能性:stackoverflow.com/questions/52869166/…これをハンマーで叩きたくないので、実際に投票することは控えています。
bolov 2018年

18
for (i=0; i<k*n; i++) a[i]=i%n;そこにある最高のRNGと同じ正確な平均と標準偏差を生成します。これでアプリケーションに十分な場合は、このシーケンスを使用してください。
N。「代名詞」m。

3
「標準偏差を可能な限り低くする」-いいえ。それは間違っている。あなたは期待SQRT(周波数)については、標準偏差があることを期待するものについてです-周波数が少し異なるように。nmが生成する「インクリメントカウンター」は、sdがはるかに低くなります(非常に悪いrngです)。
マーティンボナーは

回答:


106

「古い」のほとんどすべての実装はLCGをrand()使用します。それらは一般的に最高のジェネレーターではありませんが、通常、そのような基本的なテストで失敗することはありません。平均値と標準偏差は、最悪のPRNGでも一般的に正しくなります。

「悪い」rand()実装の一般的な失敗-しかし十分に一般的-実装は次のとおりです。

  • 下位ビットのランダム性が低い。
  • 短期間;
  • RAND_MAX;
  • 連続する抽出間の相関関係(一般に、LCGは限られた数の超平面上にある数を生成しますが、これは何らかの方法で軽減できます)。

それでも、これらはいずれものAPIに固有のものではありませんrand()。特定の実装では、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、わずか32767のスポーツであるVisual C ++です。ただし、これは過去との互換性を損なうため、簡単に変更することはできません。srand再現可能なシミュレーションに固定シードを使用する人々は、あまり満足しません(実際、IIRC前述の実装は、80年代半ばからMicrosoft Cの初期バージョン(またはLattice C)に戻ります。
  • 単純なインターフェース。rand()プログラム全体のグローバル状態を単一のジェネレーターに提供します。これは多くの単純なユースケースでは完全に問題ありませんが(実際には非常に便利です)、問題が発生します。

    • マルチスレッドコードの場合:これを修正するには、グローバルミューテックス(呼び出しのシーケンス自体がランダムになるため理由なくすべてが遅くなり、再現性の可能性がなくなる)、またはスレッドローカル状態が必要です。この最後のものは、いくつかの実装(特にVisual C ++)で採用されています。
    • グローバル状態に影響を与えない、プログラムの特定のモジュールへの「プライベート」で再現可能なシーケンスが必要な場合。

最後に、rand情勢:

  • 実際の実装を指定していません(C標準はサンプル実装のみを提供します)。したがって、異なるコンパイラ間で再現可能な出力を生成する(または既知の品質のPRNGを期待する)プログラムは、独自のジェネレータをロールする必要があります。
  • まともなシードを取得するためのクロスプラットフォームの方法を提供していません(time(NULL)十分に細かくないため、そうではなく、RTCのない組み込みデバイスを考えてください-十分にランダムではありません)。

したがって、次のような<random>アルゴリズムを提供するこの混乱を修正しようとする新しいヘッダー。

  • 完全に指定されています(クロスコンパイラの再現可能な出力と保証された特性(たとえば、ジェネレータの範囲)を持つことができます)。
  • 一般的に最先端の品質(ライブラリが設計されたときから。以下を参照)。
  • クラスにカプセル化されます(したがって、グローバル状態が強制されることはなく、完全なスレッド化と非局所性の問題が回避されます)。

...そしてrandom_deviceそれらをシードするためのデフォルトも。

さて、あなたが私に尋ねるなら、私は「簡単な」、「数を推測する」場合のためにこれの上に構築された単純なAPI好きでした(Pythonが「複雑な」APIを提供する方法と同様ですが、些細なrandom.randint&Co 。ランダムなデバイス/エンジン/アダプター/ビンゴカードの番号を抽出するたびに溺れたくない単純な人々のために、グローバルな事前シードされたPRNGを使用します)が、簡単にできることは事実です現在の機能の上に自分で構築します(単純なものの上に「完全な」APIを構築することはできません)。


最後に、パフォーマンスの比較に戻ります。他の人が指定しているように、高速のLCGを低速の(ただし一般的にはより良い品質と見なされる)メルセンヌツイスターと比較しています。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ミリ秒(古い)対21ミリ秒(新しい)を取得します。最後に、dist(従来のモジュロ演算子と比較して、入力範囲の倍数ではない出力範囲の分布スキューを処理する)を取り除き、現在の作業に戻ります。getRandNum_Old()

int getRandNum_New() {
    static std::minstd_rand eng{std::random_device{}()};
    return eng() % 6;
}

私はへの呼び出しとは異なり、おそらく、ので、6ミリ秒(そう、30%速い)にそれを降りrand()std::minstd_randインラインに簡単です。


ちなみに、私は手巻きを使用して同じテストを行いました(ただし、標準ライブラリインターフェイスにほぼ準拠しています)。これはXorShift64*、2.3倍高速ですrand()(3.68ミリ秒対8.61ミリ秒)。メルセンヌツイスターと様々な提供LCGsとは異なり、それは、与えられたことを見事に現在のランダム・テスト・スイートを渡す 、それは驚くほど速いです、それはそれはまだ標準ライブラリに含まれていませんなぜあなたが思ってしまいます。


3
問題が発生srandするのはstd::rand、まさにその組み合わせと不特定のアルゴリズムです。別の質問に対する私の回答も参照してください。
ピーターO.

2
randシード(したがって、生成できる可能なシーケンスの数)がによって制限されるという点で、APIレベルでは基本的に制限されていますUINT_MAX+1
R .. GitHub STOP HELPING ICE

2
注:minstdは悪いPRNGであり、mt19973の方が優れていますが、それほどではありません:pcg-random.org/…(このグラフでは、minstd == LCG32 / 64)。C ++がPCGやxoroshiro128 +のような高品質で高速なPRNGを提供しないのは非常に残念です。
user60561 2018年

2
@MatteoItalia私たちは意見の相違はありません。これもビャーネの主張でした。私たちは本当に<random>標準を望んでいますが、「今すぐ使用できる適切な実装を提供してください」というオプションも必要です。PRNGやその他のものに。
ravnsgaard

2
いくつかの注意事項:1。に置き換えるstd::uniform_int_distribution<int> dist{0, 5}(eng);eng() % 6std::randコードが被るスキュー係数が再導入されます(この場合、エンジンに2**31 - 1出力があり、それらを6つのバケットに分散している場合は明らかにマイナーなスキューです)。2.書かれているように、可能な出力を制限する「srandtakes unsigned int」についてのメモでは、エンジンにシードを設定しstd::random_device{}()ても同じ問題が発生します。ほとんどのPRNGを適切に初期化するseed_seqためにが必要です
ShadowRanger 2018年

6

5より大きい範囲で実験を繰り返すと、おそらく異なる結果が表示されます。範囲が大幅に小さい場合RAND_MAX、ほとんどのアプリケーションで問題は発生しません。

たとえば、RAND_MAXが25の場合rand() % 5、次の頻度で数値が生成されます。

0: 6
1: 5
2: 5
3: 5
4: 5

RAND_MAX32767以上であることを保証し、少なくとも可能性が高いと最も可能性が高いとの周波数の差さ分布は、ほとんどのユースケースのためにランダムに十分近くにある少数のために、唯一の1です。


3
これは、STLの2番目のスライド
Alan Birtles 2018年

4
わかりました、しかし... STLは誰ですか?そして、何がスライドしますか?(深刻な質問)
kebs 2018年

@ kebs、Stephan Lavavej、質問のYouTubeリファレンスを参照してください。
平均2018年

3

まず、驚くべきことに、乱数を何に使用しているかによって答えが変わります。たとえば、ランダムな背景色チェンジャーを駆動する場合は、rand()を使用しても問題ありません。乱数を使用してランダムなポーカーハンドまたは暗号的に安全なキーを作成している場合、それは問題ありません。

予測可能性:シーケンス012345012345012345012345 ...は、サンプル内の各数値の均等な分布を提供しますが、明らかにランダムではありません。シーケンスがランダムであるためには、n + 1の値をnの値(またはn、n-1、n-2、n-3などの値)で簡単に予測することはできません。明らかに繰り返しシーケンスです。同じ数字の縮退の場合ですが、線形合同法で生成されたシーケンスは分析の対象になります。共通ライブラリの共通LCGのデフォルトの初期設定を使用すると、悪意のある人が何の努力もせずに「シーケンスを壊す」可能性があります。過去には、いくつかのオンラインカジノ(およびいくつかの実店舗のカジノ)は、貧弱な乱数ジェネレーターを使用するマシンによって損失を被りました。もっとよく知っているはずの人でさえも巻き込まれています。

分布:ビデオでほのめかされているように、100のモジュロ(またはシーケンスの長さに均等に分割できない値)を取ると、一部の結果が他の結果よりも少なくともわずかに高くなることが保証されます。100を法とする32767の可能な開始値のユニバースでは、0から66までの数値は、67から99までの値よりも328/327(0.3%)多く表示されます。攻撃者に利点を提供する可能性のある要因。


1
「予測可能性:シーケンス012345012345012345012345 ...は、サンプル内の各数値が均等に分布するという点で、「ランダム性」の検定に合格します」実際にはそうではありません。彼が測定しているのは、実行間のstddevのstddevです。つまり、基本的に、さまざまな実行ヒストグラムがどのように分散されているかです。012345012345012345 ...ジェネレーターを使用すると、常にゼロになります。
Matteo Italia

いい視点ね; OPのコードを少し速く読みすぎたので、怖いです。反映するように私の答えを編集しました。
JackLThornton 2018年

Hehe私もそのテストを行うつもりだったので知っています、そして私は異なる結果が得られたことに気づきました😄–
Matteo Italia

1

正解は次のとおりです。「より良い」とはどういう意味かによって異なります。

「新しい」<random>エンジンは13年以上前にC ++に導入されたため、実際には新しいものではありません。Cライブラリrand()は数十年前に導入され、当時はさまざまなことに非常に役立ちました。

C ++標準ライブラリは、3つのクラスの乱数ジェネレータエンジンを提供します。線形合同法(rand()その一例です)、Lagged Fibonacci、およびMersenneTwisterです。各クラスにはトレードオフがあり、各クラスは特定の点で「最良」です。たとえば、LCGの状態は非常に小さく、適切なパラメータが選択されている場合、最新のデスクトッププロセッサではかなり高速です。LFGは状態が大きく、メモリフェッチと加算演算のみを使用するため、特殊な数学ハードウェアがない組み込みシステムやマイクロコントローラーでは非常に高速です。MTGは巨大な状態で低速ですが、優れたスペクトル特性を備えた非常に大きな非反復シーケンスを持つことができます。

提供されているジェネレーターのいずれも特定の用途に十分でない場合、C ++標準ライブラリーは、ハードウェアジェネレーターまたは独自のカスタムエンジンのいずれかのインターフェイスも提供します。ジェネレーターはいずれもスタンドアロンでの使用を目的としていません。それらの使用目的は、特定の確率分布関数を持つランダムシーケンスを提供する分布オブジェクトを介したものです。

<random>overのもう1つの利点rand()は、rand()グローバル状態を使用し、再入可能またはスレッドセーフではなく、プロセスごとに1つのインスタンスを許可することです。きめ細かい制御または予測可能性が必要な場合(つまり、RNGシードの状態を指定してバグを再現できる場合)は役に立ちませんrand()<random>発電機は、局部的にインスタンス化して、シリアライズ(および復元)状態を持っています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.