Mersenne-Twister
Der Mersenne-Twister ist ein Pseudozufallszahlengenerator, der 1997 von Makoto Matsumoto und Takuji Nishimura entwickelt wurde. Er generiert Sequenzen von Pseudozufallszahlen und wurde darauf zugeschnitten, die Probleme älterer Algorithmen zu überwinden (wie z. B. linearer Kongruenzgeneratoren).
Es gibt zwei Varianten dieses Algorithmus; die neuere und weiter verbreitete ist der Mersenne-Twister „MT 19937“, der hier beschrieben wird.
Eigenschaften
- Extrem lange Periode von . Diese Periodenlänge erklärt auch den Namen des Algorithmus: Sie ist eine Mersenne-Primzahl, und einige Eigenschaften des Algorithmus resultieren daraus.
- Alle Bits der Ausgabesequenz sind gleichverteilt. Somit sind die zurückgelieferten Integer-Werte ebenfalls hochgradig gleichverteilt (bis zur Dimension 623, siehe unten). Daraus folgt eine extrem geringe Korrelation zwischen aufeinanderfolgenden Wertefolgen der Ausgabesequenz.
- Der Algorithmus ist schnell. Er generiert immer 624 neue Zustandswörter auf einmal, die er bei den nächsten 624 Aufrufen dann Wert für Wert zurückliefert. Die Neuberechnung des Zustandsvektors lässt sich auf SIMD-Rechnerarchitekturen fast beliebig parallelisieren, was der Ausführungsgeschwindigkeit zugutekommt.
Andererseits hat er den Nachteil, auf einer großen Datenmenge von etwa 2,5 kByte (624 Wörter mit je 32 Bits) zu arbeiten. Das kann bei Rechnerarchitekturen mit relativ kleinem Cache und langsamerem Arbeitsspeicher einen Geschwindigkeitsnachteil ergeben.
Das Wort „Twister“ bezieht sich auf eine bestimmte Transformation innerhalb des Algorithmus, durch die diese hochgradige Gleichverteilung sichergestellt wird (reine lineare Kongruenzgeneratoren können mit vertretbarem Aufwand nur fünfdimensionale Gleichverteilung garantieren).
Eine n-dimensionale Gleichverteilung heißt: teilt man die Ausgabesequenz in Tupel von je n Zahlen, dann ist die Sequenz der n-Tupel gleichverteilt im n-dimensionalen Raum.
Im Gegensatz zu anderen Algorithmen ist der Mersenne-Twister in seiner Reinform nicht kryptographisch sicher. Für viele andere Anwendungen wird er aber bereits erfolgreich verwendet.
Algorithmus
Die Werte bis (mit ) werden als Startwerte vorgegeben. Die weiteren Werte mit werden folgendermaßen berechnet:
Das Symbol bezeichnet die bitweise XOR-Verknüpfung, und „hex“ steht für hexadezimal. Das Symbol ist die Gaußklammer und steht für den abgerundeten Wert, d. h. die größte Ganzzahl, die nicht größer als das Argument in der Klammer ist.
Um die 623-dimensionale Gleichverteilung für alle 32 Bits der sicherzustellen, werden die noch modifiziert:
Dabei steht für die bitweise UND-Verknüpfung.
Die so berechneten werden als Zufallszahlen verwendet.
Initialisierung
Als Startwerte bis wählt man im Idealfall echte Zufallszahlen, die z. B. durch einen physikalischen Zufallszahlengenerator erzeugt werden können. Es können aber auch Pseudozufallszahlen von einem anderen Generator verwendet werden.
Es dürfen nicht alle Bits, die den Zustand des Mersenne-Twisters ausmachen, mit Null initialisiert werden, denn sonst erzeugt er immer nur Null als „Zufallszahl“. Dies sind das höchstwertige Bit in sowie alle Bits in den übrigen Variablen bis .
Je weniger zufällig die Startwerte sind (d. h. je ungleicher die Bits verteilt sind), umso länger ist die „Aufwärmphase“, die der Mersenne-Twister braucht, bis er gute Pseudozufallszahlen ausgibt. Die schlechtest mögliche Initialisierung besteht aus nur einem einzigen gesetzten Bit im Initialisierungsvektor. In diesem Fall benötigt der Mersenne-Twister über 700.000 Aufrufe, bis er wieder eine gleichverteilte Bitsequenz liefert.[1] Im Zweifelsfall sollte man also etwa 800.000 Zufallszahlen generieren lassen, bevor man die Zahlen verwendet. Alternativ existieren auch moderne Generatoren, die wesentlich kürzere Erholungszeiten besitzen, wie z. B. der WELL oder Marsaglias Xorshift.
Allerdings kann man sich auf diese Weise auch die Initialisierung mit einem weiteren PRNG sparen (falls man diesem bspw. nicht traut): Man setzt (im Code y[1]
) auf einen zufälligen Seed-Wert (z. B. die Uhrzeit) und alle weitere auf 0 (im C-Code sind sie das i. d. R. wegen des static
-Attributs bereits). Anschließend ruft man den Generator einfach 800.000 mal auf.
Code
Diese Berechnungen lassen sich z. B. in C-Code effizient implementieren. Die folgende Funktion berechnet immer N = 624 Wörter auf einmal, und danach werden diese aus dem Vektor y
der Reihe nach ausgelesen:
#include <stdint.h>
#define N 624
#define M 397
/*
* Initialisiere den Vektor mit Pseudozufallszahlen.
* Siehe Donald E. Knuth: The Art of Computer Programming, Band 2, 3. Ausgabe, S. 106 für geeignete Werte für den Faktor mult
*
* 2002/01/09 modifiziert von Makoto Matsumoto:
* In der vorhergehenden Version die MSBs des Seeds haben auch nur die MSBs des Arrays beeinflusst.
*/
static void
mersenne_twister_vector_init (uint32_t* const p, const int len)
{
const uint32_t mult = 1812433253ul;
uint32_t seed = 5489ul;
int i;
for (i = 0; i < len; i++)
{
p[i] = seed;
seed = mult * (seed ^ (seed >> 30)) + (i+1);
}
}
/*
* Berechne neuen Zustandsvektor aus altem Zustandsvektor
* Dies erfolgt periodisch alle N=624 Aufrufe von mersenne_twister().
*
* Der folgende Code stellt eine optimierte Version von
* ...
* for (i = 0; i < N; i++)
* Proc3 (p[i], p[(i+1) % N], p[(i+M) % N]);
* ...
* mit
* void Proc3 (uint32_t &a0, uint32_t a1, uint32_t aM)
* {
* a0 = aM ^ (((a0 & 0x80000000) | (a1 & 0x7FFFFFFF)) >> 1) ^ A[a1 & 1];
* }
* dar, in der die (zeitintensiven) Modulo N-Operationen vermieden werden.
*/
static void
mersenne_twister_vector_update (uint32_t* const p)
{
static const uint32_t A[2] = { 0, 0x9908B0DF };
int i = 0;
for (; i < N-M; i++)
p[i] = p[i+(M )] ^ (((p[i ] & 0x80000000) | (p[i+1] & 0x7FFFFFFF)) >> 1) ^ A[p[i+1] & 1];
for (; i < N-1; i++)
p[i] = p[i+(M-N)] ^ (((p[i ] & 0x80000000) | (p[i+1] & 0x7FFFFFFF)) >> 1) ^ A[p[i+1] & 1];
p[N-1] = p[M-1] ^ (((p[N-1] & 0x80000000) | (p[0 ] & 0x7FFFFFFF)) >> 1) ^ A[p[0 ] & 1];
}
/*
* Die eigentliche Funktion:
* - beim 1. Aufruf wird der Vektor mittels mersenne_twister_vector_init() initialisiert.
* - beim 1., 625., 1249., ... Aufruf wird ein neuer Vektor mittels mersenne_twister_vector_update berechnet.
* - bei jedem Aufruf wird eine Zufallszahl aus dem Vektor gelesen und noch einem Tempering unterzogen.
*/
uint32_t
mersenne_twister ()
{
static uint32_t vektor [N]; /* Zustandsvektor */
static int idx = N+1; /* Auslese-Index; idx >= N: neuer Vektor muss berechnet werden */
/* idx = N+1: Vektor muss initialisiert werden */
uint32_t e;
if (idx >= N)
{
if (idx > N)
mersenne_twister_vector_init (vektor, N);
mersenne_twister_vector_update (vektor);
idx = 0;
}
e = vektor [idx++];
e ^= (e >> 11); /* Tempering */
e ^= (e << 7) & 0x9D2C5680;
e ^= (e << 15) & 0xEFC60000;
e ^= (e >> 18);
return e;
}
#undef N
#undef M
TT800
Matsumoto und Nishimura entwickelten zuvor bereits einen „kleinen Bruder“ des Mersenne-Twisters mit der Bezeichnung TT800. Er arbeitet nach dem gleichen Funktionsprinzip, aber auf einer kleineren Datenmenge von nur 25 Wörtern, und sein Algorithmus ist ein wenig einfacher, weil zur Berechnung des jeweils nächsten Zustandswortes nicht drei, sondern nur zwei alte 32-bit-Zustandworte verrechnet werden. Seine Periodenlänge beträgt .
#include <stdint.h>
#define N 25
#define M 7
/*
* Initialisiere den Vektor mit Pseudozufallszahlen.
*/
static void
TT800_vector_init (uint32_t* const p, const int len)
{
const uint32_t mult = 509845221ul;
const uint32_t add = 3ul;
uint32_t seed1 = 9;
uint32_t seed2 = 3402;
int i;
for (i = 0; i < len; i++)
{
seed1 = seed1 * mult + add;
seed2 *= seed2 + 1;
p[i] = seed2 + (seed1 >> 10);
}
}
/*
* Berechne neuen Zustandsvektor aus altem Zustandsvektor
* Dies erfolgt periodisch alle N=25 Aufrufe von TT800().
*
* Der folgende Code stellt eine optimierte Version von
* ...
* for (i = 0; i < N; i++)
* Proc2 (p[i], p[(i+M) % N]);
* ...
* mit
* void Proc2 (uint32_t &a0, uint32_t aM)
* {
* a0 = aM ^ (a0 >> 1) ^ A[a0 & 1];
* }
* dar, in der die (zeitintensiven) Modulo N-Operationen vermieden werden.
*/
static void
TT800_vector_update (uint32_t* const p)
{
static const uint32_t A[2] = { 0, 0x8ebfd028 };
int i = 0;
for (; i < N-M; i++)
p[i] = p[i+(M )] ^ (p[i] >> 1) ^ A[p[i] & 1];
for (; i < N ; i++)
p[i] = p[i+(M-N)] ^ (p[i] >> 1) ^ A[p[i] & 1];
}
/*
* Die eigentliche Funktion:
* - beim 1. Aufruf wird der Vektor mittels TT800_vector_init() initialisiert.
* - beim 1., 26., 51., ... Aufruf wird ein neuer Vektor mittels TT800_vector_update berechnet.
* - bei jedem Aufruf wird eine Zufallszahl aus dem Vektor ausgelesen und noch einem Tempering unterzogen.
*/
uint32_t
TT800 ()
{
static uint32_t vektor [N]; /* Zustandsvektor */
static int idx = N+1; /* Auslese-Index; idx >= N: neuer Vektor muss berechnet werden, */
/* idx=N+1: Vektor muss überhaupt erst mal initialisiert werden */
uint32_t e;
if (idx >= N)
{
if (idx > N)
TT800_vector_init (vektor, N);
TT800_vector_update (vektor);
idx = 0;
}
e = vektor [idx++];
e ^= (e << 7) & 0x2b5b2500; /* Tempering */
e ^= (e << 15) & 0xdb8b0000;
e ^= (e >> 16);
return e;
}
#undef N
#undef M
Siehe auch
Literatur
Weblinks
- Webpräsenz des Mersenne Twister (englisch)
Einzelnachweise
- iro.umontreal.ca (PDF; 301 kB)