C Language
Type d'aliasing et efficace
Recherche…
Remarques
Les violations des règles d'alias et la violation du type effectif d'un objet sont deux choses différentes et ne doivent pas être confondues.
L'aliasing est la propriété de deux pointeurs
a
etb
qui se réfèrent au même objet, à savoir quea == b
.Le type effectif d'un objet de données est utilisé par C pour déterminer quelles opérations peuvent être effectuées sur cet objet. En particulier, le type effectif est utilisé pour déterminer si deux pointeurs peuvent s’appeler mutuellement.
La création d'alias peut être un problème pour l'optimisation, car la modification de l'objet par le biais d'un pointeur, a
exemple, peut modifier l'objet visible via l'autre pointeur, b
. Si votre compilateur C devait supposer que les pointeurs pouvaient toujours s’allier, quel que soit leur type et leur provenance, de nombreuses possibilités d’optimisation seraient perdues et de nombreux programmes seraient plus lents.
Les règles d'aliasing strictes de C font référence aux cas dans lesquels le compilateur peut supposer quels objets font (ou non) des pseudonymes. Il y a deux règles à suivre pour les pointeurs de données.
Sauf indication contraire, deux pointeurs avec le même type de base peuvent être alias.
Deux pointeurs avec un type de base différent ne peuvent pas créer d'alias, à moins qu'au moins l'un des deux types soit un type de caractère.
Voici le type de base signifie que nous mettons de côté des qualifications de type tels que const
, par exemple , si a
est double*
et b
est const double*
, le compilateur doit généralement présumer qu'un changement de *a
peut changer *b
.
Violer la deuxième règle peut avoir des résultats catastrophiques. Ici, violer la règle d'aliasing strict signifie que vous présentez deux pointeurs a
et b
de type différent au compilateur qui, en réalité, pointe vers le même objet. Le compilateur peut alors toujours supposer que les deux pointent vers des objets différents, et ne mettra pas à jour son idée de *b
si vous avez modifié l'objet via *a
.
Si vous le faites, le comportement de votre programme devient indéfini. Par conséquent, C impose des restrictions assez sévères sur les conversions de pointeurs afin de vous aider à éviter une telle situation.
À moins que le type de source ou de cible ne soit
void
, toutes les conversions de pointeurs entre pointeurs de type base différent doivent être explicites .
Ou en d' autres termes, ils ont besoin d' un casting, à moins que vous faites une conversion qui ajoute juste un qualificatif tel que const
le type de cible.
Éviter les conversions de pointeurs en général et les conversions en particulier vous protège contre les problèmes d'alias. À moins que vous en ayez vraiment besoin et que ces cas soient très particuliers, vous devriez les éviter comme vous le pouvez.
Les types de caractères ne sont pas accessibles via des types autres que des caractères.
Si un objet est défini avec une durée statique, de thread ou de stockage automatique et qu'il a un type de caractère, soit: char
, unsigned char
, ou signed char
, il ne sera pas accessible par un type non-caractère. Dans l'exemple ci - dessous un char
tableau est réinterprété comme le type int
, et le comportement est indéfini sur chaque déréférencement du int
pointeur b
.
int main( void )
{
char a[100];
int* b = ( int* )&a;
*b = 1;
static char c[100];
b = ( int* )&c;
*b = 2;
_Thread_local char d[100];
b = ( int* )&d;
*b = 3;
}
Ceci n'est pas défini car il viole la règle du "type effectif", aucun objet de données ayant un type efficace ne peut être accédé via un autre type qui n'est pas un type de caractère. Étant donné que l'autre type ici est int
, ce n'est pas autorisé.
Même si l'alignement et la taille des pointeurs étaient connus pour s'adapter, cela ne dispenserait pas de cette règle, le comportement serait toujours indéfini.
Cela signifie en particulier qu’il n’existe aucun moyen de réserver un objet tampon de type caractère pouvant être utilisé avec des pointeurs de différents types, car vous utiliseriez un tampon reçu par malloc
ou une fonction similaire.
Une manière correcte d'atteindre le même objectif que dans l'exemple ci-dessus serait d'utiliser une union
.
typedef union bufType bufType;
union bufType {
char c[sizeof(int[25])];
int i[25];
};
int main( void )
{
bufType a = { .c = { 0 } }; // reserve a buffer and initialize
int* b = a.i; // no cast necessary
*b = 1;
static bufType a = { .c = { 0 } };
int* b = a.i;
*b = 2;
_Thread_local bufType a = { .c = { 0 } };
int* b = a.i;
*b = 3;
}
Dans ce cas, l' union
garantit que le compilateur sait dès le départ que le tampon peut être accédé via différentes vues. Cela a également l'avantage que le tampon a maintenant une "vue" ai
qui est déjà de type int
et qu'aucune conversion de pointeur n'est nécessaire.
Type efficace
Le type effectif d'un objet de données est la dernière information de type qui lui était associée, le cas échéant.
// a normal variable, effective type uint32_t, and this type never changes
uint32_t a = 0.0;
// effective type of *pa is uint32_t, too, simply
// because *pa is the object a
uint32_t* pa = &a;
// the object pointed to by q has no effective type, yet
void* q = malloc(sizeof uint32_t);
// the object pointed to by q still has no effective type,
// because nobody has written to it
uint32_t* qb = q;
// *qb now has effective type uint32_t because a uint32_t value was written
*qb = 37;
// the object pointed to by r has no effective type, yet, although
// it is initialized
void* r = calloc(1, sizeof uint32_t);
// the object pointed to by r still has no effective type,
// because nobody has written to or read from it
uint32_t* rc = r;
// *rc now has effective type uint32_t because a value is read
// from it with that type. The read operation is valid because we used calloc.
// Now the object pointed to by r (which is the same as *rc) has
// gained an effective type, although we didn't change its value.
uint32_t c = *rc;
// the object pointed to by s has no effective type, yet.
void* s = malloc(sizeof uint32_t);
// the object pointed to by s now has effective type uint32_t
// because an uint32_t value is copied into it.
memcpy(s, r, sizeof uint32_t);
Observez que pour ce dernier, il n'était pas nécessaire que nous ayons même un pointeur uint32_t*
vers cet objet. Le fait que nous ayons copié un autre objet uint32_t
est suffisant.
Violer les règles strictes d'aliasing
Dans le code suivant, supposons pour simplifier que float
et uint32_t
aient la même taille.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
et f
ont un type de base différent, et le compilateur peut donc supposer qu'ils pointent vers des objets différents. Il n’ya pas de possibilité que *f
ait changé entre les deux initialisations de a
et b
, et le compilateur peut donc optimiser le code en quelque chose d’équivalent à
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
C'est-à-dire que la seconde opération de chargement de *f
peut être complètement optimisée.
Si nous appelons cette fonction "normalement"
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
tout va bien et quelque chose comme
4 devrait être égal à 4
est imprimé. Mais si nous trichons et passons le même pointeur, après la conversion,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
nous violons la règle stricte d'aliasing. Le comportement devient alors indéfini. La sortie pourrait être comme ci-dessus, si le compilateur avait optimisé le second accès, ou quelque chose de complètement différent, et que votre programme se retrouverait dans un état totalement non fiable.
restreindre la qualification
Si nous avons deux arguments de pointeur du même type, le compilateur ne peut faire aucune supposition et devra toujours supposer que la modification de *e
peut changer *f
:
void fun(float* e, float* f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
float fval = 4;
float eval = 77;
fun(&eval, &fval);
tout va bien et quelque chose comme
est 4 égal à 4?
est imprimé. Si nous passons le même pointeur, le programme fera quand même le bon choix et imprimera
est 4 égal à 22?
Cela peut s'avérer inefficace si nous connaissons par des informations externes que e
et f
ne pointeront jamais vers le même objet de données. Nous pouvons refléter cette connaissance en ajoutant restrict
qualificateurs de restrict
aux paramètres du pointeur:
void fan(float*restrict e, float*restrict f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
Le compilateur peut alors toujours supposer que e
et f
pointent vers des objets différents.
Changer d'octets
Une fois qu'un objet a un type efficace, vous ne devriez pas tenter de le modifier par un pointeur d'un autre type, à moins que cet autre type est un type de caractère, char
, signed char
ou unsigned char
.
#include <inttypes.h>
#include <stdio.h>
int main(void) {
uint32_t a = 57;
// conversion from incompatible types needs a cast !
unsigned char* ap = (unsigned char*)&a;
for (size_t i = 0; i < sizeof a; ++i) {
/* set each byte of a to 42 */
ap[i] = 42;
}
printf("a now has value %" PRIu32 "\n", a);
}
Ceci est un programme valide qui imprime
a maintenant une valeur 707406378
Cela fonctionne parce que:
- L'accès est fait aux octets individuels vus avec le type
unsigned char
afin que chaque modification soit bien définie. - Les deux vues de l'objet, à travers
a
alias a et through*ap
, maisap
étant un pointeur sur un type de caractère, la règle d'aliasing stricte ne s'applique pas. Le compilateur doit donc supposer que la valeur dea
peut avoir été modifiée dans la bouclefor
. La valeur modifiée dea
doit être construite à partir des octets modifiés. - Le type de
a
,uint32_t
n'a pas de bits de remplissage. Tous ses bits de la représentation comptent pour la valeur, ici707406378
, et il ne peut y avoir aucune représentation de piège.