Perché rand () ripete i numeri molto più spesso su Linux che su Mac?


88

Stavo implementando un hashmap in C come parte di un progetto a cui sto lavorando e usando inserimenti casuali per testarlo quando ho notato che rand()su Linux sembra ripetere numeri molto più spesso che su Mac. RAND_MAXè 2147483647 / 0x7FFFFFFF su entrambe le piattaforme. L'ho ridotto a questo programma di test che rende un array di byte RAND_MAX+1lungo, genera RAND_MAXnumeri casuali, note se ciascuno è un duplicato e lo controlla dall'elenco come visto.

#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 genera costantemente circa 790 milioni di duplicati. Il Mac genera costantemente solo uno, quindi scorre attraverso ogni numero casuale che può generare quasi senza ripetere. Qualcuno può spiegarmi come funziona? Non posso dire nulla di diverso dalle pagine man, non posso dire quale RNG sta usando ciascuno e non riesco a trovare nulla online. Grazie!


4
Poiché rand () restituisce valori da 0..RAND_MAX inclusi, l'array deve essere dimensionato RAND_MAX + 1
Blastfurnace

21
Forse avrai notato che RAND_MAX / e ~ = 790 milioni. Anche il limite di (1-1 / n) ^ n quando n si avvicina all'infinito è 1 / e.
David Schwartz,

3
@DavidSchwartz Se ti capisco correttamente, ciò potrebbe spiegare perché il numero su Linux è costantemente circa 790 milioni. Immagino che la domanda sia: perché / come Mac non si ripete così tante volte?
Theron S

26
Non ci sono requisiti di qualità per il PRNG nella libreria di runtime. L'unico vero requisito è la ripetibilità con lo stesso seme. Apparentemente, la qualità del PRNG nel tuo Linux è migliore che nel tuo Mac.
pm

4
@chux Sì, ma poiché si basa sulla moltiplicazione, lo stato non può mai essere zero o anche il risultato (stato successivo) sarebbe zero. In base al codice sorgente, controlla se lo zero è un caso speciale se viene eseguito il seeding con zero, ma non produce mai zero come parte della sequenza.
Arkku,

Risposte:


119

Mentre all'inizio può sembrare che macOS rand()sia in qualche modo migliore per non ripetere alcun numero, si dovrebbe notare che con questa quantità di numeri generati si prevede di vedere molti duplicati (in effetti, circa 790 milioni, o (2 31 -1 -1 ) / e ). Allo stesso modo, iterare i numeri in sequenza non produrrebbe duplicati, ma non sarebbe considerato molto casuale. Quindi l' rand()implementazione di Linux è in questo test indistinguibile da una vera fonte casuale, mentre macOS rand()non lo è.

Un'altra cosa che sembra sorprendente a prima vista è come MacOS rand()riesce a evitare così bene i duplicati. Guardando il suo codice sorgente , troviamo che l'implementazione è la seguente:

/*
 * 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));

Ciò comporta effettivamente tutti i numeri compresi tra 1 e RAND_MAX, compreso, esattamente una volta, prima che la sequenza si ripeta nuovamente. Poiché lo stato successivo si basa sulla moltiplicazione, lo stato non può mai essere zero (o anche tutti gli stati futuri sarebbero zero). Quindi il numero ripetuto che vedi è il primo e zero è quello che non viene mai restituito.

Apple ha promosso l'uso di generatori di numeri casuali migliori nella loro documentazione ed esempi almeno da quando esiste macOS (o OS X), quindi la qualità di rand()probabilmente non è considerata importante e si sono semplicemente bloccati con uno di i generatori pseudocasuali più semplici disponibili. (Come hai notato, loro rand()è anche commentato con una raccomandazione da usare arc4random()invece.)

Su una nota correlata, il più semplice generatore di numeri pseudocasuali che ho trovato che produce risultati decenti in questo (e molti altri) test per casualità è xorshift * :

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

Questa implementazione genera quasi esattamente 790 milioni di duplicati nel test.


5
Un articolo di rivista pubblicato negli anni '80 ha proposto un test statistico per i PRNG basato sul "problema del compleanno".
PJS

14
"Apple ha promosso l'uso di generatori di numeri casuali migliori nella loro documentazione" -> ovviamente Apple potrebbe impiegare arc4random()come codice dietro rand()e ottenere un buon rand()risultato. Invece di provare a guidare i programmatori a programmare diversamente, basta creare funzioni di libreria migliori. "Hanno appena bloccato" è la loro scelta.
chux - Ripristina Monica il

23
la mancanza di un offset costante nei mac lo rand()rende così grave che non è utile per un uso pratico: perché rand ()% 7 restituisce sempre 0? , Rand ()% 14 genera solo i valori 6 o 13
phuclv

4
@PeterCordes: Esiste un tale requisito rand, che rieseguendolo con lo stesso seme produce la stessa sequenza. OpenBSD randè rotto e non obbedisce a questo contratto.
R .. GitHub smette di aiutare ICE il

8
@ R..GitHubSTOPHELPINGICE Vedi un requisito C che rand()con lo stesso seme produce la stessa sequenza tra versioni diverse della libreria? Tale garanzia potrebbe essere utile per i test di regressione tra le versioni della libreria, ma non trovo alcun requisito C per questo.
chux - Ripristina Monica il

34

MacOS fornisce una funzione rand () non documentata in stdlib. Se lo lasci senza semi, i primi valori che emette sono 16807, 282475249, 1622650073, 984943658 e 1144108930. Una rapida ricerca mostrerà che questa sequenza corrisponde a un generatore di numeri casuali LCG di base che ripete la seguente formula:

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

Poiché lo stato di questo RNG è interamente descritto dal valore di un singolo numero intero a 32 bit, il suo periodo non è molto lungo. Per essere precisi, si ripete ogni 2 31 - 2 iterazioni, generando ogni valore da 1 a 2 31 - 2.

Non penso che ci sia un'implementazione standard di rand () per tutte le versioni di Linux, ma c'è una funzione glibc rand () che viene spesso usata. Invece di una singola variabile di stato a 32 bit, utilizza un pool di oltre 1000 bit, che a tutti gli effetti non produrrà mai una sequenza completamente ripetitiva. Ancora una volta, puoi probabilmente scoprire quale versione hai stampando i primi pochi output di questo RNG senza prima eseguirne il seeding. (La funzione glibc rand () produce i numeri 1804289383, 846930886, 1681692777, 1714636915 e 1957747793.)

Quindi il motivo per cui stai ottenendo più collisioni in Linux (e quasi in MacOS) è che la versione Linux di rand () è sostanzialmente più casuale.


5
un nonrand()srand(1);
seme

5
Il codice sorgente per rand()in macOS è disponibile: opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/… FWIW, ho eseguito lo stesso test su questo compilato dal sorgente e in effetti risulta solo un duplicato. Apple ha promosso l'uso di altri generatori di numeri casuali (come arc4random()prima che Swift prendesse il sopravvento) nei loro esempi e documentazione, quindi l'uso di rand()probabilmente non è molto comune nelle app native sulle loro piattaforme, il che potrebbe spiegare perché non è meglio.
Arkku,

Grazie per la risposta, che risponde alla mia domanda. E un periodo di (2 ^ 31) -2 spiega perché alla fine comincerebbe a ripetersi come ho osservato. Tu (@ r3mainer) hai dichiarato di rand()non avere documenti, ma @Arkku ha fornito un link alla fonte apparente. Qualcuno di voi sa perché non riesco a trovare quel file sul mio sistema e perché lo vedo solo int rand(void) __swift_unavailable("Use arc4random instead.");su Mac stdlib.h? Suppongo che il codice a cui è collegato @Arkku sia appena compilato in ... quale libreria?
Theron S

1
@TheronS è compilato nella libreria C, libc, /usr/lib/libc.dylib. =)
Arkku,

5
Quale versione di rand()un dato programma usa C non è determinato dal "compilatore" o il "sistema operativo", ma piuttosto l'attuazione della libreria standard C (ad esempio, glibc, libc.dylib, msvcrt*.dll).
Peter O.

10

rand()è definito dallo standard C e lo standard C non specifica quale algoritmo utilizzare. Ovviamente, Apple sta usando un algoritmo inferiore per l'implementazione GNU / Linux: quello Linux è indistinguibile da una vera fonte casuale nel tuo test, mentre l'implementazione Apple mescola semplicemente i numeri.

Se vuoi numeri casuali di qualsiasi qualità, usa un PRNG migliore che offra almeno alcune garanzie sulla qualità dei numeri che restituisce, o semplicemente leggi da /dev/urandomo simili. Il successivo ti dà numeri di qualità crittografica, ma è lento. Anche se è troppo lento da solo, /dev/urandompuò fornire alcuni semi eccellenti ad altri PRNG più veloci.


Grazie per la risposta. In realtà non ho bisogno di un buon PRNG, ero solo preoccupato che ci fosse un comportamento indefinito in agguato nella mia hashmap, quindi mi sono incuriosito quando ho eliminato quella possibilità e le piattaforme si sono comportate diversamente.
Theron S

btw ecco un esempio di un crittograficamente sicuro generatore di numeri casuali: github.com/divinity76/phpcpp/commit/... - ma di C ++ invece di C e sto lasciando gli implementatori STL fanno tutto il lavoro pesante ..
hanshenrik

3
@hanshenrik Un crypto RNG è generalmente eccessivo e troppo lento per una semplice tabella hash.
PM 2Ring

1
@ PM2Ring Assolutamente. Un hash table hash deve essere principalmente veloce, non buono. Tuttavia, se vuoi sviluppare un algoritmo di tabella hash che non sia solo veloce ma anche decente, credo che sia utile conoscere alcuni dei trucchi degli algoritmi di hash crittografici. Ti aiuterà a evitare la maggior parte degli errori più eclatanti che risolvono gli algoritmi di hash più veloci. Tuttavia, non avrei pubblicizzato per un'implementazione specifica qui.
cmaster - ripristina monica il

@cmaster Abbastanza vero. È sicuramente una buona idea conoscere un po 'cose come il mixare le funzioni e l' effetto valanga . Fortunatamente ci sono funzioni hash non crittografiche con buone proprietà che non sacrificano troppa velocità (se implementate correttamente), ad esempio xxhash, murmur3 o siphash.
PM 2Ring

5

In generale, la coppia rand / srand è stata considerata una specie di deprecato per lungo tempo a causa dei bit di ordine inferiore che mostrano una casualità inferiore rispetto ai bit di ordine elevato nei risultati. Questo potrebbe non avere nulla a che fare con i tuoi risultati, ma penso che sia ancora una buona opportunità per ricordare che anche se alcune implementazioni rand / srand sono ora più aggiornate, le implementazioni più vecchie persistono ed è meglio usare casualmente (3) ). Sulla mia scatola di Arch Linux, la seguente nota è ancora nella pagina man di 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.)

Appena sotto, la pagina man fornisce in realtà implementazioni di esempio molto brevi e molto semplici di rand e srand che riguardano i più semplici RNG LC che tu abbia mai visto e che abbiano un piccolo RAND_MAX. Non penso che corrispondano a ciò che è nella libreria C standard, se mai lo facessero. O almeno spero di no.

In generale, se hai intenzione di usare qualcosa dalla libreria standard, usa random se puoi (la pagina man lo elenca come POSIX standard a POSIX.1-2001, ma rand è standard molto prima ancora che C fosse standardizzato) . O meglio ancora, apri le Ricette numeriche (o cercale online) o Knuth e implementane una. Sono davvero facili e devi solo farlo una volta per avere un RNG generico con gli attributi di cui hai più bisogno e che è di qualità nota.


Grazie per il contesto. In realtà non ho bisogno di casualità di alta qualità e ho implementato MT19937, sebbene in Rust. Per lo più ero solo curioso di sapere come mai le due piattaforme si comportavano diversamente.
Theron S

1
A volte le domande migliori vengono poste per semplice interesse anziché per necessità rigorosa: sembra che siano spesso quelle che generano una serie di buone risposte da un punto specifico di curiosità. Il tuo e uno di loro. Ecco a tutti i curiosi, gli hacker reali e originali.
Thomas Kammeyer,

È divertente che il consiglio fosse di "smettere di usare rand ()" invece di migliorare rand (). Nulla nello standard dice mai che deve essere un generatore specifico.
pipe

2
@pipe Se rendere rand()'migliore' significherebbe renderlo più lento (cosa che probabilmente farebbe - i numeri casuali crittograficamente sicuri richiedono molto sforzo), allora è probabilmente meglio mantenerlo veloce anche se leggermente più prevedibile. Caso in questione: avevamo un'applicazione di produzione che impiegava molto tempo per avviarsi, che abbiamo rintracciato in un GNR la cui inizializzazione doveva attendere la generazione di entropia sufficiente ... Si è scoperto che non doveva essere così sicuro, quindi sostituirlo con un RNG "peggiore" è stato un grande miglioramento.
saluta il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.