Ten przykładowy kod pokazuje, że std::rand
jest to przypadek legendarnego baldaszka kultowego cargo, który powinien podnosić brwi za każdym razem, gdy go widzisz.
Jest tu kilka problemów:
Ludzie kontraktowi zwykle zakładają - nawet biedne, nieszczęsne dusze, które nie wiedzą nic lepszego i nie będą myśleć o tym dokładnie w ten sposób - są takie, że rand
próbki z jednolitego rozkładu liczb całkowitych w 0, 1, 2,… RAND_MAX
,, a każde wywołanie daje niezależną próbkę.
Pierwszy problem polega na tym, że założona umowa, niezależne, jednolite losowe próbki w każdym zaproszeniu, nie jest w rzeczywistości tym, co mówi dokumentacja - aw praktyce historycznie wdrożenia nie zapewniły nawet najdrobniejszego symulakru niezależności. Na przykład C99 §7.20.2.1 „ rand
Funkcja” mówi bez rozwinięcia:
rand
Funkcja wylicza sekwencję liczb pseudo-losowych w przedziale od 0 do RAND_MAX
.
To bezsensowne zdanie, ponieważ pseudolosowość jest właściwością funkcji (lub rodziny funkcji ), a nie liczby całkowitej, ale to nie powstrzymuje nawet biurokratów ISO przed nadużywaniem języka. W końcu jedyni czytelnicy, którzy byliby tym zdenerwowani, wiedzą, że lepiej nie czytać dokumentacji rand
ze strachu przed rozpadem ich komórek mózgowych.
Typowa historyczna implementacja w C działa tak:
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;
}
Ma to niefortunną właściwość, że nawet jeśli pojedyncza próbka może być równomiernie rozłożona w ramach jednolitego losowego ziarna (co zależy od określonej wartości RAND_MAX
), naprzemiennie zmienia się między parzystymi i nieparzystymi liczbami całkowitymi w kolejnych wywołaniach - po
int a = rand();
int b = rand();
wyrażenie (a & 1) ^ (b & 1)
daje 1 ze 100% prawdopodobieństwem, co nie ma miejsca w przypadku niezależnych prób losowych w dowolnym rozkładzie obsługiwanym przez parzyste i nieparzyste liczby całkowite. W ten sposób pojawił się kult cargo, w którym należy odrzucić mniej znaczące bity, aby ścigać nieuchwytną bestię o „lepszej losowości”. (Uwaga spoilera: to nie jest termin techniczny. To znak, że ktokolwiek czytasz prozę, albo nie wie, o czym mówi, albo myśli , że nie masz pojęcia i musi być protekcjonalny.)
Drugi problem polega na tym, że nawet gdyby każde wywołanie próbowało niezależnie od jednolitego losowego rozkładu na 0, 1, 2,… RAND_MAX
,, wynik rand() % 6
nie byłby równomiernie rozłożony na 0, 1, 2, 3, 4, 5 jak kostka rzuć, chyba że RAND_MAX
jest przystająca do -1 modulo 6. Prosty kontrprzykład: Jeśli RAND_MAX
= 6, to z rand()
, wszystkie wyniki mają równe prawdopodobieństwo 1/7, ale z rand() % 6
, wynik 0 ma prawdopodobieństwo 2/7, podczas gdy wszystkie inne wyniki mają prawdopodobieństwo 1/7 .
Właściwym sposobem na to jest próbkowanie odrzucenia: wielokrotnie losuj niezależną, jednolitą próbkę losową s
z 0, 1, 2,… RAND_MAX
i odrzucaj (na przykład) wyniki 0, 1, 2,… - ((RAND_MAX + 1) % 6) - 1
jeśli otrzymasz jeden z te, zacznij od nowa; w przeciwnym razie wydajność s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
W ten sposób zbiór wyników z rand()
, który akceptujemy, jest podzielny po równo przez 6, a każdy możliwy wynik z s % 6
jest uzyskiwany przez tę samą liczbę zaakceptowanych wyników z rand()
, więc jeśli rand()
jest równomiernie rozłożony, to tak jest s
. Nie ma ograniczeń co do liczby prób, ale oczekiwana liczba jest mniejsza niż 2, a prawdopodobieństwo sukcesu rośnie wykładniczo wraz z liczbą prób.
Wybór , który Efekty o rand()
odrzuceniu ma znaczenia, pod warunkiem, że mapa równą liczbę nich do każdej liczby całkowitej poniżej 6. Kod na cppreference.com sprawia, że inny wybór, bo od pierwszego problemu wyżej, że nic nie jest gwarantowane o dystrybucja lub niezależność wyników rand()
, aw praktyce bity niskiego rzędu wykazywały wzorce, które nie „wyglądają wystarczająco losowo” (nie wspominając o tym, że następny wynik jest deterministyczną funkcją poprzedniego).
Ćwiczeń dla czytelnika: udowodnić, że kod w cppreference.com uzyskuje się równomierne rozprowadzenie na matrycy rolek, jeżeli rand()
wydajność rozkład jednolity o 0, 1, 2, ..., RAND_MAX
.
Ćwiczenie dla czytelnika: Dlaczego wolałbyś odrzucić jeden lub drugi podzbiór? Jakie obliczenia są potrzebne dla każdego procesu w dwóch przypadkach?
Trzeci problem polega na tym, że przestrzeń nasienna jest tak mała, że nawet jeśli ziarno jest równomiernie rozłożone, przeciwnik uzbrojony w wiedzę o twoim programie i jednym wyniku, ale nie w ziarnie, może łatwo przewidzieć ziarno i późniejsze wyniki, co sprawia, że nie wydają się takie w końcu losowe. Więc nawet nie myśl o używaniu tego do kryptografii.
Możesz przejść fantazyjną, nadmiernie inżynierską trasę i std::uniform_int_distribution
klasę C ++ 11 z odpowiednim losowym urządzeniem i ulubionym losowym silnikiem, takim jak zawsze popularny twister Mersenne, std::mt19937
aby grać w kości ze swoim czteroletnim kuzynem, ale nawet to nie będzie być zdolnym do generowania materiału klucza kryptograficznego - a twister Mersenne jest również strasznym świrem kosmicznym ze stanem wielokilobajtowym siejącym spustoszenie w pamięci podręcznej procesora z nieprzyzwoitym czasem konfiguracji, więc jest zły nawet dla np. równoległych symulacji Monte Carlo z odtwarzalne drzewa obliczeń podrzędnych; jego popularność prawdopodobnie wynika głównie z chwytliwej nazwy. Ale możesz go użyć do rzucania zabawkowymi kośćmi, jak na tym przykładzie!
Innym podejściem jest użycie prostego kryptograficznego generatora liczb pseudolosowych z małym stanem, na przykład prostego szybkiego usuwania klucza PRNG , lub po prostu szyfrowania strumieniowego, takiego jak AES-CTR lub ChaCha20, jeśli masz pewność ( np. W symulacji Monte Carlo dla badania w naukach przyrodniczych), że nie ma żadnych negatywnych konsekwencji w przewidywaniu przeszłych skutków, jeśli państwo kiedykolwiek zostanie zagrożone.
std::uniform_int_distribution
do gry w kości