Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Adresse d'une variable automatique et appel terminal

6 réponses
Avatar
Manuel Pégourié-Gonnard
Bonjour,

On sait bien que c'est une mauvaise idée pour une fonction de retourner
l'adresse d'une variable automatique, car l'objet correspondant aura
cessé de vivre à la fin de la fonction.

Par contre, je me demande si c'est légal et portable de passer l'adresse
d'une variable automatique en argument à une autre fonction, en
particulier quand l'appel est terminal. Par exemple :

#include <stdio.h>

void bar(int *n) {
printf("%i\n", *n);
}

void foo(void) {
int a = 42;
bar(&a);
}

int main(void) {
foo();
return 0;
}

Il me semble que, l'appel à bar() étant terminal, le compilo devrait
l'optimiser et réutiliser la stack frame de foo() pour bar(), terminant
ainsi la vie de la variable 'a' au moment de l'appel (saut) à bar(), et
que donc les choses devraient mal se passer.

Pourtant, si je fais l'essai, ça « marche » : le compilo (gcc -ansi
-pedantic -Wall -Wextra -O2) ne bronche pas et à l'exécution, ça écrit
bien 42.

En fait, si j'examine l'assembleur généré, je me rends compte qu'avec
-O0 l'appel récursif n'est pas optimisé, et qu'à partir de -O1, foo et
bar sont inlinés, de sorte que le problème ne se pose plus. Dans la
situation un peu plus complexe où je me posai initialement la question,
à partir de -O1 aussi une des fonctions en jeu est inlinée.

Est-ce juste un hasard que dans les situations où j'ai testé, avec le
compilateur et les options que j'ai essayées, ça « marche » à tous les
coup, ou pas ?

--
Manuel Pégourié-Gonnard - http://people.math.jussieu.fr/~mpg/

6 réponses

Avatar
Jean-Marc Bourguet
Manuel Pégourié-Gonnard writes:

#include <stdio.h>

void bar(int *n) {
printf("%in", *n);
}

void foo(void) {
int a = 42;
bar(&a);
}

int main(void) {
foo();
return 0;
}

Il me semble que, l'appel à bar() étant terminal, le compilo devrait



pourrait, les optimisations sont rarement obligatoires,

l'optimiser et réutiliser la stack frame de foo() pour bar(), terminant
ainsi la vie de la variable 'a' au moment de l'appel (saut) à bar(), et
que donc les choses devraient mal se passer.



La durée de vie logique de a va jusqu'au retour de foo, le compilateur ne
peut pas (sous peine de ne plus être conforme), la réduire. Et comme la
transformation d'un appel terminal en saut n'est pas une optimisation
classique des compilateurs C, ne pas être conforme sur ce point poserait
des problèmes.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
espie
Tiens, dans mon gcc un peu plus ancien, il n'y a pas d'inlining, mais
on voit bien la tail-recursion se mettre en place seulement si elle
est raisonnable.

foo:
.LFB11:
subq $24, %rsp
.LCFI0:
leaq 20(%rsp), %rdi
movl $42, 20(%rsp)
call bar
addq $24, %rsp
ret


et si je declare a comme static, hop d'un coup:
foo:
.LFB11:
movl $a.2536, %edi
jmp bar


Et pour repondre a ta question. Oui, bien sur, ton code est valide.
Les optimisations n'entrent pas vraiment en compte dans la norme.
Plus precisement: le compilo a le droit d'optimiser tout ce qu'il veut:
1/ tant qu'il ne perd pas la validite d'un code conforme;
2/ tant qu'il ne sort pas de la description de ses comportements "definis
par l'implementation"
3/ tant qu'il ne modifie pas un comportement observable.


Le 1/ etant particulierement piegant, obeissant au vieil adage
"Garbage In, garbage out": un compilo devrait sortir un diagnostic sur
du code non-conforme, s'il peut --- pas toujours decidable, typiquement pour
les problemes d'intervalles de valeur, mais il a le droit de degainer des
optimisations qui ne sont valides que sur du code conforme, puisque justement,
un code non conforme, par definition, va faire n'importe quoi... ;)
Avatar
Antoine Leca
Manuel Pégourié-Gonnard écrivit :
On sait bien que c'est une mauvaise idée pour une fonction de retourner
l'adresse d'une variable automatique, car l'objet correspondant aura
cessé de vivre à la fin de la fonction.

Par contre, je me demande si c'est légal et portable de passer l'adresse
d'une variable automatique en argument à une autre fonction, en
particulier quand l'appel est terminal.



Même quand l'appel est terminal, la fin de la fonction c'est lorsqu'elle
revient à son appelant (éventuellement au travers du RET d'une autre
fonction), pas lorsqu'elle saute à la fonction terminale.

void foo(void) {
int a = 42;
bar(&a);
}
Il me semble que, l'appel à bar() étant terminal, le compilo devrait
l'optimiser et réutiliser la stack frame de foo() pour bar(), terminant
ainsi la vie de la variable 'a' au moment de l'appel (saut) à bar(),



Réutiliser le cadre de pile ne signifie pas forcément le détruire...
Cela dépend évidemment des architectures (ABI), mais tu peux parfois
élargir le cadre de pile, à charge pour la fonction terminale, à _sa_
sortie, de détruire l'ensemble du cadre, l'extension comme la base.


Est-ce juste un hasard que dans les situations où j'ai testé, avec le
compilateur et les options que j'ai essayées, ça « marche » à tous les
coup, ou pas ?



Le compilateur sait qu'il doit générer la destruction du cadre de pile,
c'est dans son arbre; ainsi, dans un compilateur minimaliste, s'il y a
nécessité de destruction de cadre de pile, un 'return' intermédiaire va
générer un saut vers l'instruction finale de destruction, tandis que
s'il n'y en a pas le compilateur peut placer un simple RET.

Donc, s'il sait que cette instruction doit être exécutée, et s'il y a
appel terminal et qu'il y ait possibilité d'optimisation, quand le
compilateur réordonne (intervertit) les deux opérations il se doit de
vérifier que cela n'invalide pas quelque invariant ; un oubli à ce
niveau serait clairement un bogue du compilateur.


Antoine
Avatar
Tonton Th
On 01/10/2012 11:45 AM, Antoine Leca wrote:

void foo(void) {
int a = 42;
bar(&a);
}
Il me semble que, l'appel à bar() étant terminal, le compilo devrait
l'optimiser et réutiliser la stack frame de foo() pour bar(), terminant
ainsi la vie de la variable 'a' au moment de l'appel (saut) à bar(),



Réutiliser le cadre de pile ne signifie pas forcément le détruire...
Cela dépend évidemment des architectures (ABI), mais tu peux parfois
élargir le cadre de pile, à charge pour la fonction terminale, à _sa_
sortie, de détruire l'ensemble du cadre, l'extension comme la base.



C'est exactement comme ça que fonctionnait le cross-compilateur
68k de Whitesmiths. Sjmsb, on pouvait lui donner des limites à
l'entassement des stackframes qu'il était autorisé à faire avant
de "nettoyer" la pile. Et en 89/91, vu la taille des RAMs, la
jonglerie était parfois nécessaire :)

--

Nous vivons dans un monde étrange/
http://foo.bar.quux.over-blog.com/
Avatar
Manuel Pégourié-Gonnard
Marc Espie scripsit :


Tiens, dans mon gcc un peu plus ancien, il n'y a pas d'inlining,



Ah, en fait je pense que ma confusion est plus en cause que l'âge de ton
gcc : je réalise en relisant que le code donc j'avais examiné la sortie
en assembleur n'est pas précisément celui que j'ai posté, la différence
étant que foo et bar étaient static dans mon code. Ce qui semble une
explication plausible à la différence observée. (Encore que le compilo
pourrait peut-être décider d'inliner certains appels à ces fonctions
*et* d'en garder une version non inlinée pour lier avec le reste du
code, j'imagine.)

mais
on voit bien la tail-recursion se mettre en place seulement si elle
est raisonnable.

foo:
.LFB11:
subq $24, %rsp
.LCFI0:
leaq 20(%rsp), %rdi
movl $42, 20(%rsp)
call bar
addq $24, %rsp
ret


et si je declare a comme static, hop d'un coup:
foo:
.LFB11:
movl $a.2536, %edi
jmp bar



Ah, bien vu, je cherchais un moyen de voir si le compilo évitait
volontairement cette optimisation dans ce cas, je n'avais pas pensé à
déclarer a comme static pour voir.

Et pour repondre a ta question. Oui, bien sur, ton code est valide.
Les optimisations n'entrent pas vraiment en compte dans la norme.
Plus precisement: le compilo a le droit d'optimiser tout ce qu'il veut:
1/ tant qu'il ne perd pas la validite d'un code conforme;
2/ tant qu'il ne sort pas de la description de ses comportements "definis
par l'implementation"
3/ tant qu'il ne modifie pas un comportement observable.



Ok, c'est très clair. Merci à toi, Jean-Marc et Antoine pour vos réponses.

En fait, comme l'optimisation des appels terminaux me parait
particulièrement importante, je craignais qu'il n'y ait une exception (à
la durée de vie des variables) prévue pour la faciliter. Pour une fois,
les choses sont claires et la durée de vie est celle qu'on attend dans
tous les cas, ce n'est pas moi qui vait m'en plaindre.

--
Manuel Pégourié-Gonnard - http://people.math.jussieu.fr/~mpg/
Avatar
espie
In article <jei0gj$u0$,
Manuel Pégourié-Gonnard wrote:
Marc Espie scripsit :


Tiens, dans mon gcc un peu plus ancien, il n'y a pas d'inlining,



Ah, en fait je pense que ma confusion est plus en cause que l'âge de ton
gcc : je réalise en relisant que le code donc j'avais examiné la sortie
en assembleur n'est pas précisément celui que j'ai posté, la différence
étant que foo et bar étaient static dans mon code. Ce qui semble une
explication plausible à la différence observée. (Encore que le compilo
pourrait peut-être décider d'inliner certains appels à ces fonctions
*et* d'en garder une version non inlinée pour lier avec le reste du
code, j'imagine.)



Oui, totalement.
D'ailleurs c'est pour ca qu'on n'a pas de fonctions static dans le
noyau d'OpenBSD: le compilo pourrait les optimiser violemment, et on
prefere avoir un truc un peu moins optimal, mais plus simple a debugguer
en cas de panic().