OVH Cloud OVH Cloud

post/pre incrementations et bugs de compilos

19 réponses
Avatar
Jylam
Salut a tous,

On m'a posé devant un "bug" que je n'ai pas su expliquer.

Alors, le code :



8<---------- snip

#include <stdio.h>

void f(int a, int b, int c) { printf("%i %i %i", a, b, c); }

int main(int argc, char *argv[])
{
int i = 3;
f(i++, --i, ++i);
return 0;
}

8<---------- snip

(rendu valide pour eviter les remarques ;)

Voila. Le probleme en lui meme : Voila ce que me sortent les differents
programmes compilés, suivant les compilos utilisés :

Gcc 3.3.5 x86 linux sans optim (-o0) : 3 3 4
Gcc 3.3.5 x86 linux avec optim (-o6) : 3 4 4
Visual C++ 6 Debug : 3 3 4
Visual C++ 6 Release : 3 4 4
Forte7 UltraSPARC III Solaris sans optim : 3 4 4
Forte7 UltraSPARC III Solaris avec optim : 3 4 4
Gcc 2.95 UltraSPARC III Solaris sans optim : 3 4 4
Gcc 2.95 UltraSPARC III Solaris avec optim : 3 4 4


Le resultat est sensé etre 3 3 4 (arguments de droite a gauche), selon
moi. Probleme, non seulement tous les compilos ne donnent pas ca, mais
en plus un meme compilo, suivant ses optims, peut donner un resultat
different pour un code similaire.

(Le passage a une fonction n'est pas obligatoire, afficher un printf("%d
%d %d\n", i++, --i, ++i); donne le meme bug il me semble)


Si quelqu'un avait une explication a ca, je suis preneur :)


Merci bieng.

--
Jean-Yves Lamoureux

9 réponses

1 2
Avatar
Antoine Leca
En <news:,
Jean-Marc Bourguet va escriure:
Autrement dit
int f(int a, int b) { return a * b; }
i = 0;
k = f(i++, i++);
on sait qu'apres l'execution k vaut 0 et i 2 mais on ne sait pas si
l'appel c'est fait avec a=0 et b=1 ou bien a=1 et b=0.


Ou même a=0 b=0.

De plus et surtout, entre le point de séquence à la fin de i = 0;, et celui
avant l'appel de la fonction f, i est modifié deux fois ; formellement, le
comportement est donc indéfini (et en particulier il est possible de trouver
des implémentations pour lesquelles i vaudra 1 !)


Alors qu'avec
i = 0;
k = i++ * i++;
on ne peut rien dire du tout, ni sur la valeur de k ni celle de i.


Mmmmhhh.

Oui, mais pour la même raison que ci-dessus ; la seule différence est en
effet la disparition du point de séquence avant l'appel de f ce qui fait que
l'opération d'incrémentation peut être passablement retardée ; mais cet
effet de bord est sans effet réel.

Non au sens pratique : k vaudra 0 avec tous les compilateurs (sauf celui de
la DS9k), car 0 est élément nul ; et i vaudra 2 avec la très grande
majorité, car l'incrémentation est une opération atomique pour la plupart,
et le compilateur va la poser deux fois.


Un exemple avec i = 1 et une division à la place de la multiplication serait
beaucoup plus intéressant.

int g(int a, int b) { return a / b; }
i = 1;
k = g(i++, i++);

Je dis que k peut valoir (dans la pratique) 0, 1 ou 2, avec une préférence
pour 2 dans le cas des compilateurs non optimisateurs les plus anciens (les
plus prévisibles), et i vaudra 3 avec la plupart des compilateurs.
Pour ce qui concerne l'opération en ligne,
i = 1;
k = i++ / i++;
k est encore plus difficile à prévoir, car là cela dépend de comment le
compilateur va écrire l'opération de division, ce qui est encore plus
variable que l'ordre d'évaluation des paramètres (d'ailleurs, personne ou
presque ne fait d'hypothèses sur le premier, alors que certains font des hyp
othèses sur le second, alors que formellement les deux sont non spécifiés,
en tous cas par la norme).


Antoine

Avatar
Jean-Marc Bourguet
"Antoine Leca" writes:

En <news:,
Jean-Marc Bourguet va escriure:
Autrement dit
int f(int a, int b) { return a * b; }
i = 0;
k = f(i++, i++);
on sait qu'apres l'execution k vaut 0 et i 2 mais on ne sait pas si
l'appel c'est fait avec a=0 et b=1 ou bien a=1 et b=0.


Ou même a=0 b=0.


Oops, j'ai ete voir aux sources plutot que de me baser sur ma memoire
-- ce que je devrais faire quand il s'agit de points dont je ne me
sert de toute facon pas -- et contrairement a ce que je pensais il n'y
a pas de point de sequence entre les evaluations des arguments mais
uniquement avant l'appel de la fonction.

A+

--
Jean-Marc
FAQ de fclc: http://www.isty-info.uvsq.fr/~rumeau/fclc
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Laurent Deniau
Jean-Marc Bourguet wrote:
"Antoine Leca" writes:


En <news:,
Jean-Marc Bourguet va escriure:

Autrement dit
int f(int a, int b) { return a * b; }
i = 0;
k = f(i++, i++);
on sait qu'apres l'execution k vaut 0 et i 2 mais on ne sait pas si
l'appel c'est fait avec a=0 et b=1 ou bien a=1 et b=0.


Ou même a=0 b=0.



Oops, j'ai ete voir aux sources plutot que de me baser sur ma memoire
-- ce que je devrais faire quand il s'agit de points dont je ne me
sert de toute facon pas -- et contrairement a ce que je pensais il n'y
a pas de point de sequence entre les evaluations des arguments mais
uniquement avant l'appel de la fonction.


Ni dans l'evaluation de l'expression de la fonction (ici f), un point
souvent oublie...

a+, ld.



Avatar
Charlie Gordon
"Jean-Marc Bourguet" wrote in message
news:
"Antoine Leca" writes:

En <news:,
Jean-Marc Bourguet va escriure:
Autrement dit
int f(int a, int b) { return a * b; }
i = 0;
k = f(i++, i++);
on sait qu'apres l'execution k vaut 0 et i 2 mais on ne sait pas si
l'appel c'est fait avec a=0 et b=1 ou bien a=1 et b=0.


Ou même a=0 b=0.


Oops, j'ai ete voir aux sources plutot que de me baser sur ma memoire
-- ce que je devrais faire quand il s'agit de points dont je ne me
sert de toute facon pas -- et contrairement a ce que je pensais il n'y
a pas de point de sequence entre les evaluations des arguments mais
uniquement avant l'appel de la fonction.


Exact, ce qui permet par exemple d'evaluer en parallèle sur les architectures
qui le permettent les expressions des paramètres de f. Bien sur c'est une
mauvaise idée quand elles ont des effets de bord, et le compilateur ne peut pas
ignorer que c'est le cas ici. Sauf sur la DS9000 où il est suffisamment subtil
pour constater que l'effet de bord concerne la même variable, et y voit donc une
instruction subliminale pour stopper le refroidissement du reacteur.

--
Chqrlie.



Avatar
Charlie Gordon
"Antoine Leca" wrote in message
news:dg3hhd$kuf$
En <news:, Pierre Maurette va
escriure:
Je pense qu'il n'y a qu'une seule alternative: soit respecter les
conventions d'appel, soit développer la fonction inline.


Non. Les conventions d'appel ne sont pas forcément uniques, tu peux (et
généralement tu as, pour des raisons de marché et de différenciations
marketing) des extensions ou des options (#pragma) permettant de les
modifier.
De plus, si les conventions de passage sont par pile, en général cela va
créer une contrainte d'ordre partiel. Mais si les conventions utilisent les
registres, cette contrainte disparaît ; et les compilateurs actuels vont
magouiller l'expression pour espacer le plus possible les interactions avec
chaque espace de mémoire (y compris les registres), histoire de remplir tous
les pipelines parallèles.


Ne mélange pas les conventions d'appel et l'ordre d'évaluation des paramètres,
ceux-ci peuvent être évalués dans n'importe quel ordre, voire de facon
concommitante et les résultats sont ensuite mis sur la pile dans le bon ordre
pour respecter la convention d'appel.

Voici deux techniques possibles : les parametres peuvent etre evalués dans des
registres et ensuite les registres sont poussés sur la pile dans l'ordre
adéquat. Ou encore le pointeur de pile est ajusté et les parametres sont
evalués et mis sur la pile avec un mov dans l'ordre que le compilateur choisit
avec des critères hors du controle du programmeur.

Par exemple, l'excellent compilateur en une passe de Fabrice Bellard TinyCC (
http://tinycc.org ) genere le code des expressions arguments de fonction au vol
directement dans la phase de parse. Ce code s'éxécute donc en général dans le
même ordre quelle que soit la convention d'appel, et la technique décrite
ci-dessus est utilisée pour que les paramètres soient dans le bon ordre, non
sans mal dans certains cas (listes d'arguments variables).

Se reposer sur une telle connaissance du compilateur pour écrire un code non
portable n'a d'intérêt que pour le concepteur de compilateur ou de suites de
test, c'est une pratique à bannir pour le reste des programmeurs, qui seront
forcément punis un jour ou l'autre d'avoir pris de telles habitudes risquées.

De plus des cas simples comme fun(i++, i++) devraient être refusés par le
compilateur qui devrait détecter le comportement indéfini facilement
identifiable.

--
Chqrlie.


Avatar
Jean-Marc Bourguet
"Charlie Gordon" writes:

"Jean-Marc Bourguet" wrote in message
news:
"Antoine Leca" writes:

En <news:,
Jean-Marc Bourguet va escriure:
Autrement dit
int f(int a, int b) { return a * b; }
i = 0;
k = f(i++, i++);
on sait qu'apres l'execution k vaut 0 et i 2 mais on ne sait pas si
l'appel c'est fait avec a=0 et b=1 ou bien a=1 et b=0.


Ou même a=0 b=0.


Oops, j'ai ete voir aux sources plutot que de me baser sur ma memoire
-- ce que je devrais faire quand il s'agit de points dont je ne me
sert de toute facon pas -- et contrairement a ce que je pensais il n'y
a pas de point de sequence entre les evaluations des arguments mais
uniquement avant l'appel de la fonction.


Exact, ce qui permet par exemple d'evaluer en parallèle sur les
architectures qui le permettent les expressions des paramètres de f.


Ce n'est permis evidemment que quand ces parametres sont des
expressions sans appels de fonction, sinon il y a quelques points de
sequencement en plus (mais leur ordre relatif n'est pas fixe). Pour
reprendre mon exemple:

int incr(int* i) {
return (*i)++;
}
i = 0;
k = f(incr(i), incr(i));

ici on est sur que l'appel a f se fait avec a=0 et b=1 ou a=1 et b=0.
(Ou alors il y a autre chose qui m'echappe).

Plus subtil, est-ce que

k = f(incr(i), i++);

a les memes garanties? (Pas le temps de faire un exegese pour essayer
d'apporter une reponse). Naturellement, je laisse

int j = 0;
incr(int*i) { j++; return (*i)++;}
k = f(incr(i), i++ + j++);

a ceux qui veulent reellement s'amuser.

Une chose qui m'echappe c'est un exemple realiste ou ca apporte un
gain et ou la regle du "as-if" ne permet pas le meme gain. Il faut
avouer que j'ai le meme probleme pour l'ordre d'evaluation des
parametres (si j'admets que ca simplifie un peu le compilateur, je ne
percois pas de gain en performance).

A+

--
Jean-Marc
FAQ de fclc: http://www.isty-info.uvsq.fr/~rumeau/fclc
Site de usenet-fr: http://www.usenet-fr.news.eu.org




Avatar
Antoine Leca
En <news:,
Jean-Marc Bourguet va escriure:
Ce n'est permis evidemment que quand ces parametres sont des
expressions sans appels de fonction, sinon il y a quelques points de
sequencement en plus


Oui.

(mais leur ordre relatif n'est pas fixe).


Oui.

Pour reprendre mon exemple:

int incr(int* i) {
return (*i)++;
}
i = 0;
k = f(incr(i), incr(i));


Mmmh, avoir deux objets nommés i n'aide pas...

ici on est sur que l'appel a f se fait avec a=0 et b=1 ou a=1 et b=0.
(Ou alors il y a autre chose qui m'echappe).


Bon, à moi il y a beaucoup de choses qui m'échappent ;-)
Tu as fait beaucoup trop de copier-coller avant de poster :-)

Alors, on part de main::i (qui est donc devenu int*), initialisé à NULL (!)
Bouh. On va donc corriger cela pour écrire plutôt

k = f(incr(&i), incr(&i)); /* ;-) */

Reprenons : on part de main::i, initialisé à 0. Dans le contexte de l'appel
de la fonction f, avant le point de séquence de l'appel, nous avons deux
appels (indépendants, théoriquement exécutables sur deux processeurs
distincts pour reprendre le cas de Chqrlie) ; vu que les deux appels
modifient tous les deux l'objet main::i, il faut s'attendre à des soucis
;-).

Certes, chacun des deux appels à incr sont OK : pour chacun d'entre eux, on
crée un objet (avec type pointeur, incr::i), on lui donne une valeur
(&main::i), on a un point de séquence dans le sous-fil (sans effet notable),
puis deux opérations sérialisées (affectation du résultat et
incrémentation), puis un autre point de séquence, puis destruction de
incr::i et mise à disposition du résultat.
Mais ces points de séquence n'affectent _pas_ l'autre appel à incr (on se
demande bien comment il ferait).
Résultat, le comportement est indéfini.


Plus subtil, est-ce que

k = f(incr(i), i++);

a les memes garanties?


De comportement indéfini ? Oui.


Naturellement, je laisse

int j = 0;
incr(int*i) { j++; return (*i)++;}
k = f(incr(i), i++ + j++);

a ceux qui veulent reellement s'amuser.


Tu penses probablement à

int j = 0; /*globale*/
static int incr(int*p) { j++; return (*p)++;} /*changement de nom*/
extern int f(int a, int b);
int main(){
int i=0;
return k= f(incr(&i), i++ + j++); }

i est modifié deux fois avant l'appel à f, j aussi, le comportement est
indéfini (deux fois).

Un compilateur moyennement optimisateur va simplifier cela en

return k= f( (0, j++, i++), i++ + j++); }

(le 0, représente le point de séquence à l'appel de incr), et le problème
devrait être évident.


Une chose qui m'echappe c'est un exemple realiste ou ca apporte un
gain et ou la regle du "as-if" ne permet pas le meme gain.


Je ne comprend pas la question. Gain pour qui ?

La règle de comportement indéfini vient du fait qu'avant la normalisation,
on rendontrait les différentes possibilités dans la nature (quelquefois sur
le même compilo avec des options différentes), et rien ne justifiait de
privilégier un sens plutôt qu'un autre ; et au contraire on voyait bien le
parti que pouvait tirer les optimisateurs sur la possibilité d'ordonner les
évaluations de sous-expressions à leur guise, y compris dans les expressions
complexes quand il y a pénurie de registres (spilling).
Donc la liberté a été donnée aux compilateurs, et pas aux programmeurs.


Antoine

Avatar
Antoine Leca
En <news:dg8rse$ha3$, Charlie Gordon va escriure:
"Antoine Leca" wrote in message
news:dg3hhd$kuf$
En <news:, Pierre Maurette va
escriure:
Je pense qu'il n'y a qu'une seule alternative: soit respecter les
conventions d'appel, soit développer la fonction inline.


[...] si les conventions de passage sont par pile, en général
cela va créer une contrainte d'ordre partiel.


Ne mélange pas les conventions d'appel et l'ordre d'évaluation des
paramètres, ceux-ci peuvent être évalués dans n'importe quel ordre,


Je ne mélangeais pas : là où tu écris « peuvent », j'avais écrit « en
général », avec exactement la même idée.

voire de facon concommitante et les résultats sont ensuite mis sur la
pile dans le bon ordre pour respecter la convention d'appel.


Même la mise en pile peut se faire en désordre (les empilages sont la
plupart des cas des écritures en mémoire, qui est indexée et non pas à accès
séquentiel ; sans compter les effets du cache). J'ai déjà vu des
compilateurs « réutiliser » les arguments de la pile d'un précédent appel à
une fonction (locale), en particulier this.
La seule chose qui importe, c'est que le processeur « voit » dans le
contexte de la fonction appelée les paramètres à la place attendue, le
nettoyage de la pile, et la préservation des invariants ; le reste, c'est de
la cuisine.


Antoine



Avatar
Charlie Gordon
Jean-Marc Bourguet wrote:

"Charlie Gordon" writes:

Exact, ce qui permet par exemple d'evaluer en parallèle sur les
architectures qui le permettent les expressions des paramètres de f.


Ce n'est permis evidemment que quand ces parametres sont des
expressions sans appels de fonction, sinon il y a quelques points de
sequencement en plus (mais leur ordre relatif n'est pas fixe). Pour
reprendre mon exemple:

int incr(int* i) {
return (*i)++;
}
i = 0;
k = f(incr(i), incr(i));
ici on est sur que l'appel a f se fait avec a=0 et b=1 ou a=1 et b=0.
(Ou alors il y a autre chose qui m'echappe).


Techniquement, ce n'est pas vrai, car tu as un beau comportement
indéfini ;-)

Pour que ton code compile, il faut que int soit declaré comme int * et donc
tu passes NULL à incr dans la derniere ligne.

Le passage par référence doit être explicite en C, ce qui est une
bénédiction et évite d'avoir à se poser des questions métaphysiques à
chaque appel de fonction : peut-elle avoir un effet de bord sur les
paramètre ???

Plus subtil, est-ce que

k = f(incr(i), i++);

a les memes garanties? (Pas le temps de faire un exegese pour essayer
d'apporter une reponse).


Tu voulais dir k = f(incr(&i), i++);
parce que ton code est correct avec int *i; mais est un autre exemple de
comportement indéfini.

Naturellement, je laisse

int j = 0;
incr(int*i) { j++; return (*i)++;}
k = f(incr(i), i++ + j++);


La encore avec la définition int *i; manquante cela compile, mais quel
délire !

a ceux qui veulent reellement s'amuser.


Sans aucun doute ;-)

Une chose qui m'echappe c'est un exemple realiste ou ca apporte un
gain et ou la regle du "as-if" ne permet pas le meme gain. Il faut
avouer que j'ai le meme probleme pour l'ordre d'evaluation des
parametres (si j'admets que ca simplifie un peu le compilateur, je ne
percois pas de gain en performance).


Comme d'habitude, au moment de la normalisation, différents compilateurs
implémentaient déjà des comportements différents et incohérents sur de
telles expressions, donc le comité a décidé d'entériner l'existant qui a
l'avantage de laisser une certaine liberté au compilateur. C# a pris un
chemin différent, Microsoft semble avoir spécifié le comportement
systématique de son compilateur pour ce genre de construction, ce qui est
sans doute une bonne chose pour les programmeurs, mais va en produire un
génération qui seront incapables de s'adapter au C ou au C++
ultérieurement. Je ne sais pas ce qu'il en est de java.

--
Chqrlie.


1 2