C Language
Aliasing und effektiver Typ
Suche…
Bemerkungen
Verstöße gegen Aliasing-Regeln und gegen den effektiven Typ eines Objekts sind zwei verschiedene Dinge und sollten nicht verwechselt werden.
Aliasing ist die Eigenschaft von zwei Zeigern
a
undb
, die auf dasselbe Objekt verweisen, d.a == b
.Der effektive Typ eines Datenobjekts wird von C verwendet, um zu bestimmen, welche Operationen an diesem Objekt ausgeführt werden können. Insbesondere wird der effektive Typ verwendet, um zu bestimmen, ob zwei Zeiger sich gegenseitig aliasieren können.
Aliasing kann ein Problem für die Optimierung, weil das Objekt durch einen Zeiger zu ändern, a
sagen wir, kann das Objekt ändern , die durch den anderen Zeiger sichtbar ist b
. Wenn Ihr C-Compiler davon ausgehen muss, dass Zeiger sich unabhängig von ihrem Typ und ihrer Herkunft immer gegenseitig Aliasnamen geben, gehen viele Optimierungsmöglichkeiten verloren und viele Programme laufen langsamer.
Die strengen Aliasing-Regeln von C beziehen sich auf Fälle, in denen der Compiler annehmen kann, welche Objekte sich gegenseitig aliasieren (oder nicht). Es gibt zwei Daumenregeln, die Sie für Datenzeiger immer beachten sollten.
Wenn nicht anders angegeben, können zwei Zeiger mit demselben Basistyp einen Alias haben.
Zwei Zeiger mit unterschiedlichen Basistypen können keinen Aliasnamen verwenden, es sei denn, einer der beiden Typen ist ein Zeichentyp.
Basistyp bedeutet hier, dass wir Typqualifikationen wie const
beiseite stellen, z. B. Wenn a
double*
und b
const double*
, muss der Compiler im Allgemeinen annehmen, dass eine Änderung von *a
*b
ändern kann.
Die Verletzung der zweiten Regel kann katastrophale Folgen haben. Unter Verletzung der strengen Aliasing-Regel werden hier dem Compiler zwei Zeiger a
und b
unterschiedlichen Typs präsentiert, die in Wirklichkeit auf dasselbe Objekt zeigen. Der Compiler kann dann immer davon ausgehen, dass die beiden auf unterschiedliche Objekte verweisen, und wird seine Vorstellung von *b
nicht aktualisieren, wenn Sie das Objekt durch *a
geändert haben.
Wenn Sie dies tun, wird das Verhalten Ihres Programms undefiniert. Daher setzt C ziemlich strikte Einschränkungen für die Zeigerumwandlung, um zu vermeiden, dass eine solche Situation versehentlich auftritt.
Wenn der Quell- oder Zieltyp nicht
void
, müssen alle Zeigerumwandlungen zwischen Zeigern mit unterschiedlichem Basistyp explizit sein .
Oder anders ausgedrückt, sie benötigen einen Cast , es sei denn, Sie führen eine Konvertierung durch, die dem Zieltyp lediglich ein Qualifikationsmerkmal wie const
hinzufügt.
Das Vermeiden von Zeigerkonvertierungen im Allgemeinen und von Casts im Besonderen schützt Sie vor Aliasing-Problemen. Wenn Sie sie nicht wirklich brauchen und diese Fälle sehr speziell sind, sollten Sie sie nach Möglichkeit vermeiden.
Auf Zeichentypen kann nicht über Nicht-Zeichentypen zugegriffen werden.
Wenn ein Objekt mit einer statischen, Thread- oder automatischen Speicherdauer definiert ist und einen Zeichentyp hat, entweder: char
, unsigned char
oder signed char
, kann nicht auf einen Nicht-Zeichentyp zugegriffen werden. Im folgenden Beispiel wird ein char
Array als Typ int
neu interpretiert, und das Verhalten ist bei jeder Dereferenzierung des int
Zeigers b
undefiniert.
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;
}
Dies ist undefiniert, da es gegen die Regel des "effektiven Typs" verstößt. Auf ein Datenobjekt mit einem effektiven Typ darf nicht über einen anderen Typ, der kein Zeichentyp ist, zugegriffen werden. Da der andere Typ hier int
, ist dies nicht zulässig.
Selbst wenn bekannt wäre, dass Ausrichtungs- und Zeigergrößen passen würden, wäre dies nicht von dieser Regel ausgenommen, das Verhalten wäre immer noch undefiniert.
Dies bedeutet insbesondere, dass es in Standard C nicht möglich ist, ein Pufferobjekt vom Zeichentyp zu reservieren, das durch Zeiger mit unterschiedlichen Typen verwendet werden kann, wie Sie einen Puffer verwenden würden, der von malloc
oder einer ähnlichen Funktion empfangen wurde.
Ein korrekter Weg, um das gleiche Ziel wie im obigen Beispiel zu erreichen, wäre die Verwendung einer 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;
}
Hier sorgt die union
dafür, dass der Compiler von Anfang an weiß, dass auf den Puffer über verschiedene Ansichten zugegriffen werden kann. Dies hat auch den Vorteil , dass nun der Puffer eine „Ansicht“ hat ai
, dass bereits vom Typ int
und keine Zeiger Konvertierung erforderlich ist .
Effektiver Typ
Der effektive Typ eines Datenobjekts ist, falls vorhanden, die letzte Typinformation, die ihm zugeordnet wurde.
// 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);
Beachten Sie, dass für letzteres nicht einmal ein uint32_t*
-Zeiger auf dieses Objekt vorhanden war. Die Tatsache, dass wir ein anderes uint32_t
Objekt kopiert haben, ist ausreichend.
Verletzung der strengen Aliasing-Regeln
Nehmen wir im folgenden Code an, dass float
und uint32_t
die gleiche Größe haben.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
und f
haben unterschiedliche Basistypen, und der Compiler kann daher davon ausgehen, dass sie auf verschiedene Objekte zeigen. Es gibt keine Möglichkeit, dass sich *f
zwischen den beiden Initialisierungen von a
und b
geändert haben könnte. Der Compiler kann daher den Code auf etwas Äquivalent optimieren
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
Das heißt, die zweite Ladeoperation von *f
kann vollständig optimiert werden.
Wenn wir diese Funktion "normal" nennen
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
alles geht gut und sowas
4 sollte gleich 4 sein
wird gedruckt. Aber wenn wir betrügen und den gleichen Zeiger übergeben, nachdem wir ihn konvertiert haben,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
wir verstoßen gegen die strikte Aliasing-Regel. Dann wird das Verhalten undefiniert. Die Ausgabe könnte wie oben sein, wenn der Compiler den zweiten Zugriff optimiert hat, oder etwas völlig anderes, so dass Ihr Programm in einem völlig unzuverlässigen Zustand landet.
Qualifikation einschränken
Wenn wir zwei Zeigerargumente des gleichen Typs haben, kann der Compiler keine Annahme machen , und wird immer müssen davon ausgehen , dass die Änderung *e
kann sich ändern *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);
alles geht gut und sowas
ist 4 gleich 4?
wird gedruckt. Wenn wir denselben Zeiger übergeben, macht das Programm immer noch das Richtige und druckt
ist 4 gleich 22?
Dies kann sich als ineffizient erweisen, wenn wir durch einige externe Informationen wissen, dass e
und f
niemals auf dasselbe Datenobjekt zeigen. Wir können dieses Wissen widerspiegeln, indem Sie den Zeigerparametern restrict
hinzufügen:
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);
}
Dann kann der Compiler immer annehmen, dass e
und f
auf verschiedene Objekte zeigen.
Bytes ändern
Sobald ein Objekt einen effektiven Typ hat, sollten Sie nicht versuchen, es über einen Zeiger eines anderen Typs zu ändern, es sei denn, dieser andere Typ ist ein Zeichentyp, char
, signed char
oder 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);
}
Dies ist ein gültiges Programm, das druckt
a hat jetzt den Wert 707406378
Das funktioniert aus folgenden Gründen:
- Der Zugriff erfolgt auf die einzelnen Bytes, die vom Typ
unsigned char
sodass jede Änderung gut definiert ist. - Die beiden Ansichten des Objekts durch
a
und durch*ap
, ein Alias, aber daap
ein Zeiger auf einen Zeichentyp ist, gilt die strikte Aliasing-Regel nicht. Der Compiler muss also davon ausgehen, dass der Wert vona
in derfor
Schleife geändert wurde. Der geänderte Wert vona
muss aus den Bytes erstellt werden, die geändert wurden. - Der Typ von
a
,uint32_t
enthält keine Füllbits. Alle seine Bits der Repräsentation zählen für den Wert, hier707406378
, und es kann keine Trap-Darstellung geben.