Pourquoi rand () répète-t-il les nombres beaucoup plus souvent sur Linux que sur Mac?

88

J'implémentais une table de hachage en C dans le cadre d'un projet sur lequel je travaille et j'utilise des insertions aléatoires pour le tester lorsque j'ai remarqué que rand()sous Linux, il semble répéter les nombres beaucoup plus souvent que sur Mac. RAND_MAXest 2147483647 / 0x7FFFFFFF sur les deux plates-formes. Je l'ai réduit à ce programme de test qui fait un tableau d'octets de RAND_MAX+1long, génère RAND_MAXdes nombres aléatoires, note si chacun est un doublon et le vérifie dans la liste comme on le voit.

#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 génère régulièrement environ 790 millions de doublons. Mac n'en génère systématiquement qu'un, de sorte qu'il parcourt chaque nombre aléatoire qu'il peut générer presque sans répétition. Quelqu'un peut-il m'expliquer comment cela fonctionne? Je ne peux rien dire de différent des pages de manuel, je ne peux pas dire quel RNG chacun utilise et je ne trouve rien en ligne. Merci!

Theron S
la source
4
Puisque rand () retourne des valeurs de 0..RAND_MAX inclus, votre tableau doit être dimensionné RAND_MAX + 1
Blastfurnace
21
Vous avez peut-être remarqué que RAND_MAX / e ~ = 790 millions. De plus, la limite de (1-1 / n) ^ n lorsque n approche de l'infini est 1 / e.
David Schwartz
3
@DavidSchwartz Si je vous comprends bien, cela peut expliquer pourquoi le nombre sur Linux est constamment autour de 790 millions. Je suppose que la question est alors: pourquoi / comment Mac ne répète-t-il pas autant de fois?
Theron S
26
Il n'y a aucune exigence de qualité pour le PRNG dans la bibliothèque d'exécution. La seule exigence réelle est la répétabilité avec la même graine. Apparemment, la qualité du PRNG dans votre linux est meilleure que dans votre Mac.
PMG
4
@chux Oui, mais comme il est basé sur la multiplication, l'état ne peut jamais être nul ou le résultat (état suivant) serait également nul. Sur la base du code source, il vérifie zéro comme cas spécial s'il est initialisé avec zéro, mais il ne produit jamais zéro dans le cadre de la séquence.
Arkku

Réponses:

119

Alors qu'au début, il peut sembler que macOS rand()est en quelque sorte meilleur pour ne pas répéter de chiffres, il convient de noter qu'avec cette quantité de chiffres générés, il devrait voir beaucoup de doublons (en fait, environ 790 millions, ou (2 31 -1 ) / e ). De même, l'itération à travers les nombres dans l'ordre ne produirait pas non plus de doublons, mais ne serait pas considérée comme très aléatoire. Ainsi, l' rand()implémentation Linux est dans ce test indiscernable d'une véritable source aléatoire, contrairement à macOS rand().

Une autre chose qui semble surprenante à première vue est de savoir comment le macOS rand()peut si bien éviter les doublons. En regardant son code source , nous constatons que l'implémentation est la suivante:

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

Cela se traduit en effet par tous les nombres compris entre 1 et RAND_MAX, inclus, exactement une fois, avant que la séquence ne se répète à nouveau. Étant donné que l'état suivant est basé sur la multiplication, l'état ne peut jamais être nul (ou tous les états futurs seraient également nuls). Ainsi, le nombre répété que vous voyez est le premier, et zéro est celui qui n'est jamais renvoyé.

Apple promeut l'utilisation de meilleurs générateurs de nombres aléatoires dans sa documentation et ses exemples depuis au moins aussi longtemps que macOS (ou OS X) a existé, de sorte que la qualité de rand()n'est probablement pas jugée importante, et ils sont juste restés avec l'un des les générateurs pseudo-aléatoires les plus simples disponibles. (Comme vous l'avez noté, leur rand()est même commenté avec une recommandation d'utilisation à la arc4random()place.)

Sur une note connexe, le générateur de nombres pseudo-aléatoires le plus simple que j'ai pu trouver et qui produit des résultats décents dans ce (et beaucoup d'autres) tests d'aléatoire est xorshift * :

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

Cette implémentation génère presque exactement 790 millions de doublons dans votre test.

Arkku
la source
5
Un article de journal publié dans les années 80 proposait un test statistique pour les PRNG basé sur un "problème d'anniversaire".
pjs
14
"Apple a encouragé l'utilisation de meilleurs générateurs de nombres aléatoires dans sa documentation" -> bien sûr, Apple pourrait utiliser le arc4random()même code que derrière rand()et obtenir un bon rand()résultat. Plutôt que d'essayer d'orienter les programmeurs vers un code différent, créez simplement de meilleures fonctions de bibliothèque. "ils viennent de rester" est leur choix.
chux
23
l'absence d'un décalage constant dans les macs le rand()rend si mauvais qu'il n'est pas utile pour une utilisation pratique: pourquoi rand ()% 7 renvoie toujours 0? , Rand ()% 14 génère uniquement les valeurs 6 ou 13
phuclv
4
@PeterCordes: Il y a une telle exigence sur rand, que le relancer avec la même graine produise la même séquence. OpenBSD randest cassé et n'obéit pas à ce contrat.
R .. GitHub STOP STOPINGING ICE
8
@ R..GitHubSTOPHELPINGICE Voyez-vous une exigence C qui, rand()avec la même graine, produise la même séquence entre différentes versions de la bibliothèque? Une telle garantie pourrait être utile pour les tests de régression entre les versions de bibliothèque, mais je ne trouve aucune exigence en C pour cela.
chux
34

MacOS fournit une fonction rand () non documentée dans stdlib. Si vous ne le laissez pas, les premières valeurs qu'il génère sont 16807, 282475249, 1622650073, 984943658 et 1144108930. Une recherche rapide montrera que cette séquence correspond à un générateur de nombres aléatoires LCG très basique qui réitère la formule suivante:

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

Étant donné que l'état de ce RNG est entièrement décrit par la valeur d'un seul entier 32 bits, sa période n'est pas très longue. Pour être précis, il se répète toutes les 2 31 - 2 itérations, produisant chaque valeur de 1 à 2 31 - 2.

Je ne pense pas qu'il existe une implémentation standard de rand () pour toutes les versions de Linux, mais il existe une fonction glibc rand () qui est souvent utilisée. Au lieu d'une seule variable d'état 32 bits, celle-ci utilise un pool de plus de 1000 bits, qui à toutes fins utiles ne produira jamais une séquence entièrement répétitive. Encore une fois, vous pouvez probablement découvrir quelle version vous avez en imprimant les premières sorties de ce RNG sans l'amorcer d'abord. (La fonction glibc rand () produit les nombres 1804289383, 846930886, 1681692777, 1714636915 et 1957747793.)

Donc, la raison pour laquelle vous obtenez plus de collisions sous Linux (et presque pas sous MacOS) est que la version Linux de rand () est fondamentalement plus aléatoire.

r3mainer
la source
5
un non ensemencé rand()doit se comporter comme un avecsrand(1);
pmg
5
Le code source de rand()dans macOS est disponible: opensource.apple.com/source/Libc/Libc-1353.11.2/stdlib/FreeBSD/… FWIW, j'ai exécuté le même test contre ce compilé à partir de la source et il en résulte effectivement un seul doublon. Apple a encouragé l'utilisation d'autres générateurs de nombres aléatoires (comme arc4random()avant la prise de contrôle de Swift) dans leurs exemples et leur documentation, de sorte que l'utilisation de rand()n'est probablement pas très courante dans les applications natives sur leurs plates-formes, ce qui peut expliquer pourquoi ce n'est pas mieux.
Arkku
Merci pour la réponse, cela répond à ma question. Et une période de (2 ^ 31) -2 explique pourquoi cela commencerait à se répéter à la fin comme je l'ai observé. Vous (@ r3mainer) avez dit qu'il rand()n'était pas documenté, mais @Arkku a fourni un lien vers la source apparente. L'un de vous sait-il pourquoi je ne trouve pas ce fichier sur mon système et pourquoi je ne le vois que int rand(void) __swift_unavailable("Use arc4random instead.");sur Mac stdlib.h? Je suppose que le code lié à @Arkku est juste compilé dans ... quelle bibliothèque?
Theron S
1
@TheronS Il est compilé dans la bibliothèque C, libc, /usr/lib/libc.dylib. =)
Arkku
5
Quelle est la version d' rand()un programme C donné des utilisations n'est pas déterminé par le « compilateur » ou le « système d' exploitation », mais plutôt la mise en œuvre de la bibliothèque standard C (par exemple glibc, libc.dylib, msvcrt*.dll).
Peter O.
10

rand()est défini par la norme C, et la norme C ne spécifie pas quel algorithme utiliser. De toute évidence, Apple utilise un algorithme inférieur à votre implémentation GNU / Linux: celui de Linux est indiscernable d'une véritable source aléatoire dans votre test, tandis que l'implémentation Apple mélange simplement les chiffres.

Si vous voulez des nombres aléatoires de n'importe quelle qualité, utilisez un meilleur PRNG qui donne au moins quelques garanties sur la qualité des nombres qu'il retourne, ou lisez simplement /dev/urandomou similaire. Le dernier vous donne des numéros de qualité cryptographique, mais est lent. Même s'il est trop lent par lui-même, il /dev/urandompeut fournir d'excellentes graines à un autre PRNG plus rapide.

cmaster - réintégrer monica
la source
Merci pour la réponse. Je n'ai pas vraiment besoin d'un bon PRNG, je craignais simplement qu'un comportement indéfini se cache dans ma table de hachage, puis je suis devenu curieux quand j'ai éliminé cette possibilité et que les plates-formes se comportaient toujours différemment.
Theron S
btw voici un exemple de générateur de nombres aléatoires cryptographiquement sécurisé: github.com/divinity76/phpcpp/commit/… - mais c'est C ++ au lieu de C et je laisse les implémenteurs STL faire tout le gros du travail ..
hanshenrik
3
@hanshenrik Un RNG crypto est généralement excessif et trop lent pour une simple table de hachage.
PM 2Ring
1
@ PM2Ring Absolument. Un hachage de table de hachage doit être rapide, pas bon. Cependant, si vous souhaitez développer un algorithme de table de hachage non seulement rapide mais également décent, je pense qu'il est avantageux de connaître certaines des astuces des algorithmes de hachage cryptographiques. Cela vous aidera à éviter la plupart des erreurs les plus flagrantes qui énigment les algorithmes de hachage les plus rapides. Néanmoins, je n'aurais pas annoncé ici une mise en œuvre spécifique.
cmaster - réintègre monica le
@cmaster C'est vrai. C'est certainement une bonne idée d'en savoir un peu sur des choses comme les fonctions de mixage et l' effet d'avalanche . Heureusement, il existe des fonctions de hachage non cryptographiques avec de bonnes propriétés qui ne sacrifient pas trop de vitesse (lorsqu'elles sont correctement implémentées), par exemple xxhash, murmur3 ou siphash.
PM 2Ring
5

En général, la paire rand / srand a été considérée comme obsolète depuis longtemps en raison des bits de poids faible affichant moins de caractère aléatoire que les bits de poids fort dans les résultats. Cela peut ou non avoir quelque chose à voir avec vos résultats, mais je pense que c'est toujours une bonne occasion de se rappeler que même si certaines implémentations rand / srand sont maintenant plus à jour, les implémentations plus anciennes persistent et il est préférable d'utiliser aléatoire (3 ). Sur ma boîte Arch Linux, la note suivante est toujours dans la page de manuel de 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.)

Juste en dessous, la page de manuel donne en fait des exemples d'implémentation très courts et très simples de rand et srand qui concernent les RNG LC les plus simples que vous ayez jamais vus et ayant un petit RAND_MAX. Je ne pense pas qu'ils correspondent à ce qui se trouve dans la bibliothèque standard C, si c'est le cas. Ou du moins j'espère que non.

En général, si vous allez utiliser quelque chose de la bibliothèque standard, utilisez random si vous le pouvez (la page de manuel le répertorie comme standard POSIX vers POSIX.1-2001, mais rand est un moyen standard avant que C ne soit même standardisé) . Ou mieux encore, ouvrez les recettes numériques (ou recherchez-les en ligne) ou Knuth et implémentez-en une. Ils sont vraiment faciles et vous n'avez vraiment besoin de le faire qu'une seule fois pour avoir un RNG à usage général avec les attributs dont vous avez le plus souvent besoin et qui est de qualité connue.

Thomas Kammeyer
la source
Merci pour le contexte. Je n'ai pas vraiment besoin d'aléatoire de haute qualité et j'ai implémenté MT19937, bien que dans Rust. Était surtout curieux de savoir pourquoi les deux plates-formes se comportaient différemment.
Theron S
1
Parfois, les meilleures questions sont posées par simple intérêt plutôt que par besoin strict - il semble que ce sont souvent celles qui engendrent une suite de bonnes réponses à partir d'un point de curiosité spécifique. Le vôtre en fait partie. Voici à tous les curieux, les pirates réels et originaux.
Thomas Kammeyer
C'est drôle que le conseil était de "cesser d'utiliser rand ()" au lieu de rendre rand () meilleur. Rien dans la norme ne dit jamais qu'il doit s'agir d'un générateur spécifique.
pipe
2
@pipe Si faire rand()`` mieux '' signifierait le ralentir (ce qui serait probablement le cas - les nombres aléatoires sécurisés cryptographiquement demandent beaucoup d'efforts), alors il est probablement préférable de le garder rapide même s'il est légèrement plus prévisible. Exemple: nous avions une application de production qui prenait beaucoup de temps à démarrer, que nous avons retracée à un RNG dont l'initialisation devait attendre la génération d'une entropie suffisante ... Il s'est avéré qu'elle n'avait pas besoin d'être aussi sécurisée, donc la remplacer par un «pire» RNG était une grande amélioration.
gidds