Pourquoi rand () + rand () produit-il des nombres négatifs?


304

J'ai observé que la rand()fonction de bibliothèque lorsqu'elle est appelée une seule fois dans une boucle, elle produit presque toujours des nombres positifs.

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

Mais lorsque j'ajoute deux rand()appels, les nombres générés ont maintenant plus de nombres négatifs.

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

Quelqu'un peut-il expliquer pourquoi je vois des nombres négatifs dans le deuxième cas?

PS: j'initialise la graine avant la boucle comme srand(time(NULL)).


11
rand()ne peut pas être négatif ...
twentylemon

293
rand () + rand () peut owerflow
maskacovnik

13
Quel est RAND_MAXpour votre compilateur? Vous pouvez généralement le trouver dans stdlib.h. (Drôle: en vérifiant man 3 rand, il porte la description d'une ligne "mauvais générateur de nombres aléatoires".)
usr2564301

6
faire ce que tout programmeur sensé ferait abs(rand()+rand()). Je préfère avoir un UB positif que négatif! ;)
Vinicius Kamakura

11
@hexa: ce n'est pas une solution pour l'UB, comme cela se produit déjà pour l'addition. Vous ne pouvez pas faire de UB un comportement défini . Un programe sensé éviterait l'UB comme l'enfer.
trop honnête pour ce site

Réponses:


542

rand()est défini pour renvoyer un entier entre 0et RAND_MAX.

rand() + rand()

pourrait déborder. Ce que vous observez est probablement le résultat d' un comportement indéfini provoqué par un débordement d'entier.


4
@JakubArnold: Comment ce comportement de débordement est-il spécifié différemment par chaque langue? Python par exemple n'en a pas (enfin, jusqu'à la mémoire disponible), car int ne fait que croître.
trop honnête pour ce site

2
@Olaf Cela dépend de la façon dont une langue décide de représenter des entiers signés. Java n'avait aucun mécanisme pour détecter le débordement d'entier (jusqu'à java 8) et l'a défini pour boucler et Go utilise uniquement la représentation complémentaire de 2 et le définit comme légal pour les débordements d'entier signés. C supporte évidemment plus de 2 compléments.
PP

2
@EvanCarslake Non, ce n'est pas un comportement universel. Ce que vous dites concerne la représentation du complément à 2. Mais le langage C permet également d'autres représentations. La spécification du langage C indique que le dépassement d'entier signé n'est pas défini . Donc, en général, aucun programme ne doit s'appuyer sur un tel comportement et doit coder soigneusement pour ne pas provoquer de dépassement d'entier signé. Mais cela n'est pas applicable pour les entiers non signés car ils "boucleraient" d'une manière bien définie (réduction modulo 2). [suite] ...
PP

12
Il s'agit de la citation de la norme C relative au débordement d'entier signé: si une condition exceptionnelle se produit lors de l'évaluation d'une expression (c'est-à-dire si le résultat n'est pas défini mathématiquement ou n'est pas dans la plage de valeurs représentables pour son type), le comportement n'est pas défini.
PP

3
@EvanCarslake s'éloignant un peu de la question, les compilateurs C utilisent la norme et pour les entiers signés, ils peuvent supposer que a + b > as'ils le savent b > 0. Ils peuvent également supposer que s'il y a une instruction exécutée ultérieurement, a + 5la valeur actuelle est alors inférieure INT_MAX - 5. Ainsi, même sur le processeur / interprète complément 2 sans programme pièges, le programme pourrait ne pas se comporter comme si ints était le complément 2 sans pièges.
Maciej Piechotka

90

Le problème est l'ajout. rand()renvoie une intvaleur de 0...RAND_MAX. Donc, si vous en ajoutez deux, vous vous lèverez RAND_MAX * 2. Si cela dépasse INT_MAX, le résultat de l'addition déborde la plage valide qu'un intpeut contenir. Le débordement des valeurs signées est un comportement indéfini et peut conduire votre clavier à vous parler en langues étrangères.

Comme il n'y a aucun avantage à ajouter deux résultats aléatoires, l'idée simple est simplement de ne pas le faire. Vous pouvez également convertir chaque résultat enunsigned int avant l'ajout si cela peut contenir la somme. Ou utilisez un type plus grand. Notez que ce longn'est pas nécessairement plus large que int, la même chose s'applique à long longif intest au moins 64 bits!

Conclusion: Évitez simplement l'ajout. Il ne fournit pas plus de "caractère aléatoire". Si vous avez besoin de plus de bits, vous pouvez concaténer les valeurs sum = a + b * (RAND_MAX + 1), mais cela nécessite également probablement un type de données plus grand que int.

Comme votre raison indiquée est d'éviter un résultat nul: cela ne peut pas être évité en ajoutant les résultats de deux rand()appels, car les deux peuvent être nuls. Au lieu de cela, vous pouvez simplement incrémenter. Si RAND_MAX == INT_MAXcela ne peut pas être fait en int. Cependant, (unsigned int)rand() + 1fera très, très probablement. Probablement (pas définitivement), car cela nécessite UINT_MAX > INT_MAX, ce qui est vrai sur toutes les implémentations que je connais (qui couvre plusieurs architectures intégrées, DSP et toutes les plates-formes de bureau, mobiles et serveurs des 30 dernières années).

Avertissement:

Bien que déjà saupoudré dans les commentaires ici, veuillez noter que l'ajout de deux valeurs aléatoires n'obtient pas une distribution uniforme, mais une distribution triangulaire comme lancer deux dés: pour obtenir 12(deux dés) les deux dés doivent s'afficher 6. car 11il existe déjà deux variantes possibles: 6 + 5ou 5 + 6, etc.

Donc, l'ajout est également mauvais de cet aspect.

Notez également que les résultats rand()générés ne sont pas indépendants les uns des autres, car ils sont générés par un générateur de nombres pseudo-aléatoires . Notez également que la norme ne spécifie pas la qualité ou la distribution uniforme des valeurs calculées.


14
@badmad: Et si les deux appels retournent 0?
trop honnête pour ce site

3
@badmad: Je me demande simplement si UINT_MAX > INT_MAX != falsela norme est garantie. (Cela semble probable, mais pas sûr si nécessaire). Si c'est le cas, vous pouvez simplement lancer un seul résultat et incrémenter (dans cet ordre!).
trop honnête pour ce site

3
Il y a un avantage à ajouter plusieurs nombres aléatoires lorsque vous voulez une distribution non uniforme: stackoverflow.com/questions/30492259/…
Cœur

6
pour éviter 0, un simple "alors que le résultat est 0, relancez"?
Olivier Dulac

2
Non seulement les ajouter est une mauvaise façon d'éviter le 0, mais cela entraîne également une distribution non uniforme. Vous obtenez une distribution comme les résultats des jets de dés: 7 est 6 fois plus probable que 2 ou 12.
Barmar

36

Ceci est une réponse à une clarification de la question faite en commentaire de cette réponse ,

la raison pour laquelle j'ajoutais était d'éviter «0» comme nombre aléatoire dans mon code. rand () + rand () était la solution rapide et sale qui m'est venue à l'esprit.

Le problème était d'éviter 0. Il y a (au moins) deux problèmes avec la solution proposée. L'une est, comme les autres réponses l'indiquent, qui rand()+rand()peut invoquer un comportement indéfini. Le meilleur conseil est de ne jamais invoquer un comportement indéfini. Un autre problème est qu'il n'y a aucune garantie qui rand()ne produira pas 0 deux fois de suite.

Ce qui suit rejette zéro, évite un comportement indéfini et, dans la grande majorité des cas, sera plus rapide que deux appels à rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
Et alors rand() + 1?
askvictor

3
@askvictor Cela pourrait déborder (bien que ce soit peu probable).
gerrit

3
@gerrit - dépend de MAX_INT et RAND_MAX
askvictor

3
@gerrit, je serais surpris s'ils ne sont pas les mêmes, mais je suppose que c'est un endroit pour les pédants :)
askvictor

10
Si RAND_MAX == MAX_INT, rand () + 1 débordera avec exactement la même probabilité que la valeur de rand () étant 0, ce qui rend cette solution complètement inutile. Si vous êtes prêt à le risquer et à ignorer la possibilité d'un débordement, vous pouvez également utiliser rand () tel quel et ignorer la possibilité qu'il retourne 0.
Emil Jeřábek

3

rand()Produisez essentiellement des nombres entre 0et RAND_MAX, et 2 RAND_MAX > INT_MAXdans votre cas.

Vous pouvez moduler avec la valeur maximale de votre type de données pour éviter un débordement. Bien sûr, cela perturbera la distribution des nombres aléatoires, mais randn'est qu'un moyen d'obtenir des nombres aléatoires rapides.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}

2

Peut-être pourriez-vous essayer une approche plutôt délicate en vous assurant que la valeur renvoyée par la somme de 2 rand () ne dépasse jamais la valeur de RAND_MAX. Une approche possible pourrait être sum = rand () / 2 + rand () / 2; Cela garantirait que pour un compilateur 16 bits avec une valeur RAND_MAX de 32767 même si les deux rand retournent justement 32767, même dans ce cas (32767/2 = 16383) 16383 + 16383 = 32766, cela n'entraînerait donc pas de somme négative.


1
L'OP voulait exclure 0 des résultats. L'addition ne fournit pas non plus une distribution uniforme des valeurs aléatoires.
trop honnête pour ce site

@Olaf: Il n'y a aucune garantie que deux appels consécutifs rand()ne donneront pas tous les deux zéro, donc le désir d'éviter zéro n'est pas une bonne raison pour ajouter deux valeurs. D'un autre côté, le désir d'avoir une distribution non uniforme serait une bonne raison d'ajouter deux valeurs aléatoires si l'on s'assure qu'aucun débordement ne se produit.
supercat

1

la raison pour laquelle j'ajoutais était d'éviter «0» comme nombre aléatoire dans mon code. rand () + rand () était la solution rapide et sale qui m'est venue à l'esprit.

Une solution simple (d'accord, appelez ça un "Hack") qui ne produit jamais de résultat nul et ne débordera jamais est:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

Cela limitera votre valeur maximale, mais si cela ne vous intéresse pas, cela devrait bien fonctionner pour vous.


1
Sidenote: Attention aux décalages à droite des variables signées. Elle n'est bien définie que pour les valeurs non négatives, pour les négatives, elle est définie par l'implémentation. (Heureusement, rand()renvoie toujours une valeur non négative). Cependant, je laisserais l'optimisation au compilateur ici.
trop honnête pour ce site

@Olaf: En général, une division signée par deux sera moins efficace qu'un quart de travail. À moins qu'un rédacteur de compilateur n'ait investi des efforts pour indiquer au compilateur que ce randne sera pas négatif, le décalage sera plus efficace que la division par un entier signé 2. La division par 2upourrait fonctionner, mais si xc'est le cas, cela intpeut entraîner des avertissements sur la conversion implicite de non signé à signer.
supercat

@supercat: Veuillez relire mon commentaire car3fully à nouveau. Vous devriez très bien savoir que tout compilateur raisonnable utilisera un décalage pour de / 2toute façon (je l'ai vu même pour quelque chose comme -O0, c'est-à-dire sans optimisations explicitement demandées). C'est peut-être l'optimisation la plus triviale et la plus établie du code C. Le point est que la division est bien définie par la norme pour toute la plage entière, pas seulement les valeurs non négatives. Encore une fois: laissez les optimisations au compilateur, écrivez du code correct et clair en premier lieu. C'est encore plus important pour les débutants.
trop honnête pour ce site

@Olaf: Chaque compilateur que j'ai testé génère du code plus efficace lors du décalage vers la rand()droite de un ou lors de la division 2uque lors de la division par 2, même lors de l'utilisation -O3. On pourrait raisonnablement dire qu'une telle optimisation est peu probable, mais dire "laisser de telles optimisations au compilateur" impliquerait que les compilateurs seraient susceptibles de les réaliser. Connaissez-vous des compilateurs qui sera en fait?
supercat

@supercat: Vous devriez alors utiliser des compilateurs plus modernes. gcc vient de générer du code fin la dernière fois que j'ai vérifié l'assembleur généré. Néanmoins, autant j'apprécie d'avoir un groopie, je préfère ne pas être harcelé au point que vous présentez la dernière fois. Ces messages ont des années, mes commentaires sont parfaitement valables. Je vous remercie.
trop honnête pour ce site

1

Pour éviter 0, essayez ceci:

int rnumb = rand()%(INT_MAX-1)+1;

Vous devez inclure limits.h.


4
Cela doublera la probabilité d'obtenir 1. C'est fondamentalement le même (mais possiblement plus lent) que d'ajouter conditionnellement 1 si rand()donne 0.
trop honnête pour ce site

Oui, tu as raison Olaf. Si rand () = 0 ou INT_MAX -1, le nombre sera égal à 1.
Doni

Pire encore, quand j'y pense. Il doublera en fait la propension pour 1et 2(tout cela est supposé RAND_MAX == INT_MAX). J'ai oublié le - 1.
trop honnête pour ce site

1
L' -1ici ne sert à rien. rand()%INT_MAX+1; ne générerait toujours que des valeurs dans la plage [1 ... INT_MAX].
chux

-2

Alors que ce que tout le monde a dit sur le débordement probable pourrait très bien être la cause du négatif, même lorsque vous utilisez des entiers non signés. Le vrai problème est en fait d'utiliser la fonctionnalité heure / date comme graine. Si vous vous êtes vraiment familiarisé avec cette fonctionnalité, vous saurez exactement pourquoi je dis cela. Car ce qu'il fait vraiment est de donner une distance (temps écoulé) depuis une date / heure donnée. Bien que l'utilisation de la fonctionnalité date / heure comme graine d'un rand () soit une pratique très courante, ce n'est vraiment pas la meilleure option. Vous devriez chercher de meilleures alternatives, car il existe de nombreuses théories sur le sujet et je ne pourrais pas entrer dans toutes. Vous ajoutez à cette équation la possibilité de débordement et cette approche était vouée à l'échec dès le départ.

Ceux qui ont posté le rand () + 1 utilisent la solution la plus utilisée afin de garantir qu'ils n'obtiennent pas de nombre négatif. Mais cette approche n'est vraiment pas la meilleure façon non plus.

La meilleure chose que vous puissiez faire est de prendre le temps supplémentaire pour écrire et utiliser la gestion des exceptions appropriée, et n'ajouter au nombre rand () que si et / ou lorsque vous vous retrouvez avec un résultat nul. Et, pour gérer correctement les nombres négatifs. La fonctionnalité rand () n'est pas parfaite et doit donc être utilisée en conjonction avec la gestion des exceptions pour vous assurer d'obtenir le résultat souhaité.

Prendre le temps et les efforts supplémentaires pour enquêter, étudier et implémenter correctement la fonctionnalité rand () en vaut la peine. Juste mes deux cents. Bonne chance dans vos efforts ...


2
rand()ne spécifie pas quelle graine utiliser. La norme ne précise à utiliser un générateur pseudo - aléatoire, et non une relation à tout moment. Il ne précise pas non plus la qualité du générateur. Le problème actuel est clairement le débordement. Notez que cela rand()+1est utilisé pour éviter 0; rand()ne renvoie pas de valeur négative. Désolé, mais vous avez manqué le point ici. Il ne s'agit pas de la qualité du PRNG. ...
trop honnête pour ce site

... Une bonne pratique sous GNU / Linux est de créer /dev/randomet d'utiliser un bon PRNG par la suite (vous n'êtes pas sûr de la qualité de rand()glibc) ou de continuer à utiliser l'appareil - au risque de bloquer votre application s'il n'y a pas assez d'entropie disponible. Essayer d'obtenir votre entropie dans l'application pourrait très bien être une vulnérabilité car cela est peut-être plus facile à attaquer. Et maintenant, il s'agit de durcir - pas ici
trop honnête pour ce site
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.