it-swarm.com.de

Zufällige Engine-Unterschiede

Der C++ 11-Standard spezifiziert eine Anzahl verschiedener Engines für die Zufallszahlengenerierung: linear_congruential_engine, mersenne_twister_engine, subtract_with_carry_engine und so weiter. Offensichtlich ist dies eine große Änderung gegenüber der alten Verwendung von std::Rand

Offensichtlich ist einer der Hauptvorteile von (zumindest einigen) dieser Engines die enorm verlängerte Periodenlänge (sie ist in den Namen für std::mt19937 integriert). 

Die Unterschiede zwischen den Motoren sind jedoch weniger klar. Was sind die Stärken und Schwächen der verschiedenen Motoren? Wann sollte einer über dem anderen verwendet werden? Gibt es einen vernünftigen Standardwert, der generell bevorzugt werden sollte?

47
Yuushi

Nach den folgenden Erklärungen scheint der Linearmotor schneller, aber weniger zufällig zu sein, während Marsenne Twister eine höhere Komplexität und Zufälligkeit aufweist. Die Subtract-with-Carry-Zufallszahlen-Engine ist eine Verbesserung der linearen Engine und ist definitiv zufälliger. In der letzten Referenz wird angegeben, dass Mersenne Twister eine höhere Komplexität als die Zufallszahlenmaschine Subtract-with-Carry aufweist

Linearer kongruentieller Zufallszahlenmotor

Eine Pseudo-Zufallszahlengenerator-Engine, die vorzeichenlose Ganzzahlen erzeugt.

Dies ist die einfachste Generator-Engine in der Standardbibliothek. Sein Zustand ist ein einzelner ganzzahliger Wert mit dem folgenden Übergangsalgorithmus:

x = (ax + c) mod m

Dabei ist x der aktuelle Zustandswert, a und c ihre jeweiligen Vorlagenparameter und m ist der jeweilige Vorlagenparameter, falls dieser größer als 0 ist, oder numerics_limits :: max () plus 1, andernfalls.

Der Generierungsalgorithmus ist eine direkte Kopie des Zustandswerts.

Dies macht es zu einem äußerst effizienten Generator in Bezug auf die Verarbeitung und den Speicherverbrauch, erzeugt jedoch abhängig von den verwendeten spezifischen Parametern Zahlen mit unterschiedlichem seriellen Korrelationsgrad.

Die von linear_congruential_engine erzeugten Zufallszahlen haben eine Periode von m. http://www.cplusplus.com/reference/random/linear_congruential_engine/

Mersenne Twister Zufallszahlenmodul

Eine Pseudozufallszahlengenerator-Engine, die im geschlossenen Intervall vorzeichenlose Ganzzahlen erzeugt [0,2 ^ w-1].

Der von dieser Engine verwendete Algorithmus ist optimiert, um große Zahlenreihen (wie beispielsweise Monte-Carlo-Experimente) mit einer nahezu gleichmäßigen Verteilung im Bereich zu berechnen.

Die Engine hat eine interne Zustandsfolge von n ganzzahligen Elementen, die mit einer Pseudo-Zufallsreihe gefüllt wird, die beim Konstruieren oder durch Aufruf der Member-Funktionsfunktion erzeugt wird.

Die interne Zustandsfolge wird zur Quelle für n Elemente: Wenn der Zustand vorgerückt wird (um beispielsweise eine neue Zufallszahl zu erzeugen), ändert die Engine die Zustandsfolge, indem der aktuelle Wert unter Verwendung der Xoder Maske a mit einer Mischung von Bits verdreht wird bestimmt durch den Parameter r, der von diesem Wert und von einem Wert, der m Elemente entfernt ist, stammt (siehe Operator () für Details).

Die erzeugten Zufallszahlen sind temperierte Versionen dieser verdrehten Werte. Die Temperierung ist eine Folge von Shift- und Xor-Operationen, die durch die Parameter u, d, s, b, t, c und l definiert werden, die auf den ausgewählten Zustandswert angewendet werden (siehe operator ()).

Die von mersenne_twister_engine erzeugten Zufallszahlen haben eine Periode, die der Mersenne-Zahl 2 ^ ((n-1) * w) -1 . http://www.cplusplus.com/reference/random/mersenne_twister_engine/ entspricht.

Zufallszahlen-Engine subtrahieren -mit-tragen

Eine Pseudo-Zufallszahlengenerator-Engine, die vorzeichenlose Ganzzahlen erzeugt.

Der von dieser Engine verwendete Algorithmus ist ein verzögerter Fibonacci-Generator mit einer Zustandsfolge von r Integer-Elementen plus einem Übertragswert. http://www.cplusplus.com/reference/random/subtract_with_carry_engine/

Verzögerte Fibonacci-Generatoren haben eine maximale Periode von (2k - 1) * ^ (2M-1), wenn Addition oder Subtraktion verwendet wird. Die Initialisierung von LFGs ist ein sehr komplexes Problem. Die Ausgabe von LFGs ist sehr empfindlich gegenüber den Anfangsbedingungen, und statistische Defekte können anfänglich, aber auch periodisch in der Ausgabesequenz auftreten, sofern nicht äußerste Sorgfalt angewandt wird. Ein weiteres potenzielles Problem bei LFGs ist, dass die mathematische Theorie dahinter unvollständig ist, sodass statistische Tests anstelle von theoretischen Ergebnissen erforderlich sind. http://en.wikipedia.org/wiki/Lagged_Fibonacci_generator

Und zum Schluss: Die Wahl des zu verwendenden Motors erfordert eine Reihe von Kompromissen: Der lineare Kongruenzmotor ist mäßig schnell und hat einen sehr geringen Speicherbedarf für den Zustand. Die verzögerten Fibonacci-Generatoren sind selbst auf Prozessoren ohne fortgeschrittene arithmetische Befehlssätze sehr schnell, auf Kosten einer größeren Zustandsspeicherung und manchmal weniger erwünschter spektraler Eigenschaften. Der Mersenne-Twister ist langsamer und hat höhere Speicheranforderungen, aber mit den richtigen Parametern hat er die längste, sich nicht wiederholende Sequenz mit den wünschenswertesten Spektraleigenschaften (für eine gegebene Definition von wünschenswert). in http://en.cppreference.com/w/cpp/numeric/random

27
fatihk

Ich denke, der Punkt ist, dass Zufallsgeneratoren unterschiedliche Eigenschaften haben, wodurch sie für ein gegebenes Problem besser geeignet sind oder nicht.

  • Die Periodenlänge ist eine der Eigenschaften.
  • Die Qualität der Zufallszahlen kann auch wichtig sein.
  • Die Leistung des Generators kann ebenfalls ein Problem sein.

Je nach Bedarf können Sie einen oder einen anderen Generator verwenden. Wenn Sie beispielsweise schnelle Zufallszahlen benötigen, sich aber nicht wirklich für die Qualität interessieren, kann eine LCG eine gute Option sein. Wenn Sie bessere Zufallszahlen wünschen, ist der Mersenne Twister wahrscheinlich die bessere Option.

Um Ihnen bei der Auswahl zu helfen, gibt es einige Standardtests und -ergebnisse (Die Tabelle S. 29 von dieses Papier ) gefällt mir auf jeden Fall.


EDIT: Aus dem Papier, 

  1. Die LCG-Familie (LCG(***) in the paper) ist die schnellste Generation, jedoch mit der schlechtesten Qualität.
  2. Der Mersenne Twister (MT19937) ist etwas langsamer, liefert aber bessere Zufallszahlen.
  3. Die Subtraktion mit Carry (SWB(***), denke ich) ist zwar viel langsamer, kann aber bei guter Abstimmung bessere Zufallseigenschaften ergeben.
11
Dr_Sam

Wie die anderen Antworten über ranlux vergessen, ist hier ein kleiner Hinweis eines AMD-Entwicklers, der kürzlich auf OpenCL portiert hat:

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

RANLUX ist auch einer der wenigen (den einzigen, den ich eigentlich kenne), der PRNGs untermauert, der eine Theorie zugrunde legt, warum er "zufällige" Zahlen generiert und warum sie gut sind. Wenn die Theorie richtig ist (und ich kenne niemanden, der sie bestritten hat), produziert RANLUX auf höchstem Luxusniveau vollständig dekorrelierte Zahlen bis zum letzten Bit, ohne weitreichende Korrelationen, solange wir gut bleiben unter dem Zeitraum (10 ^ 171). Die meisten anderen Generatoren können nur sehr wenig über ihre Qualität sagen (wie Mersenne Twister, KISS usw.). Sie müssen sich auf statistische Tests verlassen.

Physiker am CERN sind Fan dieser PRNG. 'nuff sagte.

5
rubenvb

Einige der Informationen in diesen anderen Antworten stehen im Widerspruch zu meinen Befunden. Ich habe Tests unter Windows 8.1 mit Visual Studio 2013 ausgeführt, und durchweg habe ich festgestellt, dass mersenne_twister_engine jedoch von höherer Qualität und deutlich schneller als linear_congruential_engine oder subtract_with_carry_engine ist. Dies lässt mich glauben, wenn die Informationen in den anderen Antworten berücksichtigt werden, dass die spezifische Implementierung einer Engine einen erheblichen Einfluss auf die Leistung hat. 

Ich bin mir sicher, dass dies für niemanden eine große Überraschung ist, aber in den anderen Antworten, wo mersenne_twister_engine als langsamer gilt, nicht erwähnt. Ich habe keine Testergebnisse für andere Plattformen und Compiler, aber mit meiner Konfiguration ist mersenne_twister_engine eindeutig die bessere Wahl, wenn es um Zeit-, Qualitäts- und Geschwindigkeitsleistung geht. Ich habe keine Speicherbelegung erstellt, daher kann ich nicht mit der Eigenschaft Platzbedarf sprechen.

Hier ist der Code, mit dem ich teste (um tragbar zu sein, müssen Sie nur die windows.h QueryPerformanceXxx()-API-Aufrufe durch einen geeigneten Timing-Mechanismus ersetzen):

// 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;
}

Ausgabe:

 Testen mit 100000000 Proben. 
 Lineare_kongruentielle Motorzeit: 10.0821 
 Mersenne_twister_engine Zeit: 6.11615 
 Subtract_with_carry_engine time: 9.26676 
1
markamos

Im Allgemeinen ist der Mersenne Twister der beste (und schnellste) RNG, benötigt jedoch etwas Platz (etwa 2,5 Kilobyte). Welcher für Ihren Bedarf geeignet ist, hängt davon ab, wie oft Sie das Generatorobjekt instanziieren müssen. (Wenn Sie es nur einmal oder ein paar Mal instanziieren müssen, ist MT die zu verwendende Instanz. Wenn Sie es millionenfach instanziieren müssen, ist möglicherweise etwas kleiner.)

Einige Leute berichten, dass MT langsamer ist als andere. Laut meinen Experimenten hängt dies stark von den Einstellungen der Compiler-Optimierung ab. Am wichtigsten ist, dass die Einstellung -march = native je nach Host-Architektur einen großen Unterschied ausmachen kann.

Ich ließ ein kleines Programm laufen, um die Geschwindigkeit verschiedener Generatoren und deren Größe zu testen, und bekam folgendes:

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
1
Warp

Ich habe gerade diese Antwort von Marnos gesehen und beschlossen, sie selbst zu testen. Ich habe std::chono::high_resolution_clock verwendet, um 100000-Samples 100 mal zu berechnen, um einen Durchschnitt zu erzeugen. Ich habe alles in std::chrono::nanoseconds gemessen und endete mit unterschiedlichen Ergebnissen:

std::minstd_Rand hatte einen Durchschnitt von 28991658 Nanosekunden 

std::mt19937 hatte einen Durchschnitt von 29871710 Nanosekunden

ranlux48_base hatte einen Durchschnitt von 29281677 Nanosekunden

Dies ist auf einem Windows 7-Computer. Compiler ist Mingw-Builds 4.8.1 64bit. Dies ist offensichtlich das C++ 11-Flag und keine Optimierungsflags.

Wenn ich -O3-Optimierungen einschalte, laufen std::minstd_Rand und ranlux48_base tatsächlich schneller als das, was die Implementierung von high_precision_clock messen kann. std::mt19937 benötigt jedoch noch 730045 Nanosekunden oder 3/4 einer Sekunde.

Wie er sagte, ist die Implementierung spezifisch, aber zumindest in GCC scheint die durchschnittliche Zeit an dem zu bleiben, was die Beschreibungen in der akzeptierten Antwort sagen. Mersenne Twister scheint am wenigsten von Optimierungen zu profitieren, wohingegen die anderen beiden die Zufallszahlen einfach unglaublich schnell wegwerfen, wenn Sie an Compiler-Optimierungen denken. 

Abgesehen davon habe ich die Mersenne Twister-Engine in meiner Geräuschgenerierungsbibliothek verwendet (es werden keine Steigungen vorausberechnet). Ich denke, ich werde zu einem anderen wechseln, um die Geschwindigkeit zu verbessern. In meinem Fall spielt die "wahre" Zufälligkeit keine Rolle.

Code:

#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;
}
0
NeomerArcana

Es ist wirklich ein Kompromiss. Ein PRNG wie Mersenne Twister ist besser, weil er eine extrem lange Periode und andere gute statistische Eigenschaften aufweist. 

Eine große Periode PRNG beansprucht jedoch mehr Speicherplatz (zur Aufrechterhaltung des internen Zustands) und benötigt auch mehr Zeit zum Erzeugen einer Zufallszahl (aufgrund komplexer Übergänge und Nachbearbeitung).

Wählen Sie je nach den Anforderungen Ihrer Anwendung eine PNRG aus. Verwenden Sie im Zweifelsfall Mersenne Twister, ist dies in vielen Tools der Standard.

0
Nishanth