Dlaczego rand () powtarza liczby znacznie częściej w systemie Linux niż Mac?

86

Wdrażałem hashap w C jako część projektu, nad którym pracuję i używam losowych wstawek do testowania go, gdy zauważyłem, że rand()w Linuksie wydaje się powtarzać liczby znacznie częściej niż na Macu. RAND_MAXto 2147483647 / 0x7FFFFFFF na obu platformach. Sprowadziłem go do tego programu testowego, który tworzy tablicę bajtów RAND_MAX+1-długą, generuje RAND_MAXliczby losowe, zauważa, czy każdy z nich jest duplikatem, i sprawdza go z listy, jak widać.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main() {
    size_t size = ((size_t)RAND_MAX) + 1;
    char *randoms = calloc(size, sizeof(char));
    int dups = 0;
    srand(time(0));
    for (int i = 0; i < RAND_MAX; i++) {
        int r = rand();
        if (randoms[r]) {
            // printf("duplicate at %d\n", r);
            dups++;
        }
        randoms[r] = 1;
    }
    printf("duplicates: %d\n", dups);
}

Linux konsekwentnie generuje około 790 milionów duplikatów. Mac konsekwentnie generuje tylko jeden, więc zapętla każdą losową liczbę, którą może wygenerować prawie bez powtarzania. Czy ktoś może mi wyjaśnić, jak to działa? Nie mogę powiedzieć nic innego niż strony podręcznika, nie mogę powiedzieć, którego RNG używa i nie mogę znaleźć niczego online. Dzięki!

Theron S.
źródło
4
Ponieważ rand () zwraca wartości od 0..RAND_MAX włącznie, twoja tablica musi mieć rozmiar RAND_MAX + 1
Blastfurnace
21
Być może zauważyłeś, że RAND_MAX / e ~ = 790 milionów. Również granica (1-1 / n) ^ n, gdy n zbliża się do nieskończoności, wynosi 1 / e.
David Schwartz
3
@DavidSchwartz Jeśli dobrze cię rozumiem, może to wyjaśniać, dlaczego liczba w Linuksie wynosi około 790 milionów. Wydaje mi się, że pytanie brzmi: dlaczego / jak Mac nie powtarza się tyle razy?
Theron S
26
W bibliotece wykonawczej nie ma wymagań dotyczących jakości PRNG. Jedynym prawdziwym wymaganiem jest powtarzalność z tym samym ziarnem. Najwyraźniej jakość PRNG w twoim systemie Linux jest lepsza niż na komputerze Mac.
pmg
4
@chux Tak, ale ponieważ jest oparty na pomnożeniu, stan nigdy nie może wynosić zero lub wynik (następny stan) również wynosiłby zero. Na podstawie kodu źródłowego sprawdza zero jako szczególny przypadek, jeśli jest zapełniany zerem, ale nigdy nie produkuje zera jako części sekwencji.
Arkku

Odpowiedzi:

118

Choć na początku może się wydawać, że macOS rand()jest w jakiś sposób lepszy, jeśli nie powtarza żadnych liczb, należy zauważyć, że przy takiej liczbie wygenerowanych liczb oczekuje się dużej liczby duplikatów (w rzeczywistości około 790 milionów lub (2 31 -1) ) / e ). Podobnie powtarzanie liczb w sekwencji również nie spowodowałoby duplikatów, ale nie byłoby uważane za bardzo przypadkowe. Tak więc rand()implementacja Linuksa jest w tym teście nie do odróżnienia od prawdziwego losowego źródła, podczas gdy macOS rand()nie.

Inną rzeczą, która wydaje się zaskakująca na pierwszy rzut oka, jest sposób, w jaki macOS rand()potrafi tak dobrze unikać duplikatów. Patrząc na jego kod źródłowy , stwierdzamy, że implementacja wygląda następująco:

/*
 * Compute x = (7^5 * x) mod (2^31 - 1)
 * without overflowing 31 bits:
 *      (2^31 - 1) = 127773 * (7^5) + 2836
 * From "Random number generators: good ones are hard to find",
 * Park and Miller, Communications of the ACM, vol. 31, no. 10,
 * October 1988, p. 1195.
 */
    long hi, lo, x;

    /* Can't be initialized with 0, so use another value. */
    if (*ctx == 0)
        *ctx = 123459876;
    hi = *ctx / 127773;
    lo = *ctx % 127773;
    x = 16807 * lo - 2836 * hi;
    if (x < 0)
        x += 0x7fffffff;
    return ((*ctx = x) % ((unsigned long) RAND_MAX + 1));

To rzeczywiście powoduje, że wszystkie liczby od 1 do RAND_MAXwłącznie, dokładnie jeden raz, przed powtórzeniem sekwencji. Ponieważ następny stan opiera się na pomnożeniu, stan nigdy nie może wynosić zero (lub wszystkie przyszłe stany również będą zerowe). Zatem powtarzana liczba, którą widzisz, jest pierwsza, a zero to ta, która nigdy nie jest zwracana.

Apple promuje stosowanie lepszych generatorów liczb losowych w ich dokumentacji i przykładach przez co najmniej tak długo, jak istnieje macOS (lub OS X), więc jakość rand()prawdopodobnie nie jest uważana za ważną i po prostu utknęli w jednym z najprostsze dostępne generatory pseudolosowe. (Jak już zauważyłeś, ich rand()komentarz został nawet opatrzony zaleceniem użycia arc4random()).

W pokrewnej uwadze, najprostszym generatorem liczb pseudolosowych, jaki udało mi się znaleźć, który daje przyzwoite wyniki w tym (i wielu innych) testach losowości, jest xorshift * :

uint64_t x = *ctx;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*ctx = x;
return (x * 0x2545F4914F6CDD1DUL) >> 33;

Ta implementacja daje prawie dokładnie 790 milionów duplikatów w teście.

Arkku
źródło
5
W artykule opublikowanym w latach 80. zaproponowano test statystyczny dla PRNG na podstawie „problemu urodzinowego”.
pjs
14
„Apple promuje stosowanie lepszych generatorów liczb losowych w swojej dokumentacji” -> oczywiście Apple może zastosować arc4random()podobny kod rand()i uzyskać dobry rand()wynik. Zamiast próbować sterować programistami inaczej, po prostu twórz lepsze funkcje biblioteczne. „właśnie utknęli” to ich wybór.
chux - Przywróć Monikę
22
brak stałego przesunięcia w systemie Mac rand()powoduje , że jest tak zły, że nie jest użyteczny w praktycznym użyciu: Dlaczego rand ()% 7 zawsze zwraca 0? , Rand ()% 14 generuje tylko wartości 6 lub 13
phuclv
4
@PeterCordes: Istnieje taki wymóg rand, aby ponowne uruchomienie go z tym samym materiałem siewnym wytworzyło tę samą sekwencję. OpenBSD randjest zepsuty i nie przestrzega tej umowy.
R .. GitHub ZATRZYMAJ LÓD
8
@ R..GitHubSTOPHELPINGICE Czy widzisz wymaganie C, aby rand()przy tym samym nasieniu produkować tę samą sekwencję między różnymi wersjami biblioteki? Taka gwarancja może być przydatna do testowania regresji między wersjami bibliotek, ale nie znajduję w niej wymagań C.
chux - Przywróć Monikę
33

MacOS zapewnia nieudokumentowaną funkcję rand () w stdlib. Jeśli pozostawisz to bez nasion, pierwsze wartości, które wyśle ​​to 16807, 282475249, 1622650073, 984943658 i 1144108930. Szybkie wyszukiwanie pokaże, że ta sekwencja odpowiada bardzo prostemu generatorowi liczb losowych LCG, który iteruje następującą formułę:

x n + 1 = 7 5 · x n (mod 2 31 - 1)

Ponieważ stan tego RNG jest opisany w całości przez wartość pojedynczej 32-bitowej liczby całkowitej, jego okres nie jest bardzo długi. Mówiąc ściślej, powtarza się co 2 31 - 2 iteracji, generując każdą wartość od 1 do 2 31 - 2.

Nie sądzę, aby istniała standardowa implementacja rand () dla wszystkich wersji Linuksa, ale często używana jest funkcja rand () glibc . Zamiast pojedynczej 32-bitowej zmiennej stanu wykorzystuje to pulę ponad 1000 bitów, która zgodnie ze wszystkimi celami i celami nigdy nie wytworzy w pełni powtarzalnej sekwencji. Ponownie, prawdopodobnie możesz dowiedzieć się, jaką wersję posiadasz, drukując kilka pierwszych wyników z tego RNG bez wcześniejszego uruchamiania. (Funkcja rand () glibc generuje liczby 1804289383, 846930886, 1681692777, 1714636915 i 1957747793.)

Powodem, dla którego masz więcej kolizji w Linuksie (a prawie wcale w MacOS) jest to, że wersja rand () Linuksa jest w zasadzie bardziej losowa.

r3mainer
źródło
5
unseeded rand()muszą zachowywać się jak jeden zsrand(1);
PMG
5
Kod źródłowy dla rand()macOS jest dostępny: opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/... FWIW, uruchomiłem ten sam test w stosunku do tego skompilowanego ze źródła i rzeczywiście powoduje to tylko jeden duplikat. Apple promuje użycie innych generatorów liczb losowych (takich jak arc4random()przed przejęciem Swift) w ich przykładach i dokumentacji, więc użycie ich rand()prawdopodobnie nie jest bardzo powszechne w natywnych aplikacjach na ich platformach, co może wyjaśniać, dlaczego nie jest lepsze.
Arkku
Dzięki za odpowiedź, która odpowiada na moje pytanie. A okres (2 ^ 31) -2 wyjaśnia, dlaczego zacznie się powtarzać na końcu, jak zauważyłem. Ty (@ r3mainer) powiedziałeś, że rand()był nieudokumentowany, ale @Arkku podał link do widocznego źródła. Czy któryś z was wie, dlaczego nie mogę znaleźć tego pliku w moim systemie i dlaczego widzę tylko int rand(void) __swift_unavailable("Use arc4random instead.");na komputerze Mac stdlib.h? Przypuszczam, że kod @Arkku, z którym jest połączony, jest po prostu wkompilowany w ... jaką bibliotekę?
Theron S
1
@TheronS To jest kompilowany do biblioteki C, libc, /usr/lib/libc.dylib. =)
Arkku
5
Która wersja rand()danego zastosowania program C nie jest określana przez „kompilator” lub „system operacyjny”, ale raczej wdrożenie standardowej biblioteki C (na przykład glibc, libc.dylib, msvcrt*.dll).
Peter O.
10

rand()jest zdefiniowany przez standard C, a standard C nie określa, którego algorytmu użyć. Oczywiście Apple używa gorszego algorytmu do implementacji GNU / Linux: Linuksa nie można odróżnić od prawdziwego losowego źródła w teście, podczas gdy implementacja Apple po prostu przetasowuje liczby.

Jeśli chcesz losowych liczb dowolnej jakości, albo użyj lepszego PRNG, który daje przynajmniej pewne gwarancje jakości liczb, które zwraca, lub po prostu odczytaj z /dev/urandomlub podobny. Później daje kryptograficzne wartości jakości, ale jest powolny. Nawet jeśli sam jest zbyt wolny, /dev/urandommoże zapewnić doskonałe nasiona innym, szybszym PRNG.

cmaster - przywróć monikę
źródło
Dziękuję za odpowiedź. Tak naprawdę nie potrzebuję dobrego PRNG, po prostu martwiłem się, że w moim haszapie czai się jakieś nieokreślone zachowanie, a potem zainteresowałem się, gdy wyeliminowałem tę możliwość, a platformy nadal zachowywały się inaczej.
Theron S
btw oto przykład kryptograficznie bezpiecznego generatora liczb losowych: github.com/divinity76/phpcpp/commit/… - ale to C ++ zamiast C i pozwalam implementatorom STL wykonywać wszystkie zadania.
hanshenrik
3
@hanshenrik Kryptowaluty RNG są na ogół przesadzone i zbyt wolne, aby można było zastosować prostą tabelę skrótów.
PM 2,
1
@ PM2Ring Absolutnie. Skrót tabeli skrótów musi przede wszystkim być szybki, a nie dobry. Jeśli jednak chcesz opracować algorytm tabeli skrótów, który jest nie tylko szybki, ale także przyzwoity, uważam, że dobrze jest znać niektóre sztuczki kryptograficznych algorytmów skrótów. Pomoże Ci to uniknąć większości rażących błędów, które rozwiązują najszybsze algorytmy mieszania. Niemniej jednak nie reklamowałbym się tutaj dla konkretnego wdrożenia.
cmaster
@cmaster Wystarczająco prawda. Z pewnością dobrym pomysłem jest zapoznanie się z funkcjami miksowania i efektem lawinowym . Na szczęście istnieją nieszyfrowe funkcje skrótu o dobrych właściwościach, które nie poświęcają zbyt dużej prędkości (jeśli są poprawnie zaimplementowane), np. Xxhash, murmur3 lub siphash.
PM 2, dzwoni
5

Ogólnie rzecz biorąc, para rand / srand była uważana za przestarzałą przez długi czas z powodu bitów niskiego rzędu, które wykazują mniej losowości niż bity wysokiego rzędu w wynikach. To może, ale nie musi mieć nic wspólnego z twoimi wynikami, ale myślę, że nadal jest to dobra okazja, aby pamiętać, że mimo iż niektóre implementacje rand / srand są teraz bardziej aktualne, starsze implementacje są nadal dostępne i lepiej używać losowych (3). ). W moim Arch Linuxie następująca uwaga jest wciąż na stronie podręcznika dla rand (3):

  The versions of rand() and srand() in the Linux C Library use the  same
   random number generator as random(3) and srandom(3), so the lower-order
   bits should be as random as the higher-order bits.  However,  on  older
   rand()  implementations,  and  on  current implementations on different
   systems, the lower-order bits are much less random than the  higher-or-
   der bits.  Do not use this function in applications intended to be por-
   table when good randomness is needed.  (Use random(3) instead.)

Tuż poniżej tego strona podręcznika podaje bardzo krótkie, bardzo proste przykładowe implementacje rand i srand, które dotyczą najprostszych LC RNG, jakie kiedykolwiek widziałeś i mają małą RAND_MAX. Nie sądzę, żeby pasowały do ​​tego, co jest w standardowej bibliotece C, jeśli kiedykolwiek tak było. A przynajmniej mam nadzieję, że nie.

Zasadniczo, jeśli zamierzasz użyć czegoś ze standardowej biblioteki, użyj losowo, jeśli możesz (strona podręcznika wymienia to jako standard POSIX z powrotem do POSIX.1-2001, ale rand jest standardem znacznie wcześniej niż C został znormalizowany) . Albo jeszcze lepiej: otwórz przepisy numeryczne (lub poszukaj ich online) lub Knuth i zaimplementuj je. Są naprawdę łatwe i naprawdę musisz to zrobić tylko raz, aby mieć RNG ogólnego przeznaczenia z atrybutami, których najczęściej potrzebujesz i który ma znaną jakość.

Thomas Kammeyer
źródło
Dzięki za kontekst. Tak naprawdę nie potrzebuję losowości wysokiej jakości i zaimplementowałem MT19937, chociaż w Rust. Był głównie ciekawy, jak dowiedzieć się, dlaczego obie platformy zachowywały się inaczej.
Theron S
1
Czasami najlepsze pytania są zadawane z prostego zainteresowania, a nie ze ścisłej potrzeby - wydaje się, że to one często rodzą zestaw dobrych odpowiedzi z określonego punktu zainteresowania. Twoja jest jedną z nich. Oto wszystkie ciekawe osoby, prawdziwi i oryginalni hakerzy.
Thomas Kammeyer
Zabawne, że rada polegała na „zaprzestaniu używania rand ()” zamiast ulepszania rand (). Nic w tym standardzie nigdy nie mówi, że musi to być konkretny generator.
rura
2
@pipe Jeśli rand()ulepszenie oznaczałoby spowolnienie (co zapewne zrobiłoby - zabezpieczone kryptograficznie liczby losowe wymagają dużo wysiłku), prawdopodobnie lepiej jest zachować je szybko, nawet jeśli są nieco bardziej przewidywalne. Przykład: mieliśmy aplikację produkcyjną, której uruchomienie trwało wieki, którą prześledziliśmy do RNG, którego inicjalizacja musiała czekać na wygenerowanie wystarczającej entropii… Okazało się, że nie musiała być tak bezpieczna, więc zastąpiliśmy ją „gorszy” RNG był dużą poprawą.
gidds