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

container_of et strict-aliasing

2 réponses
Avatar
Pierre Habouzit
Pour le code suivant:

#include <stddef.h>

#define container_of(obj, type_t, member) \
((type_t *)((char *)(obj) - offsetof(type_t, member)))

struct foo {
unsigned short a;
unsigned b;
};

unsigned *get_ptr(void *);

int foo(struct foo *f)
{
unsigned *p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;
return f->a;
}



Est-ce que le compilateur a le droit d'optimiser le "return f->a" en
"return 0" ?

Je dirais que §6.5.7 me le garantit via:

An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:
[…]
- an aggregate or union type that includes one of the
aforementioned types among its members (including, recursively,
a member of a subaggregate or contained union), or a character
type.

Du coup vu que 'struct foo' contient des unsigned short ce n'est pas
légal d'optimiser le load de f->a après le store dans
container_of(...)->a vu qu'ils pourraient s'aliaser.


j'ai bon ?


D'ailleurs pour faire en sorte que gcc génère 'return 0' il faut ajouter
les restricts suivants:

int foo(struct foo *restrict f)
{
unsigned *restrict p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;
return f->a;
}

pushq %rbx
movq %rdi, %rbx
call get_ptr
movl $0, 4(%rbx)
movl $1, (%rax)
xorl %eax, %eax
popq %rbx
ret

J'avoue que j'ai du mal à rationaliser le pourquoi du comment par
contre. Je m'attendais à ce que ce code retourne 0 aussi, mais ce n'est
pas le cas:

int foo(struct foo *restrict f)
{
unsigned *p = get_ptr(f);

f->a = 0;
((struct foo * restrict)container_of(p, struct foo, b))->a = 1;
return f->a;
}

pushq %rbx
movq %rdi, %rbx
call get_ptr
movw $0, (%rbx)
movw $1, -4(%rax)
movzwl (%rbx), %eax
popq %rbx
ret


========================================================================

La question prend tout son sens lorsque au lieu de struct foo on utilise
les listes du kernel linux…

struct list_head {
struct list_head *next, *prev;
};

struct my_linked_struct {
/* fields ... */
struct list_head link;
/* fields ... */
};

et qu'on se sert de container_of pour "remonter" à la "my_linked_struct"
et qu'on en modifie des membres quelconques.

--
·O· Pierre Habouzit
··O madcoder@debian.org
OOO http://www.madism.org

2 réponses

Avatar
Antoine Leca
Pierre Habouzit écrivit :
Pour le code suivant:

#include <stddef.h>
#define container_of(obj, type_t, member)
((type_t *)((char *)(obj) - offsetof(type_t, member)))

struct foo {
unsigned short a;
unsigned b;
};

unsigned *get_ptr(void *);

int foo(struct foo *f)
{
unsigned *p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;



Une façon tarabiscotée d'écrire

fonction_pouvant_retouner_f()->a = 1;

return f->a;
}

Est-ce que le compilateur a le droit d'optimiser le "return f->a" en
"return 0" ?



Je dirais non, dans les conditions ci-dessus qui font que l'on ne peut
pas faire d'hypothèse sur la fonction externe get_ptr(), et les
éventuelles relations entre f et p.


Je dirais que §6.5.7 me le garantit via:

An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:
[…]
- an aggregate or union type that includes one of the
aforementioned types among its members (including, recursively,
a member of a subaggregate or contained union), or a character
type.



Euh.... f->a est donc bien un objet qui peut être accédé, et même
modifié. J'ai peur de ne pas suivre ton raisonnement, je lis plutôt la
garantie du contraire...


D'ailleurs pour faire en sorte que gcc génère 'return 0' il faut ajouter
les restricts suivants:

int foo(struct foo *restrict f)
{
unsigned *restrict p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;
return f->a;
}



Là c'est différent, les restrictions spécifient (au moins dans l'esprit)
que p ne peut pas valoir &(f->b), et donc que container_of() ne peut pas
valoir f. Ce qui autorise l'optimisation.


J'avoue que j'ai du mal à rationaliser le pourquoi du comment par
contre. Je m'attendais à ce que ce code retourne 0 aussi, mais ce n'est
pas le cas:

int foo(struct foo *restrict f)
{
unsigned *p = get_ptr(f);

f->a = 0;
((struct foo * restrict)container_of(p, struct foo, b))->a = 1;
return f->a;
}



Rappel : restrict n'est jamais qu'une indication, pas une instruction
pour forcer le compilateur à faire des optimisations douteuses.

Si la fonction ne retourne pas 0, c'est :
- soit que le compilateur génère du code faux (j'ai pas lu l'assembleur
mais je suppose que ce n'est pas le cas)
- soit que le processeur est bogué ou vicié d'une manière ou d'une
autre (genre mettre un point d'arrêt et forcer f->a à valoir 42)
- soit que le résultat est 1 et que la fonction get_ptr() est du genre
void * get_ptr(struct foo * f) { return &(f->b); }
(en violation flagrante des intentions indiquées par restrict).


Maintenant, si ta question est, « en supposant que get_ptr soit la
fonction ci-dessus et que donc je mentes au compilateur, pourquoi est-ce
qu'il fait l'optimisation (incorrecte) dans le premier cas et pas dans
le second ? », la réponse est probablement parce que dans le premier cas
il y a deux objets restreints (f et p), et que dans le second il n'y en
plus qu'un, f : le qualificatif ajouté par le transtypage ne créant pas
d'objet nouveau.
(Et chapeau au passage aux programmeurs du dit compilateur qui ont su
faire le distinguo.)


Antoine
Avatar
Pierre Habouzit
On 2011-05-26, Antoine Leca wrote:
Pierre Habouzit écrivit :
Pour le code suivant:

#include <stddef.h>
#define container_of(obj, type_t, member)
((type_t *)((char *)(obj) - offsetof(type_t, member)))

struct foo {
unsigned short a;
unsigned b;
};

unsigned *get_ptr(void *);

int foo(struct foo *f)
{
unsigned *p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;



Une façon tarabiscotée d'écrire

fonction_pouvant_retouner_f()->a = 1;

return f->a;
}

Est-ce que le compilateur a le droit d'optimiser le "return f->a" en
"return 0" ?



Je dirais non, dans les conditions ci-dessus qui font que l'on ne peut
pas faire d'hypothèse sur la fonction externe get_ptr(), et les
éventuelles relations entre f et p.


Je dirais que §6.5.7 me le garantit via:

An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:
[…]
- an aggregate or union type that includes one of the
aforementioned types among its members (including, recursively,
a member of a subaggregate or contained union), or a character
type.



Euh.... f->a est donc bien un objet qui peut être accédé, et même
modifié. J'ai peur de ne pas suivre ton raisonnement, je lis plutôt la
garantie du contraire...



Oui pardon, je voulais dire "me garantit que l'optimisation est
impossible". Je ne me suis pas relu :)

D'ailleurs pour faire en sorte que gcc génère 'return 0' il faut ajouter
les restricts suivants:

int foo(struct foo *restrict f)
{
unsigned *restrict p = get_ptr(f);

f->a = 0;
container_of(p, struct foo, b)->a = 1;
return f->a;
}



Là c'est différent, les restrictions spécifient (au moins dans l'esprit)
que p ne peut pas valoir &(f->b), et donc que container_of() ne peut pas
valoir f. Ce qui autorise l'optimisation.



Ah oui anéfé, container_of() est une expression dérivée de 'p' qui est
restrict, 'f' aussi. facile.

J'avoue que j'ai du mal à rationaliser le pourquoi du comment par
contre. Je m'attendais à ce que ce code retourne 0 aussi, mais ce n'est
pas le cas:

int foo(struct foo *restrict f)
{
unsigned *p = get_ptr(f);

f->a = 0;
((struct foo * restrict)container_of(p, struct foo, b))->a = 1;
return f->a;
}



Rappel : restrict n'est jamais qu'une indication, pas une instruction
pour forcer le compilateur à faire des optimisations douteuses.



Je sais bien :)

Si la fonction ne retourne pas 0, c'est :
- soit que le compilateur génère du code faux (j'ai pas lu l'assembleur
mais je suppose que ce n'est pas le cas)



Non il recharche f->a et le retourne. De toute façon il a le choix
essentiellement entre ça et retourner 0 dans les options qui ont du
sens[0].

- soit que le processeur est bogué ou vicié d'une manière ou d'une
autre (genre mettre un point d'arrêt et forcer f->a à valoir 42)



cf mon [0] oui :)

Maintenant, si ta question est, « en supposant que get_ptr soit la
fonction ci-dessus et que donc je mentes au compilateur, pourquoi est-ce
qu'il fait l'optimisation (incorrecte) dans le premier cas et pas dans
le second ? »,



Dans le premier cas elle n'est pas incorrecte au vu de mes annotations!
Ce sont les annotations qui le sont éventuellement.

la réponse est probablement parce que dans le premier cas il y a deux
objets restreints (f et p), et que dans le second il n'y en plus
qu'un, f : le qualificatif ajouté par le transtypage ne créant pas
d'objet nouveau.
(Et chapeau au passage aux programmeurs du dit compilateur qui ont su
faire le distinguo.)



Il serait plus logique en fait après réflexion que gcc s'apercoit que
je dis que une expression dérivée d'un pointeur restrict (p) est
restrict, ce qui est invalide. Et que gcc décide dans le doute d'ignorer
les attributs en question sur p et cette expression :)

Mais -Wstrict-aliasing=2 n'a rien montré malheureusement.


[0] Bien sur vu que ce que je fais est invalide (j'assigne un pointeur
restrict dans un autre qui s'aliasent ce qui est interdit!) il
*aurait le droit* de compiler ça en return system("rm -rf /")… mais
on va dire que les devs de GCC sont rationnels (quoi que…)
--
·O· Pierre Habouzit
··O
OOO http://www.madism.org