La variable locale non initialisée est-elle le générateur de nombres aléatoires le plus rapide?


329

Je sais que la variable locale non initialisée est un comportement indéfini ( UB ), et la valeur peut également avoir des représentations d'interruption qui peuvent affecter le fonctionnement ultérieur, mais parfois je veux utiliser le nombre aléatoire uniquement pour la représentation visuelle et ne les utiliserai pas dans d'autres parties de programme, par exemple, définir quelque chose avec une couleur aléatoire dans un effet visuel, par exemple:

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

est-ce plus rapide que

void updateEffect(){
    for(int i=0;i<1000;i++){
        star[i].setColor(rand()%255,rand()%255,rand()%255);
        star[i].setVisible(rand()%2==0?true:false);
    }
}

et aussi plus rapide que les autres générateurs de nombres aléatoires?


88
+1 C'est une question parfaitement légitime. Il est vrai qu'en pratique, les valeurs non initialisées peuvent être plutôt aléatoires. Le fait qu'ils ne sont pas particulièrement et qu'il est UB ne fait pas demander si mal que ça.
imallett

35
@imallett: Absolument. C'est une bonne question, et au moins un ancien jeu Z80 (Amstrad / ZX Spectrum) a utilisé son programme comme données pour configurer son terrain. Il y a donc même des précédents. Je ne peux pas faire ça de nos jours. Les systèmes d'exploitation modernes enlèvent tout le plaisir.
Bathsheba

81
Certes, le principal problème est qu'il n'est pas aléatoire.
john

30
En fait, il y a un exemple d'une variable non initialisée utilisée comme valeur aléatoire, voir le désastre Debian RNG (exemple 4 dans cet article ).
PaperBirdMaster

31
En pratique - et croyez-moi, je fais beaucoup de débogage sur diverses architectures - votre solution peut faire deux choses: soit lire des registres non initialisés, soit de la mémoire non initialisée. Maintenant, alors que "non initialisé" signifie aléatoire d'une certaine manière, en pratique, il contiendra très probablement a) des zéros , b) des valeurs répétitives ou cohérentes (en cas de lecture de mémoire autrefois occupée par des médias numériques) ou c) des ordures cohérentes avec une valeur limitée ensemble (en cas de lecture de mémoire autrefois occupée par des données numériques encodées). Aucune de ces sources n'est une véritable source d'entropie.
mg30rg

Réponses:


299

Comme d'autres l'ont noté, il s'agit d'un comportement indéfini (UB).

En pratique, cela fonctionnera (probablement) en fait (en quelque sorte). La lecture à partir d'un registre non initialisé sur les architectures x86 [-64] produira effectivement des résultats inutiles, et ne fera probablement rien de mal (contrairement à par exemple Itanium, où les registres peuvent être marqués comme invalides , de sorte que les lectures propagent des erreurs comme NaN).

Il existe cependant deux problèmes principaux:

  1. Ce ne sera pas particulièrement aléatoire. Dans ce cas, vous lisez dans la pile, vous obtiendrez donc tout ce qui s'y trouvait auparavant. Ce qui pourrait être effectivement aléatoire, complètement structuré, le mot de passe que vous avez entré il y a dix minutes ou la recette de biscuits de votre grand-mère.

  2. C'est une mauvaise pratique (majuscule «B») de laisser des choses comme celle-ci se glisser dans votre code. Techniquement, le compilateur peut insérer reformat_hdd();chaque fois que vous lisez une variable non définie. Ce ne sera pas le cas , mais vous ne devriez pas le faire de toute façon. Ne faites pas de choses dangereuses. Moins vous faites d'exceptions, plus vous êtes à l'abri des erreurs accidentelles en tout temps.

Le problème le plus urgent avec UB est qu'il rend le comportement de l'ensemble de votre programme indéfini. Les compilateurs modernes peuvent l'utiliser pour éluder d'énormes portions de votre code ou même remonter dans le temps . Jouer avec UB, c'est comme un ingénieur victorien démanteler un réacteur nucléaire sous tension. Il y a des millions de choses qui tournent mal, et vous ne connaîtrez probablement pas la moitié des principes sous-jacents ou de la technologie mise en œuvre. C'est peut- être bien, mais vous ne devriez toujours pas laisser cela se produire. Regardez les autres belles réponses pour plus de détails.

Aussi, je te virerais.


39
@Potatoswatter: les registres Itanium peuvent contenir du NaT (Not a Thing) qui est en fait un "registre non initialisé". Sur Itanium, la lecture d'un registre lorsque vous n'y avez pas écrit peut annuler votre programme (en savoir plus ici: blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx ). Il y a donc une bonne raison pour laquelle la lecture de valeurs non initialisées est un comportement non défini. C'est aussi probablement une des raisons pour lesquelles Itanium n'est pas très populaire :)
tbleher

58
Je m'oppose vraiment à la notion de "ce genre de travaux". Même si c'était vrai aujourd'hui, ce qui n'est pas le cas, cela pourrait changer à tout moment en raison de compilateurs plus agressifs. Le compilateur peut remplacer n'importe quelle lecture unreachable()et supprimer la moitié de votre programme. Cela se produit également dans la pratique. Je crois que ce comportement a complètement neutralisé le RNG dans certaines distributions Linux .; La plupart des réponses à cette question semblent supposer qu'une valeur non initialisée se comporte comme une valeur. C'est faux.
usr

25
De plus, je dirais que vous semble une chose assez stupide à dire, en supposant que les bonnes pratiques devraient être prises en compte lors de la révision du code, discutées et ne devraient plus jamais se reproduire. Cela devrait certainement être rattrapé car nous utilisons les bons drapeaux d'avertissement, non?
Shafik Yaghmour

17
@Michael En fait, ça l'est. Si un programme a un comportement indéfini à tout moment, le compilateur peut optimiser votre programme d'une manière qui affecte le code précédant celui invoquant le comportement indéfini. Il existe divers articles et des démonstrations de l' esprit ahurissant cela peut devenir Voici une assez bonne: blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx (qui comprend le bit dans la norme qui dit tous les paris sont désactivés si un chemin dans votre programme invoque UB)
Tom Tanner

19
Cette réponse donne l'impression que "invoquer un comportement indéfini est mauvais en théorie, mais cela ne vous fera pas beaucoup de mal en pratique" . C'est faux. La collecte de l'entropie à partir d'une expression qui provoquerait l'UB peut (et probablement entraînera ) la perte de toute l'entropie précédemment collectée . Il s'agit d'un grave danger.
Theodoros Chatzigiannakis

213

Permettez-moi de dire ceci clairement: nous n'invoquons pas de comportement indéfini dans nos programmes . Ce n'est jamais une bonne idée, point final. Il existe de rares exceptions à cette règle; par exemple, si vous êtes un implémenteur de bibliothèque implémentant offsetof . Si votre cas relève d'une telle exception, vous le savez probablement déjà. Dans ce cas, nous savons que l'utilisation de variables automatiques non initialisées est un comportement non défini .

Les compilateurs sont devenus très agressifs avec des optimisations concernant un comportement indéfini et nous pouvons trouver de nombreux cas où un comportement indéfini a conduit à des failles de sécurité. Le cas le plus tristement célèbre est probablement la suppression de la vérification du pointeur nul du noyau Linux que je mentionne dans ma réponse au bogue de compilation C ++? où une optimisation du compilateur autour d'un comportement non défini a transformé une boucle finie en boucle infinie.

Nous pouvons lire les Optimisations dangereuses et la perte de causalité du CERT ( vidéo ) qui disent, entre autres:

De plus en plus, les rédacteurs de compilateurs profitent de comportements non définis dans les langages de programmation C et C ++ pour améliorer les optimisations.

Fréquemment, ces optimisations interfèrent avec la capacité des développeurs à effectuer une analyse de cause à effet sur leur code source, c'est-à-dire analyser la dépendance des résultats en aval par rapport aux résultats antérieurs.

Par conséquent, ces optimisations éliminent la causalité dans les logiciels et augmentent la probabilité de pannes, de défauts et de vulnérabilités logicielles.

En ce qui concerne spécifiquement les valeurs indéterminées, le rapport de défaut standard C 451: Instabilité des variables automatiques non initialisées rend la lecture intéressante. Il n'a pas encore été résolu mais introduit le concept de valeurs bancales qui signifie que l'indétermination d'une valeur peut se propager à travers le programme et peut avoir différentes valeurs indéterminées à différents points du programme.

Je ne connais aucun exemple où cela se produit, mais à ce stade, nous ne pouvons pas l'exclure.

De vrais exemples, pas le résultat que vous attendez

Il est peu probable que vous obteniez des valeurs aléatoires. Un compilateur pourrait optimiser complètement la boucle. Par exemple, avec ce cas simplifié:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r ;
    }
}

clang l'optimise ( voir en direct ):

updateEffect(int*):                     # @updateEffect(int*)
    retq

ou peut-être obtenir tous les zéros, comme avec ce cas modifié:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r%255 ;
    }
}

voir en direct :

updateEffect(int*):                     # @updateEffect(int*)
    xorps   %xmm0, %xmm0
    movups  %xmm0, 64(%rdi)
    movups  %xmm0, 48(%rdi)
    movups  %xmm0, 32(%rdi)
    movups  %xmm0, 16(%rdi)
    movups  %xmm0, (%rdi)
    retq

Ces deux cas sont des formes parfaitement acceptables de comportement indéfini.

Remarque, si nous sommes sur un Itanium, nous pourrions nous retrouver avec une valeur de piège :

[...] si le registre contient une valeur spéciale non-chose, lire les pièges du registre à l'exception de quelques instructions [...]

Autres notes importantes

Il est intéressant de noter la variance entre gcc et clang notée dans le projet UB Canaries sur leur volonté de profiter d'un comportement indéfini par rapport à la mémoire non initialisée. L'article note ( soulignement le mien ):

Bien sûr, nous devons être parfaitement clairs avec nous-mêmes: une telle attente n'a rien à voir avec la norme de langage et tout à voir avec ce qu'un compilateur particulier arrive à faire, soit parce que les fournisseurs de ce compilateur ne veulent pas exploiter cette UB ou simplement parce qu'ils n'ont pas encore réussi à l'exploiter . Lorsqu'aucune garantie réelle du fournisseur de compilateur n'existe, nous aimons à dire que les UB non encore exploitées sont des bombes à retardement : elles attendent de se déclencher le mois prochain ou l'année prochaine lorsque le compilateur deviendra un peu plus agressif.

Comme Matthieu M. le souligne, ce que tout programmeur C devrait savoir sur le comportement indéfini # 2/3 est également pertinent pour cette question. Il dit entre autres (c'est moi qui souligne ):

La chose importante et effrayante à réaliser est que n'importe quelle optimisation basée sur un comportement non défini peut commencer à être déclenchée à tout moment dans le code bogué . L'intégration, le déroulement des boucles, la promotion de la mémoire et d'autres optimisations continueront de s'améliorer, et une partie importante de leur raison d'être est d'exposer des optimisations secondaires comme celles ci-dessus.

Pour moi, c'est profondément insatisfaisant, en partie parce que le compilateur finit inévitablement par être blâmé, mais aussi parce que cela signifie que d'énormes corps de code C sont des mines terrestres qui ne demandent qu'à exploser.

Par souci d'exhaustivité, je devrais probablement mentionner que les implémentations peuvent choisir de rendre le comportement indéfini bien défini, par exemple, gcc autorise la punition de type via les unions tandis qu'en C ++, cela semble être un comportement indéfini . Si tel est le cas, l'implémentation doit le documenter et ce ne sera généralement pas portable.


1
+ (int) (PI / 3) pour les exemples de sortie du compilateur; un exemple concret que UB est, eh bien, UB .

2
Utiliser UB était en fait la marque de fabrique d'un excellent pirate informatique. Cette tradition dure depuis probablement 50 ans ou plus maintenant. Malheureusement, les ordinateurs sont désormais nécessaires pour minimiser les effets de l'UB à cause des mauvaises personnes. J'ai vraiment aimé découvrir comment faire des choses sympas avec le code machine UB ou le port lecture / écriture, etc. dans les années 90, lorsque le système d'exploitation n'était pas aussi capable de protéger l'utilisateur contre lui-même.
sfdcfox

1
@sfdcfox si vous le faisiez dans le code machine / assembleur, ce n'était pas un comportement indéfini (c'était peut-être un comportement non conventionnel).
Caleth

2
Si vous avez un assemblage spécifique à l'esprit, utilisez-le et n'écrivez pas un C. non conforme. Tout le monde saura que vous utilisez une astuce non portable spécifique. Et ce ne sont pas les mauvaises personnes qui signifient que vous ne pouvez pas utiliser UB, c'est Intel, etc.
Caleth

2
@ 500-InternalServerError car ils peuvent ne pas être facilement détectables ou ne pas être détectables du tout dans le cas général et il n'y aurait donc aucun moyen de les interdire. Ce qui est différent des violations de la grammaire qui peuvent être détectées. Nous avons également un diagnostic mal formé et mal formé, qui sépare en général les programmes mal formés qui pourraient être détectés en théorie de ceux qui en théorie ne pouvaient pas être détectés de manière fiable.
Shafik Yaghmour

164

Non, c'est terrible.

Le comportement de l'utilisation d'une variable non initialisée n'est pas défini à la fois en C et C ++, et il est très peu probable qu'un tel schéma ait des propriétés statistiques souhaitables.

Si vous voulez un générateur de nombres aléatoires "rapide et sale", alors rand()c'est votre meilleur pari. Dans sa mise en œuvre, tout ce qu'il fait est une multiplication, une addition et un module.

Le générateur le plus rapide que je connaisse vous oblige à utiliser a uint32_tcomme type de variable pseudo-aléatoire I, et à utiliser

I = 1664525 * I + 1013904223

pour générer des valeurs successives. Vous pouvez choisir n'importe quelle valeur initiale de I(appelée la graine ) qui vous convient. De toute évidence, vous pouvez coder cela en ligne. L'enveloppe standard garantie d'un type non signé agit comme module. (Les constantes numériques sont choisies à la main par ce remarquable programmeur scientifique Donald Knuth.)


9
Le générateur "congruentiel linéaire" que vous présentez est bon pour les applications simples, mais uniquement pour les applications non cryptographiques. Il est possible de prédire son comportement. Voir par exemple " Déchiffrer un chiffrement congruentiel linéaire " par Don Knuth lui-même (IEEE Transactions on Information Theory, Volume 31)
Jay

24
@Jay par rapport à une variable unialisée pour rapide et sale? C'est une bien meilleure solution.
Mike McMahon

2
rand()n'est pas apte à l'usage et devrait être entièrement déconseillé, à mon avis. Ces jours-ci, vous pouvez télécharger gratuitement des générateurs de nombres aléatoires sous licence et largement supérieurs (par exemple Mersenne Twister) qui sont presque aussi rapides avec la plus grande facilité, il n'est donc vraiment pas nécessaire de continuer à utiliser le très défectueuxrand()
Jack Aidley

1
rand () a un autre problème terrible: il utilise une sorte de verrou, appelé à l'intérieur des threads, il ralentit considérablement votre code. Au moins, il existe une version réentrante. Et si vous utilisez C ++ 11, l'API aléatoire fournit tout ce dont vous avez besoin.
Marwan Burelle

4
Pour être juste, il n'a pas demandé si c'était un bon générateur de nombres aléatoires. Il a demandé si c'était rapide. Eh bien, oui, c'est probablement le jeûne., Mais les résultats ne seront pas du tout très aléatoires.
jcoder

42

Bonne question!

Undefined ne signifie pas qu'il est aléatoire. Pensez-y, les valeurs que vous obtiendriez dans les variables globales non initialisées y ont été laissées par le système ou vos / autres applications en cours d'exécution. Selon ce que fait votre système avec la mémoire qui n'est plus utilisée et / ou le type de valeurs générées par le système et les applications, vous pouvez obtenir:

  1. Toujours le même.
  2. Faites partie d'un petit ensemble de valeurs.
  3. Obtenez des valeurs dans une ou plusieurs petites plages.
  4. Voir de nombreuses valeurs divisibles par 2/4/8 à partir de pointeurs sur un système 16/32/64 bits
  5. ...

Les valeurs que vous obtiendrez dépendent entièrement des valeurs non aléatoires laissées par le système et / ou les applications. Ainsi, en effet, il y aura du bruit (à moins que votre système n'efface plus la mémoire), mais le pool de valeurs dont vous tirerez ne sera en aucun cas aléatoire.

Les choses empirent pour les variables locales car elles proviennent directement de la pile de votre propre programme. Il y a de très bonnes chances que votre programme écrive réellement ces emplacements de pile pendant l'exécution d'un autre code. J'estime que les chances de chance dans cette situation sont très faibles, et un changement de code «aléatoire» que vous effectuez tente cette chance.

Lisez à propos de l' aléatoire . Comme vous le verrez, le caractère aléatoire est une propriété très spécifique et difficile à obtenir. C'est une erreur courante de penser que si vous prenez simplement quelque chose qui est difficile à suivre (comme votre suggestion), vous obtiendrez une valeur aléatoire.


7
... et cela laisse de côté toutes les optimisations du compilateur qui videraient complètement ce code.
Déduplicateur

6 ... Vous obtiendrez un "caractère aléatoire" différent dans Debug et Release. Undefined signifie que vous vous trompez.
Sql Surfer

Droite. Je résumerais ou résumerais par "non défini"! = "Arbitraire"! = "Aléatoire". Tous ces types d '«inconnues» ont des propriétés différentes.
fche

Les variables globales sont garanties d'avoir une valeur définie, qu'elles soient explicitement initialisées ou non. C'est certainement vrai en C ++ et en C également .
Brian Vandenberg

32

Beaucoup de bonnes réponses, mais permettez-moi d'en ajouter une autre et d'insister sur le fait que dans un ordinateur déterministe, rien n'est aléatoire. Cela est vrai à la fois pour les nombres produits par un pseudo-RNG et pour les nombres apparemment "aléatoires" trouvés dans les zones de mémoire réservées aux variables locales C / C ++ sur la pile.

MAIS ... il y a une différence cruciale.

Les nombres générés par un bon générateur pseudo-aléatoire ont les propriétés qui les rendent statistiquement similaires à des tirages vraiment aléatoires. Par exemple, la distribution est uniforme. La durée du cycle est longue: vous pouvez obtenir des millions de nombres aléatoires avant que le cycle ne se répète. La séquence n'est pas autocorrélée: par exemple, vous ne commencerez pas à voir des motifs étranges émerger si vous prenez tous les 2, 3 ou 27 nombres, ou si vous regardez des chiffres spécifiques dans les nombres générés.

En revanche, les nombres "aléatoires" laissés sur la pile n'ont aucune de ces propriétés. Leurs valeurs et leur caractère aléatoire apparent dépendent entièrement de la façon dont le programme est construit, comment il est compilé et comment il est optimisé par le compilateur. À titre d'exemple, voici une variante de votre idée en tant que programme autonome:

#include <stdio.h>

notrandom()
{
        int r, g, b;

        printf("R=%d, G=%d, B=%d", r&255, g&255, b&255);
}

int main(int argc, char *argv[])
{
        int i;
        for (i = 0; i < 10; i++)
        {
                notrandom();
                printf("\n");
        }

        return 0;
}

Lorsque je compile ce code avec GCC sur une machine Linux et que je l'exécute, il s'avère être plutôt désagréablement déterministe:

R=0, G=19, B=0
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255

Si vous regardiez le code compilé avec un désassembleur, vous pourriez reconstruire ce qui se passait, en détail. Le premier appel à notrandom () a utilisé une zone de la pile qui n'était pas utilisée par ce programme auparavant; qui sait ce qu'il y avait dedans. Mais après cet appel à notrandom (), il y a un appel à printf () (que le compilateur GCC optimise en fait à un appel à putchar (), mais peu importe) et qui écrase la pile. Ainsi, la fois suivante et suivante, lorsque notrandom () est appelé, la pile contiendra des données périmées de l'exécution de putchar (), et puisque putchar () est toujours appelé avec les mêmes arguments, ces données périmées seront toujours les mêmes, aussi.

Il n'y a donc absolument rien aléatoire dans ce comportement, et les nombres ainsi obtenus n'ont aucune des propriétés souhaitables d'un générateur de nombres pseudo-aléatoires bien écrit. En fait, dans la plupart des scénarios réels, leurs valeurs seront répétitives et fortement corrélées.

En effet, comme d'autres, j'envisagerais également sérieusement de licencier quelqu'un qui a tenté de faire passer cette idée comme un "RNG haute performance".


1
«Dans un ordinateur déterministe, rien n'est aléatoire» - Ce n'est pas vrai en réalité. Les ordinateurs modernes contiennent toutes sortes de capteurs qui vous permettent de produire un véritable aléatoire imprévisible sans générateurs matériels séparés. Sur une architecture moderne, les valeurs de /dev/randomsont souvent issues de telles sources matérielles, et sont en fait du «bruit quantique», c'est-à-dire vraiment imprévisible au meilleur sens physique du terme.
Konrad Rudolph

2
Mais alors, ce n'est pas un ordinateur déterministe, n'est-ce pas? Vous comptez maintenant sur les données environnementales. En tout cas, cela nous amène bien au-delà de la discussion d'un pseudo-RNG conventionnel par rapport aux bits "aléatoires" dans la mémoire non initialisée. Aussi ... regardez la description de / dev / random pour apprécier à quel point les implémenteurs sont allés loin pour s'assurer que les nombres aléatoires sont cryptographiquement sécurisés ... précisément parce que les sources d'entrée ne sont pas du bruit quantique pur et non corrélé mais plutôt, des lectures de capteurs potentiellement hautement corrélées avec seulement un faible degré de hasard. C'est assez lent aussi.
Viktor Toth

29

Un comportement indéfini signifie que les auteurs des compilateurs sont libres d'ignorer le problème car les programmeurs n'auront jamais le droit de se plaindre quoi qu'il arrive.

Alors qu'en théorie, lorsque vous entrez dans un terrain UB, tout peut arriver (y compris un démon volant de votre nez ), ce qui signifie normalement que les auteurs du compilateur ne s'en soucient pas et, pour les variables locales, la valeur sera tout ce qui est dans la mémoire de la pile à ce moment-là .

Cela signifie également que souvent le contenu sera "étrange" mais fixe ou légèrement aléatoire ou variable mais avec un modèle évident clair (par exemple, augmentant les valeurs à chaque itération).

Pour sûr, vous ne pouvez pas vous attendre à ce qu'il soit un générateur aléatoire décent.


28

Le comportement indéfini n'est pas défini. Cela ne signifie pas que vous obtenez une valeur indéfinie, cela signifie que le programme peut faire n'importe quoi et toujours répondre aux spécifications du langage.

Un bon compilateur d'optimisation devrait prendre

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

et compilez-le dans un noop. C'est certainement plus rapide que n'importe quelle alternative. Il a l'inconvénient de ne rien faire, mais tel est l'inconvénient d'un comportement indéfini.


3
Beaucoup dépend si le but d'un compilateur est d'aider les programmeurs à produire des fichiers exécutables qui répondent aux exigences du domaine, ou si le but est de produire l'exécutable le plus "efficace" dont le comportement sera cohérent avec les exigences minimales de la norme C, sans déterminer si un tel comportement servira à quelque fin que ce soit. En ce qui concerne le premier objectif, faire en sorte que le code utilise des valeurs initiales arbitraires pour r, g, b, ou déclencher une interruption du débogueur si cela est pratique, serait plus utile que de transformer le code en nop. En ce qui concerne ce dernier objectif ...
supercat

2
... un compilateur optimal devrait déterminer quelles entrées entraîneraient l'exécution de la méthode ci-dessus et éliminer tout code qui ne serait pertinent que lorsque de telles entrées sont reçues.
supercat

1
@supercat Ou son objectif pourrait être C. de produire des exécutables efficaces en conformité avec la norme tout en aidant le programmeur à trouver des endroits où la conformité peut ne pas être utile. Les compilateurs peuvent atteindre cet objectif de compromis en émettant plus de diagnostics que ne l'exige la norme, comme les GCC -Wall -Wextra.
Damian Yerrick

1
Le fait que les valeurs ne soient pas définies ne signifie pas que le comportement du code environnant n'est pas défini. Aucun compilateur ne devrait utiliser cette fonction. Les deux appels de fonction, quelles que soient les entrées qui leur sont données, DOIVENT absolument être appelés; le premier DOIT être appelé avec trois nombres entre 0 et 255 et le second DOIT être appelé avec une valeur vraie ou fausse. Un "bon compilateur d'optimisation" pourrait optimiser les paramètres de la fonction à des valeurs statiques arbitraires, en supprimant complètement les variables, mais c'est aussi loin que possible (enfin, à moins que les fonctions elles-mêmes ne puissent être réduites à noops sur certaines entrées).
Dewi Morgan

@DewiMorgan - comme les fonctions appelées sont du type "définir ce paramètre", elles se réduisent presque certainement à noops lorsque l'entrée est la même que la valeur actuelle du paramètre, ce que le compilateur est libre de supposer que c'est le cas.
Jules

18

Pas encore mentionné, mais les chemins de code qui invoquent un comportement non défini sont autorisés à faire tout ce que le compilateur veut, par exemple

void updateEffect(){}

Ce qui est certainement plus rapide que votre boucle correcte, et à cause de l'UB, est parfaitement conforme.


18

Pour des raisons de sécurité, la nouvelle mémoire affectée à un programme doit être nettoyée, sinon les informations pourraient être utilisées et les mots de passe pourraient fuir d'une application à une autre. Ce n'est que lorsque vous réutilisez la mémoire que vous obtenez des valeurs différentes de 0. Et il est très probable que sur une pile la valeur précédente soit juste fixe, car l'utilisation précédente de cette mémoire est fixe.


13

Votre exemple de code particulier ne ferait probablement pas ce que vous attendez. Alors que techniquement chaque itération de la boucle recrée les variables locales pour les valeurs r, g et b, en pratique c'est exactement le même espace mémoire sur la pile. Par conséquent, il ne sera pas re-randomisé à chaque itération, et vous finirez par attribuer les mêmes 3 valeurs pour chacune des 1000 couleurs, quelle que soit la façon dont les r, g et b sont aléatoires individuellement et initialement.

En effet, si cela fonctionnait, je serais très curieux de savoir ce qui le re-randomise. La seule chose à laquelle je peux penser serait une interruption entrelacée qui empaqueterait au sommet de cette pile, très peu probable. Peut-être qu'une optimisation interne qui les garderait comme variables de registre plutôt que comme de véritables emplacements de mémoire, où les registres sont réutilisés plus bas dans la boucle, ferait aussi l'affaire, surtout si la fonction de visibilité définie est particulièrement gourmande en registres. Pourtant, loin d'être aléatoire.


12

Comme la plupart des gens ici ont mentionné un comportement indéfini. Undefined signifie également que vous pouvez obtenir une valeur entière valide (heureusement) et dans ce cas, ce sera plus rapide (car l'appel de fonction rand n'est pas effectué). Mais ne l'utilisez pas pratiquement. Je suis sûr que ce sera terrible, car la chance n'est pas toujours avec vous.


1
Très bon point! C'est peut-être une astuce pragmatique, mais qui requiert en effet de la chance.
sens importe

1
Il n'y a absolument aucune chance. Si le compilateur n'optimise pas le comportement indéfini, les valeurs que vous obtiendrez seront parfaitement déterministes (= dépendent entièrement de votre programme, de ses entrées, de son compilateur, des bibliothèques qu'il utilise, du timing de ses threads s'il a des threads). Le problème est que vous ne pouvez pas raisonner sur ces valeurs car elles dépendent des détails d'implémentation.
cmaster - réintègre monica le

En l'absence d'un système d'exploitation avec une pile de gestion des interruptions distincte de la pile d'application, la chance pourrait bien être impliquée, car les interruptions perturberont fréquemment le contenu de la mémoire légèrement au-delà du contenu de la pile actuelle.
supercat

12

Vraiment mauvais! Mauvaise habitude, mauvais résultat. Considérer:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

Si la fonction A_Function_that_use_a_lot_the_Stack()fait toujours la même initialisation, elle laisse la pile avec les mêmes données dessus. Ces données sont ce que nous appelons updateEffect(): toujours la même valeur! .


11

J'ai effectué un test très simple, et ce n'était pas du tout aléatoire.

#include <stdio.h>

int main() {

    int a;
    printf("%d\n", a);
    return 0;
}

Chaque fois que j'ai exécuté le programme, il imprimait le même numéro ( 32767dans mon cas) - vous ne pouvez pas obtenir beaucoup moins aléatoire que cela. Il s'agit probablement du code de démarrage de la bibliothèque d'exécution laissé sur la pile. Puisqu'il utilise le même code de démarrage à chaque exécution du programme et que rien d'autre ne varie dans le programme entre les exécutions, les résultats sont parfaitement cohérents.


Bon point. Un résultat dépend fortement de l'endroit où ce générateur de nombres "aléatoires" est appelé dans le code. C'est plutôt imprévisible qu'aléatoire.
NO_NAME

10

Vous devez avoir une définition de ce que vous entendez par «aléatoire». Une définition sensée implique que les valeurs que vous obtenez doivent avoir peu de corrélation. C'est quelque chose que vous pouvez mesurer. Il n'est pas non plus trivial de réaliser de manière contrôlée et reproductible. Un comportement indéfini n'est donc certainement pas ce que vous recherchez.


7

Il existe certaines situations dans lesquelles la mémoire non initialisée peut être lue en toute sécurité en utilisant le type "unsigned char *" [par exemple un tampon renvoyé par malloc]. Le code peut lire une telle mémoire sans avoir à se soucier du fait que le compilateur jette la causalité par la fenêtre, et il y a des moments où il peut être plus efficace de préparer du code pour tout ce que la mémoire peut contenir que de s'assurer que les données non initialisées ne seront pas lues ( un exemple courant de cela serait d'utilisermemcpy sur un tampon partiellement initialisé plutôt que de copier discrètement tous les éléments qui contiennent des données significatives).

Même dans de tels cas, cependant, il faut toujours supposer que si une combinaison d'octets est particulièrement vexatoire, sa lecture donnera toujours ce modèle d'octets (et si un certain modèle serait vexatoire en production, mais pas en développement, un tel le modèle n'apparaîtra pas tant que le code n'est pas en production).

La lecture de la mémoire non initialisée peut être utile dans le cadre d'une stratégie de génération aléatoire dans un système embarqué où l'on peut être sûr que la mémoire n'a jamais été écrite avec un contenu sensiblement non aléatoire depuis la dernière mise sous tension du système, et si la fabrication processus utilisé pour la mémoire fait varier son état de mise sous tension de façon semi-aléatoire. Le code devrait fonctionner même si tous les appareils fournissent toujours les mêmes données, mais dans les cas où, par exemple, un groupe de nœuds doit chacun sélectionner des ID uniques arbitraires le plus rapidement possible, ayant un générateur "peu aléatoire" qui donne à la moitié des nœuds la même initiale L'ID pourrait être mieux que de ne pas avoir de source initiale de hasard.


2
"si une combinaison d'octets est particulièrement vexatoire, la lecture produira toujours ce modèle d'octets" - jusqu'à ce que vous codiez pour faire face à ce modèle, auquel cas il n'est plus vexatoire et un modèle différent sera lu à l'avenir.
Steve Jessop

@SteveJessop: Précisément. Ma ligne sur le développement vs la production était destinée à transmettre une notion similaire. Le code ne devrait pas se soucier de ce qui se trouve dans la mémoire non initialisée au-delà d'une vague notion de "Un certain hasard pourrait être agréable". Si le comportement du programme est affecté par le contenu d'un morceau de mémoire non initialisée, le contenu des morceaux acquis à l'avenir peut à son tour être affecté par cela.
supercat

5

Comme d'autres l'ont dit, ce sera rapide, mais pas aléatoire.

Ce que la plupart des compilateurs feront pour les variables locales, c'est de prendre de l'espace pour eux sur la pile, mais pas la peine de le définir sur quoi que ce soit (la norme dit qu'ils n'en ont pas besoin, alors pourquoi ralentir le code que vous générez?).

Dans ce cas, la valeur que vous obtiendrez dépendra de ce qui était précédemment sur la pile - si vous appelez une fonction avant celle-ci qui a une centaine de variables de caractères locales toutes définies sur 'Q' puis appelez votre fonction après qui revient, alors vous trouverez probablement que vos valeurs «aléatoires» se comportent comme si vous les aviez memset()toutes à «Q».

Surtout pour votre exemple de fonction essayant d'utiliser ceci, ces valeurs ne changeront pas chaque fois que vous les lirez, elles seront les mêmes à chaque fois. Ainsi, vous obtiendrez 100 étoiles toutes définies sur la même couleur et la même visibilité.

De plus, rien ne dit que le compilateur ne devrait pas initialiser ces valeurs - donc un futur compilateur pourrait le faire.

En général: mauvaise idée, ne le faites pas. (comme beaucoup d'optimisations de niveau de code "intelligentes" vraiment ...)


2
Vous faites de fortes prédictions sur ce qui se passera, bien que rien de tout cela ne soit garanti grâce à UB. Ce n'est pas vrai non plus dans la pratique.
usr

3

Comme d'autres l'ont déjà mentionné, il s'agit d'un comportement indéfini ( UB ), mais cela peut "fonctionner".

À l'exception des problèmes déjà mentionnés par d'autres, je vois un autre problème (inconvénient) - il ne fonctionnera pas dans un langage autre que C et C ++. Je sais que cette question concerne le C ++, mais si vous pouvez écrire du code qui sera un bon code C ++ et Java et que ce n'est pas un problème, alors pourquoi pas? Peut-être qu'un jour, quelqu'un devra le porter dans une autre langue et rechercher des bugs causés par "tours de magie" UB comme celui-ci sera certainement un cauchemar (en particulier pour un développeur C / C ++ inexpérimenté).

Ici, il est question d'un autre UB similaire. Imaginez-vous essayer de trouver un bug comme celui-ci sans connaître cet UB. Si vous voulez en savoir plus sur ces choses étranges en C / C ++, lisez les réponses aux questions du lien et voyez ce GRAND diaporama. Cela vous aidera à comprendre ce qui se cache sous le capot et comment cela fonctionne; ce n'est pas seulement un autre diaporama plein de "magie". Je suis sûr que même la plupart des programmeurs C / c ++ expérimentés peuvent en apprendre beaucoup.


3

Ce n'est pas une bonne idée de s'appuyer sur une logique quelconque sur un comportement indéfini du langage. En plus de tout ce qui est mentionné / discuté dans cet article, je voudrais mentionner qu'avec l'approche / le style C ++ moderne, ce programme peut ne pas être compilé.

Cela a été mentionné dans mon post précédent qui contient l'avantage de la fonction automatique et un lien utile pour la même chose.

https://stackoverflow.com/a/26170069/2724703

Donc, si nous modifions le code ci-dessus et remplaçons les types réels par auto , le programme ne compilera même pas.

void updateEffect(){
    for(int i=0;i<1000;i++){
        auto r;
        auto g;
        auto b;
        star[i].setColor(r%255,g%255,b%255);
        auto isVisible;
        star[i].setVisible(isVisible);
    }
}

3

J'aime ta façon de penser. Vraiment hors des sentiers battus. Cependant, le compromis n'en vaut vraiment pas la peine. Le compromis mémoire-exécution est une chose, y compris un comportement non défini pour l'exécution n'est pas .

Cela doit vous donner un sentiment très troublant de savoir que vous utilisez un tel "aléatoire" que votre logique métier. Je ne le ferais pas.


3

Utilisez 7757chaque endroit où vous êtes tenté d'utiliser des variables non initialisées. Je l'ai choisi au hasard dans une liste de nombres premiers:

  1. c'est un comportement défini

  2. il est garanti de ne pas toujours être 0

  3. c'est premier

  4. il est susceptible d'être aussi aléatoire statistiquement que les variables non initialisées

  5. il est susceptible d'être plus rapide que les variables non initialisées car sa valeur est connue au moment de la compilation


Pour comparaison, voir les résultats dans cette réponse: stackoverflow.com/a/31836461/2963099
Glenn Teitelbaum

1

Il y a encore une possibilité à considérer.

Les compilateurs modernes (ahem g ++) sont si intelligents qu'ils parcourent votre code pour voir quelles instructions affectent l'état, et ce qui ne le fait pas, et si une instruction est garantie de ne PAS affecter l'état, g ++ supprimera simplement cette instruction.

Voici donc ce qui va se passer. g ++ verra certainement que vous lisez, effectuez de l'arithmétique, enregistrez, ce qui est essentiellement une valeur de déchets, ce qui produit plus de déchets. Puisqu'il n'y a aucune garantie que la nouvelle poubelle soit plus utile que l'ancienne, elle supprimera simplement votre boucle. BLOOP!

Cette méthode est utile, mais voici ce que je ferais. Combinez UB (Undefined Behavior) avec la vitesse rand ().

Bien sûr, réduisez les rand()s exécutés, mais mélangez-les afin que le compilateur ne fasse rien que vous ne vouliez pas.

Et je ne te virerai pas.


Je trouve très difficile de croire qu'un compilateur peut décider que votre code fait quelque chose de stupide et le supprimer. Je m'attendrais à ce qu'il optimise uniquement le code inutilisé , pas le code déconseillé . Avez-vous un cas de test reproductible? De toute façon, la recommandation de UB est dangereuse. De plus, GCC n'est pas le seul compilateur compétent, il est donc injuste de le distinguer comme "moderne".
underscore_d

-1

L'utilisation de données non initialisées pour l'aléatoire n'est pas nécessairement une mauvaise chose si elle est effectuée correctement. En fait, OpenSSL fait exactement cela pour amorcer son PRNG.

Apparemment, cette utilisation n'était cependant pas bien documentée, car quelqu'un a remarqué que Valgrind se plaignait d'utiliser des données non initialisées et les "corrigeait", provoquant un bogue dans le PRNG .

Vous pouvez donc le faire, mais vous devez savoir ce que vous faites et vous assurer que toute personne lisant votre code comprend cela.


1
Cela va dépendre de votre compilateur, ce qui est prévu avec un comportement indéfini, comme nous pouvons le voir dans ma réponse, clang aujourd'hui ne fera pas ce qu'il veut.
Shafik Yaghmour

6
Le fait qu'OpenSSL ait utilisé cette méthode comme entrée d'entropie ne signifie pas que c'était bon. Après tout, la seule autre source d'entropie utilisée était le PID . Pas exactement une bonne valeur aléatoire. De quelqu'un qui s'appuie sur une si mauvaise source d'entropie, je ne m'attendrai pas à un bon jugement sur son autre source d'entropie. J'espère juste que les gens qui maintiennent actuellement OpenSSL sont plus brillants.
cmaster - réintègre monica le
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.