Dieser Beispielcode zeigt, dass std::rand
es sich um einen alten Frachtkult-Balderdash handelt, bei dem Ihre Augenbrauen jedes Mal hochgezogen werden sollten, wenn Sie ihn sehen.
Hier gibt es mehrere Probleme:
Die Vertragsleute gehen normalerweise davon aus - selbst die armen, unglücklichen Seelen, die es nicht besser wissen und nicht genau so denken -, dass rand
Stichproben aus der gleichmäßigen Verteilung auf die ganzen Zahlen in 0, 1, 2,… RAND_MAX
,, und jeder Aufruf ergibt eine unabhängige Stichprobe.
Das erste Problem besteht darin, dass der angenommene Vertrag, unabhängige einheitliche Zufallsstichproben bei jedem Aufruf, nicht den Angaben in der Dokumentation entspricht - und in der Praxis haben Implementierungen in der Vergangenheit nicht einmal das geringste Simulakrum der Unabhängigkeit geliefert. Zum Beispiel rand
sagt C99 §7.20.2.1 'Die Funktion' ohne Ausarbeitung:
Die rand
Funktion berechnet eine Folge von Pseudozufallszahlen im Bereich von 0 bis RAND_MAX
.
Dies ist ein bedeutungsloser Satz, da Pseudozufälligkeit eine Eigenschaft einer Funktion (oder Funktionsfamilie ) ist, nicht einer ganzen Zahl, aber das hindert nicht einmal ISO-Bürokraten daran, die Sprache zu missbrauchen. Schließlich wissen die einzigen Leser, die sich darüber aufregen würden, besser, als die Dokumentation zu lesen, rand
weil sie befürchten, dass ihre Gehirnzellen verfallen.
Eine typische historische Implementierung in C funktioniert folgendermaßen:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Dies hat die unglückliche Eigenschaft, dass eine einzelne Stichprobe , obwohl sie unter einem einheitlichen zufälligen Startwert (der vom spezifischen Wert von abhängt) gleichmäßig verteilt sein kann,RAND_MAX
bei aufeinanderfolgenden Aufrufen nachher zwischen geraden und ungeraden Ganzzahlen wechselt
int a = rand();
int b = rand();
Der Ausdruck (a & 1) ^ (b & 1)
ergibt 1 mit einer Wahrscheinlichkeit von 100%, was bei unabhängigen Zufallsstichproben für jede Verteilung, die auf geraden und ungeraden ganzen Zahlen unterstützt wird, nicht der Fall ist . So entstand ein Frachtkult, dass man die niederwertigen Teile wegwerfen sollte, um das schwer fassbare Tier der "besseren Zufälligkeit" zu jagen. (Spoiler-Alarm: Dies ist kein Fachbegriff. Dies ist ein Zeichen dafür, dass die Prosa, über die Sie lesen, entweder nicht weiß, wovon sie sprechen, oder dass Sie ahnungslos sind und sich herablassen müssen.)
Das zweite Problem ist, dass selbst wenn jeder Aufruf unabhängig von einer gleichmäßigen Zufallsverteilung auf 0, 1, 2, ... RAND_MAX
abgetastet rand() % 6
würde , das Ergebnis von nicht wie ein Würfel in 0, 1, 2, 3, 4, 5 gleichmäßig verteilt wäre roll, es RAND_MAX
sei denn, es ist kongruent zu -1 modulo 6. Einfaches Gegenbeispiel: Wenn RAND_MAX
= 6, dann rand()
haben alle Ergebnisse die gleiche Wahrscheinlichkeit 1/7, aber ab rand() % 6
hat das Ergebnis 0 die Wahrscheinlichkeit 2/7, während alle anderen Ergebnisse die Wahrscheinlichkeit 1/7 haben .
Der richtige Weg, dies zu tun, ist die Ablehnungsstichprobe: Ziehen Sie wiederholt eine unabhängige einheitliche Zufallsstichprobe s
aus 0, 1, 2,… RAND_MAX
und lehnen Sie (zum Beispiel) die Ergebnisse 0, 1, 2,… ab - ((RAND_MAX + 1) % 6) - 1
wenn Sie eine von erhalten diese fangen von vorne an; ansonsten ergeben s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
Auf diese Weise ist die Menge der Ergebnisse rand()
, die wir akzeptieren, gleichmäßig durch 6 teilbar, und jedes mögliche Ergebnis von s % 6
wird durch die gleiche Anzahl akzeptierter Ergebnisse von erhalten rand()
. Wenn rand()
es also gleichmäßig verteilt ist, ist dies auch der Fall s
. Die Anzahl der Versuche ist nicht begrenzt , aber die erwartete Anzahl beträgt weniger als 2, und die Erfolgswahrscheinlichkeit steigt exponentiell mit der Anzahl der Versuche.
Die Wahl, welche Ergebnisse rand()
Sie ablehnen, ist unerheblich, vorausgesetzt, Sie ordnen jeder Ganzzahl unter 6 eine gleiche Anzahl zu. Der Code auf cppreference.com trifft aufgrund des ersten oben genannten Problems eine andere Wahl: Es wird nichts über das garantiert Verteilung oder Unabhängigkeit der Ausgaben von rand()
und in der Praxis zeigten die niederwertigen Bits Muster, die nicht zufällig genug aussehen (egal, dass die nächste Ausgabe eine deterministische Funktion der vorherigen ist).
Übung für den Leser: Beweisen Sie, dass der Code auf cppreference.com eine gleichmäßige Verteilung auf Würfelrollen ergibt, wenn er rand()
eine gleichmäßige Verteilung auf 0, 1, 2,… , RAND_MAX
.
Übung für den Leser: Warum möchten Sie vielleicht die eine oder andere Teilmenge ablehnen? Welche Berechnung ist in beiden Fällen für jeden Versuch erforderlich?
Ein drittes Problem ist, dass der Samenraum so klein ist, dass selbst wenn der Samen gleichmäßig verteilt ist, ein Gegner, der mit Kenntnis Ihres Programms und einem Ergebnis, aber nicht dem Samen ausgestattet ist, den Samen und die nachfolgenden Ergebnisse leicht vorhersagen kann, was sie nicht so erscheinen lässt Immerhin zufällig. Denken Sie also nicht einmal daran, dies für die Kryptografie zu verwenden.
std::uniform_int_distribution
Mit einem geeigneten Zufallsgerät und Ihrer bevorzugten Zufalls-Engine wie dem allseits beliebten Mersenne-Twister können Sie die ausgefallene überentwickelte Route und die Klasse von C ++ 11 gehen std::mt19937
, um mit Ihrem vierjährigen Cousin Würfel zu spielen, aber selbst das wird nicht fit sein für kryptographischen Schlüssel Material-und die Mersenne Erzeugung Twister ein schrecklichen Raum Schwein mit einem Multi-Kilobyte Zustand verheerend auf Ihrem CPU-Cache mit einer obszönen Rüstzeit auch ist, so ist es schlecht , auch für ist, zB parallel Monte - Carlo - Simulationen mit reproduzierbare Bäume von Teilberechnungen; seine Popularität ergibt sich wahrscheinlich hauptsächlich aus seinem eingängigen Namen. Aber Sie können es für Spielzeugwürfel verwenden, die wie in diesem Beispiel rollen!
Ein anderer Ansatz besteht darin, einen einfachen kryptografischen Pseudozufallszahlengenerator mit einem kleinen Status zu verwenden, z. B. ein einfaches PRNG zum schnellen Löschen von Schlüsseln , oder nur eine Stream-Verschlüsselung wie AES-CTR oder ChaCha20, wenn Sie sicher sind ( z. B. in einer Monte-Carlo-Simulation für naturwissenschaftliche Forschung), dass die Vorhersage früherer Ergebnisse keine nachteiligen Folgen hat, wenn der Staat jemals kompromittiert wird.
std::uniform_int_distribution
für Würfel zu verwenden