La norme C donne aux compilateurs une grande latitude pour effectuer des optimisations. Les conséquences de ces optimisations peuvent être surprenantes si l'on part d'un modèle naïf de programmes où la mémoire non initialisée est définie par un motif de bits aléatoire et où toutes les opérations sont exécutées dans l'ordre où elles sont écrites.
Remarque : les exemples suivants ne sont valables que parce que x
n'a jamais son adresse prise, il est donc "de type registre". Ils seraient également valables si le type de x
avaient des représentations pièges ; c'est rarement le cas pour les types non signés (cela nécessite de "gaspiller" au moins un bit de stockage, et doit être documenté), et impossible pour les types unsigned char
. Si x
avait un type signé, alors l'implémentation pouvait définir la configuration binaire qui n'est pas un nombre entre -(2 n-1 -1) et 2 n-1 -1 comme représentation du piège. Voir La réponse de Jens Gustedt .
Les compilateurs essaient d'affecter des registres aux variables, car les registres sont plus rapides que la mémoire. Comme le programme peut utiliser plus de variables que le processeur n'a de registres, les compilateurs effectuent une allocation de registres, ce qui fait que différentes variables utilisent le même registre à différents moments. Considérons le fragment de programme
unsigned x, y, z; /* 0 */
y = 0; /* 1 */
z = 4; /* 2 */
x = - x; /* 3 */
y = y + z; /* 4 */
x = y + 1; /* 5 */
Quand la ligne 3 est évaluée, x
n'est pas encore initialisé, donc (raisonne le compilateur) la ligne 3 doit être une sorte de coup de chance qui ne peut pas se produire en raison d'autres conditions que le compilateur n'était pas assez intelligent pour comprendre. Puisque z
n'est pas utilisé après la ligne 4, et x
n'est pas utilisé avant la ligne 5, le même registre peut être utilisé pour les deux variables. Ainsi ce petit programme est compilé aux opérations suivantes sur les registres :
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
La valeur finale de x
est la valeur finale de r0
et la valeur finale de y
est la valeur finale de r1
. Ces valeurs sont x = -3 et y = -4, et non 5 et 4 comme cela se produirait si x
a été correctement initialisé.
Pour un exemple plus élaboré, considérez le fragment de code suivant :
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Supposons que le compilateur détecte que condition
n'a aucun effet secondaire. Depuis condition
ne modifie pas x
le compilateur sait que la première exécution de la boucle ne peut pas être un accès x
puisqu'il n'est pas encore initialisé. Par conséquent, la première exécution du corps de la boucle est équivalente à x = some_value()
il n'y a pas besoin de tester la condition. Le compilateur peut compiler ce code comme si vous aviez écrit
unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
La façon dont cela peut être modélisé dans le compilateur est de considérer que toute valeur dépendant de x
tiene la valeur qui vous convient à condition que x
n'est pas initialisé. Parce que le comportement lorsqu'une variable non initialisée est indéfinie, plutôt que la variable ayant simplement une valeur non spécifiée, le compilateur n'a pas besoin de garder la trace d'une relation mathématique spéciale entre les valeurs quelconques. Ainsi, le compilateur peut analyser le code ci-dessus de cette manière :
- pendant la première itération de la boucle,
x
n'est pas initialisé au moment où -x
est évaluée.
-
-x
a un comportement non défini, donc sa valeur est ce qui est le plus commode.
- La règle d'optimisation
_condition_ ? _value_ : _value_
s'applique, donc ce code peut être simplifié en _condition_; _value_
.
Lorsqu'il est confronté au code de votre question, ce même compilateur analyse que lorsque x = - x
est évaluée, la valeur de -x
est ce qui est le plus pratique. Ainsi, l'affectation peut être optimisée.
Je n'ai pas cherché d'exemple de compilateur qui se comporte comme décrit ci-dessus, mais c'est le genre d'optimisations que les bons compilateurs essaient de faire. Je ne serais pas surpris d'en rencontrer un. Voici un exemple moins plausible d'un compilateur avec lequel votre programme se plante. (Il n'est peut-être pas si invraisemblable si vous compilez votre programme dans une sorte de mode de débogage avancé).
Ce compilateur hypothétique place chaque variable dans une page de mémoire différente et configure les attributs de page de sorte que la lecture d'une variable non initialisée provoque un piège du processeur qui invoque un débogueur. Toute affectation à une variable doit d'abord s'assurer que sa page mémoire est mappée normalement. Ce compilateur n'essaie pas d'effectuer une optimisation avancée - il est en mode débogage, destiné à localiser facilement les bogues tels que les variables non initialisées. Lorsque x = - x
est évalué, le côté droit provoque un piège et le débogueur se déclenche.