Wenn nach häufigem undefiniertem Verhalten in C gefragt wird , wird manchmal auf die strenge Aliasing-Regel verwiesen.
Worüber reden sie?
Wenn nach häufigem undefiniertem Verhalten in C gefragt wird , wird manchmal auf die strenge Aliasing-Regel verwiesen.
Worüber reden sie?
Antworten:
Eine typische Situation, in der Sie auf strenge Aliasing-Probleme stoßen, besteht darin, eine Struktur (wie eine Geräte- / Netzwerknachricht) auf einen Puffer mit der Wortgröße Ihres Systems (wie einen Zeiger auf uint32_t
s oder uint16_t
s) zu legen . Wenn Sie eine Struktur auf einen solchen Puffer oder einen Puffer auf eine solche Struktur durch Zeigerumwandlung überlagern, können Sie leicht gegen strenge Aliasing-Regeln verstoßen.
Wenn ich in dieser Art von Setup eine Nachricht an etwas senden möchte, muss ich zwei inkompatible Zeiger haben, die auf denselben Speicherblock zeigen. Ich könnte dann naiv so etwas codieren (auf einem System mit sizeof(int) == 2
):
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
Die strikte Aliasing-Regel macht dieses Setup unzulässig: Das Dereferenzieren eines Zeigers, der ein Objekt aliasisiert, das nicht von einem kompatiblen Typ oder einem der anderen in C 2011 6.5 Absatz 7 1 zugelassenen Typen ist, ist ein undefiniertes Verhalten. Leider können Sie immer noch auf diese Weise codieren, möglicherweise einige Warnungen erhalten und es gut kompilieren lassen, nur um ein seltsames unerwartetes Verhalten zu haben, wenn Sie den Code ausführen.
(GCC scheint in seiner Fähigkeit, Aliasing-Warnungen zu geben, etwas inkonsistent zu sein, manchmal gibt es eine freundliche Warnung und manchmal nicht.)
Um zu sehen, warum dieses Verhalten undefiniert ist, müssen wir uns überlegen, was die strenge Aliasing-Regel dem Compiler kauft. Grundsätzlich muss bei dieser Regel nicht über das Einfügen von Anweisungen nachgedacht werden, um den Inhalt buff
jedes Laufs der Schleife zu aktualisieren. Stattdessen können bei der Optimierung mit einigen ärgerlich nicht erzwungenen Annahmen zum Aliasing diese Anweisungen weggelassen, geladen buff[0]
und buff[1
] einmal in CPU-Register geladen werden, bevor die Schleife ausgeführt wird, und der Hauptteil der Schleife beschleunigt werden. Bevor ein striktes Aliasing eingeführt wurde, musste der Compiler in einem Zustand der Paranoia leben, in dem sich der Inhalt buff
von jedem jederzeit und von jedem ändern kann. Um einen zusätzlichen Leistungsvorteil zu erzielen und davon auszugehen, dass die meisten Benutzer keine Wortspielzeiger eingeben, wurde die strenge Aliasing-Regel eingeführt.
Denken Sie daran, wenn Sie glauben, dass das Beispiel erfunden wurde, kann dies sogar passieren, wenn Sie einen Puffer an eine andere Funktion übergeben, die das Senden für Sie ausführt, falls Sie dies getan haben.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
Und haben Sie unsere frühere Schleife neu geschrieben, um diese praktische Funktion zu nutzen
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
Der Compiler ist möglicherweise nicht in der Lage oder nicht intelligent genug, um zu versuchen, SendMessage zu inline, und kann sich entscheiden, den Buff erneut zu laden oder nicht. Wenn SendMessage
es Teil einer anderen API ist, die separat kompiliert wurde, enthält es wahrscheinlich Anweisungen zum Laden des Buff-Inhalts. Andererseits sind Sie vielleicht in C ++ und dies ist eine Implementierung nur für Header mit Vorlagen, die der Compiler für inline hält. Oder vielleicht ist es nur etwas, das Sie zu Ihrer eigenen Bequemlichkeit in Ihre .c-Datei geschrieben haben. Auf jeden Fall kann es dennoch zu undefiniertem Verhalten kommen. Selbst wenn wir wissen, was unter der Haube passiert, ist dies immer noch ein Verstoß gegen die Regel, sodass kein genau definiertes Verhalten garantiert ist. Es hilft also nicht unbedingt, nur eine Funktion einzuschließen, die unseren wortgetrennten Puffer verwendet.
Wie komme ich darum herum?
Verwenden Sie eine Gewerkschaft. Die meisten Compiler unterstützen dies, ohne sich über striktes Aliasing zu beschweren. Dies ist in C99 zulässig und in C11 ausdrücklich zulässig.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Sie können das strikte Aliasing in Ihrem Compiler deaktivieren ( f [no-] striktes Aliasing in gcc))
Sie können char*
Aliasing anstelle des Wortes Ihres Systems verwenden. Die Regeln erlauben eine Ausnahme für char*
(einschließlich signed char
und unsigned char
). Es wird immer angenommen, dass char*
Aliase andere Typen sind. Dies funktioniert jedoch nicht umgekehrt: Es wird nicht davon ausgegangen, dass Ihre Struktur einen Zeichenpuffer aliasisiert.
Anfänger aufgepasst
Dies ist nur ein potenzielles Minenfeld, wenn zwei Typen übereinander gelegt werden. Sie sollten auch darüber erfahren , endianness , Wortausrichtung und wie mit Ausrichtungsprobleme durch behandeln Verpackung structs richtig.
1 Die Typen, auf die C 2011 6.5 7 einen l-Wert zulässt, sind:
unsigned char*
weit char*
verwendet werden? Ich neige dazu, unsigned char
eher als char
den zugrunde liegenden Typ zu verwenden, byte
weil meine Bytes nicht signiert sind und ich nicht die Verrücktheit des signierten Verhaltens (insbesondere zum Überlaufen) möchte
unsigned char *
in Ordnung.
uint32_t* buff = malloc(sizeof(Msg));
und die nachfolgenden Union unsigned int asBuffer[sizeof(Msg)];
Buffer-Deklarationen haben unterschiedliche Größen und keine ist korrekt. Der malloc
Anruf basiert auf der 4-Byte-Ausrichtung unter der Haube (tun Sie es nicht) und die Vereinigung wird viermal größer sein, als es sein muss ... Ich verstehe, dass es der Klarheit halber ist, aber es nervt mich trotzdem. weniger ...
Die beste Erklärung, die ich gefunden habe, ist von Mike Acton, Understanding Strict Aliasing . Es konzentriert sich ein wenig auf die PS3-Entwicklung, aber das ist im Grunde nur GCC.
Aus dem Artikel:
"Striktes Aliasing ist eine Annahme des C- (oder C ++) Compilers, dass die Dereferenzierung von Zeigern auf Objekte unterschiedlichen Typs niemals auf denselben Speicherort verweist (dh auf einander Alias)."
Wenn Sie also int*
auf einen Speicher zeigen, der ein enthält, int
und dann float*
auf diesen Speicher zeigen und ihn verwenden, float
brechen Sie die Regel. Wenn Ihr Code dies nicht berücksichtigt, wird der Optimierer des Compilers höchstwahrscheinlich Ihren Code beschädigen.
Die Ausnahme von der Regel ist a char*
, die auf einen beliebigen Typ verweisen darf.
Dies ist die strikte Aliasing-Regel in Abschnitt 3.10 des C ++ 03- Standards (andere Antworten liefern eine gute Erklärung, aber keine liefert die Regel selbst):
Wenn ein Programm versucht, über einen anderen Wert als einen der folgenden Typen auf den gespeicherten Wert eines Objekts zuzugreifen, ist das Verhalten undefiniert:
- der dynamische Typ des Objekts,
- eine cv-qualifizierte Version des dynamischen Typs des Objekts,
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem dynamischen Typ des Objekts entspricht;
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer lebenslaufqualifizierten Version des dynamischen Typs des Objekts entspricht;
- ein Aggregat- oder Gewerkschaftstyp, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Gewerkschaft);
- ein Typ, der ein (möglicherweise lebenslaufqualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
- a
char
oderunsigned char
Typ.
Wortlaut von C ++ 11 und C ++ 14 (Änderungen hervorgehoben):
Wenn ein Programm versucht, über einen anderen Wert als einen der folgenden Typen auf den gespeicherten Wert eines Objekts zuzugreifen, ist das Verhalten undefiniert:
- der dynamische Typ des Objekts,
- eine cv-qualifizierte Version des dynamischen Typs des Objekts,
- ein Typ, der dem dynamischen Typ des Objekts ähnlich ist (wie in 4.4 definiert),
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem dynamischen Typ des Objekts entspricht;
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer lebenslaufqualifizierten Version des dynamischen Typs des Objekts entspricht;
- ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Elementen oder nicht statischen Datenelementen enthält (einschließlich rekursiv eines Elements oder nicht statischen Datenelements eines Unteraggregats oder einer enthaltenen Vereinigung);
- ein Typ, der ein (möglicherweise lebenslaufqualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
- a
char
oderunsigned char
Typ.
Zwei Änderungen waren gering: glWert statt lWert und Klärung des Aggregat- / Vereinigungsfalls.
Die dritte Änderung bietet eine stärkere Garantie (lockert die Regel für starkes Aliasing): Das neue Konzept ähnlicher Typen , die jetzt für Alias sicher sind.
Auch der C- Wortlaut (C99; ISO / IEC 9899: 1999 6.5 / 7; genau der gleiche Wortlaut wird in ISO / IEC 9899: 2011 §6.5 ¶7 verwendet):
Auf einen gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen 73) oder 88) hat :
- ein Typ, der mit dem effektiven Typ des Objekts kompatibel ist,
- eine qualifizierte Version eines Typs, der mit dem effektiven Typ des Objekts kompatibel ist;
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem effektiven Typ des Objekts entspricht;
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht;
- ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Vereinigung), oder
- ein Zeichentyp.
73) oder 88) Mit dieser Liste sollen die Umstände angegeben werden, unter denen ein Objekt möglicherweise einen Alias aufweist oder nicht.
wow(&u->s1,&u->s2)
wäre, müsste er auch dann legal sein, wenn ein Zeiger zum Ändern verwendet wird u
, und dies würde die meisten Optimierungen negieren, die der Die Aliasing-Regel wurde entwickelt, um die Arbeit zu erleichtern.
Dies ist ein Auszug aus meinem "Was ist die strikte Aliasing-Regel und warum interessiert es uns?" Aufschreiben.
In C und C ++ hat Aliasing damit zu tun, über welche Ausdruckstypen wir auf gespeicherte Werte zugreifen dürfen. Sowohl in C als auch in C ++ gibt der Standard an, welche Ausdruckstypen welche Typen aliasisieren dürfen. Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln strikt befolgen, daher der Begriff strikte Aliasing-Regel . Wenn wir versuchen, mit einem nicht zulässigen Typ auf einen Wert zuzugreifen, wird dieser als undefiniertes Verhalten ( UB ) klassifiziert . Sobald wir ein undefiniertes Verhalten haben, sind alle Wetten ungültig, und die Ergebnisse unseres Programms sind nicht mehr zuverlässig.
Leider erhalten wir bei strengen Aliasing-Verstößen häufig die erwarteten Ergebnisse, sodass die Möglichkeit besteht, dass eine zukünftige Version eines Compilers mit einer neuen Optimierung den Code beschädigt, den wir für gültig hielten. Dies ist unerwünscht und es ist ein lohnendes Ziel, die strengen Aliasing-Regeln zu verstehen und zu vermeiden, dass sie verletzt werden.
Um mehr darüber zu verstehen, warum es uns wichtig ist, werden wir Probleme diskutieren, die bei Verstößen gegen strenge Aliasing-Regeln auftreten.
Schauen wir uns einige Beispiele an, dann können wir genau darüber sprechen, was die Standards sagen, einige weitere Beispiele untersuchen und dann herausfinden, wie striktes Aliasing vermieden und Verstöße abgefangen werden können, die wir verpasst haben. Hier ist ein Beispiel, das nicht überraschen sollte ( Live-Beispiel ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Wir haben ein int *, das auf den von einem int belegten Speicher verweist, und dies ist ein gültiges Aliasing. Der Optimierer muss davon ausgehen, dass Zuweisungen über IP den von x belegten Wert aktualisieren können .
Das nächste Beispiel zeigt Aliasing, das zu undefiniertem Verhalten führt ( Live-Beispiel ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
In der Funktion foo nehmen wir ein int * und ein float * , in diesem Beispiel rufen wir foo auf und setzen beide Parameter so, dass sie auf denselben Speicherort zeigen, der in diesem Beispiel ein int enthält . Beachten Sie, dass der reinterpret_cast den Compiler anweist, den Ausdruck so zu behandeln, als hätte er den durch seinen Vorlagenparameter angegebenen Typ. In diesem Fall weisen wir ihn an, den Ausdruck & x so zu behandeln, als hätte er den Typ float * . Wir können naiv das Ergebnis der zweiten erwarten cout sein 0 , aber mit der Optimierung aktiviert mit -O2 sowohl gcc und Klappern erzeugen folgendes Ergebnis:
0
1
Was nicht zu erwarten ist, aber vollkommen gültig ist, da wir undefiniertes Verhalten aufgerufen haben. Ein Float kann ein int- Objekt nicht gültig aliasen . Daher kann der Optimierer annehmen, dass die Konstante 1, die beim Dereferenzieren von i gespeichert wird, der Rückgabewert ist, da ein Speicher über f ein int- Objekt nicht gültig beeinflussen kann. Das Einstecken des Codes in den Compiler Explorer zeigt, dass genau dies geschieht ( Live-Beispiel ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Der Optimierer, der die typbasierte Alias-Analyse (TBAA) verwendet, geht davon aus, dass 1 zurückgegeben wird, und verschiebt den konstanten Wert direkt in das Register eax, das den Rückgabewert enthält. TBAA verwendet die Sprachregeln für die Alias-Typen, um das Laden und Speichern zu optimieren. In diesem Fall weiß TBAA, dass ein Float nicht alias und int kann und optimiert die Last von i .
Was genau sagt der Standard, dass wir dürfen und was nicht? Die Standardsprache ist nicht einfach, daher werde ich versuchen, für jedes Element Codebeispiele bereitzustellen, die die Bedeutung demonstrieren.
Der C11- Standard schreibt in Abschnitt 6.5 Ausdrücke Absatz 7 Folgendes vor :
Auf einen gespeicherten Wert eines Objekts darf nur über einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen aufweist: 88) - ein Typ, der mit dem effektiven Typ des Objekts kompatibel ist;
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- eine qualifizierte Version eines Typs, der mit dem effektiven Typ des Objekts kompatibel ist;
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem effektiven Typ des Objekts entspricht;
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang hat eine Erweiterung und ermöglicht auch das Zuweisen von int * ohne Vorzeichen zu int * , obwohl es sich nicht um kompatible Typen handelt.
- ein Typ, der der signierte oder nicht signierte Typ ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht;
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- ein Aggregat- oder Gewerkschaftstyp, der einen der oben genannten Typen unter seinen Mitgliedern umfasst (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Gewerkschaft), oder
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- ein Zeichentyp.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
Der C ++ 17-Standardentwurf in Abschnitt [basic.lval] Absatz 11 lautet:
Wenn ein Programm versucht, über einen anderen Wert als einen der folgenden Typen auf den gespeicherten Wert eines Objekts zuzugreifen, ist das Verhalten undefiniert: 63 (11.1) - der dynamische Typ des Objekts,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - eine lebenslaufqualifizierte Version des dynamischen Objekttyps,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - ein Typ, der dem dynamischen Typ des Objekts ähnlich ist (wie in 7.5 definiert),
(11.4) - Ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem dynamischen Typ des Objekts entspricht.
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer cv-qualifizierten Version des dynamischen Typs des Objekts entspricht;
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Elementen oder nicht statischen Datenelementen enthält (einschließlich rekursiv eines Elements oder eines nicht statischen Datenelements eines Unteraggregats oder einer enthaltenen Vereinigung);
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - Ein Typ, der ein (möglicherweise lebenslaufqualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist.
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - ein char-, unsigned char- oder std :: byte-Typ.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Bemerkenswert ist, dass signiertes Zeichen nicht in der obigen Liste enthalten ist. Dies ist ein bemerkenswerter Unterschied zu C, das einen Zeichentyp angibt .
Wir sind an diesem Punkt angelangt und fragen uns vielleicht, warum wir einen Alias haben wollen. Die Antwort ist normalerweise die Eingabe von Wortspiel . Oft verstoßen die verwendeten Methoden gegen strenge Aliasing-Regeln.
Manchmal wollen wir das Typsystem umgehen und ein Objekt als einen anderen Typ interpretieren. Dies wird als Typ-Punning bezeichnet , um ein Speichersegment als einen anderen Typ neu zu interpretieren. Typ Punning ist nützlich für Aufgaben, die Zugriff auf die zugrunde liegende Darstellung eines Objekts zum Anzeigen, Transportieren oder Bearbeiten benötigen . Typische Bereiche, in denen Typ-Punning verwendet wird, sind Compiler, Serialisierung, Netzwerkcode usw.
Traditionell wurde dies erreicht, indem die Adresse des Objekts in einen Zeiger des Typs umgewandelt wurde, als den wir es neu interpretieren möchten, und dann auf den Wert zugegriffen wurde, oder mit anderen Worten durch Aliasing. Zum Beispiel:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Wie wir bereits gesehen haben, ist dies kein gültiges Aliasing, daher rufen wir undefiniertes Verhalten auf. Aber traditionell nutzten Compiler strenge Aliasing-Regeln nicht und diese Art von Code funktionierte normalerweise nur. Entwickler haben sich leider daran gewöhnt, Dinge auf diese Weise zu tun. Eine übliche alternative Methode zum Typ-Punning sind Gewerkschaften, die in C gültig sind, in C ++ jedoch undefiniertes Verhalten ( siehe Live-Beispiel ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
Dies ist in C ++ nicht gültig und einige betrachten den Zweck von Gewerkschaften ausschließlich als Implementierung von Variantentypen und halten die Verwendung von Gewerkschaften für Typ-Punning für einen Missbrauch.
Die Standardmethode für Typ-Punning in C und C ++ ist memcpy . Dies mag ein wenig hartnäckig erscheinen, aber der Optimierer sollte die Verwendung von memcpy für das Typ-Punning erkennen und es wegoptimieren und ein Register generieren, um die Bewegung zu registrieren. Wenn wir beispielsweise wissen, dass int64_t dieselbe Größe wie double hat :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
wir können memcpy verwenden :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Bei einer ausreichenden Optimierungsstufe generiert jeder anständige moderne Compiler identischen Code wie die zuvor erwähnte reinterpret_cast- Methode oder Union- Methode für Typ-Punning . Wenn wir den generierten Code untersuchen, sehen wir, dass nur register mov verwendet wird ( Live Compiler Explorer-Beispiel ).
In C ++ 20 erhalten wir möglicherweise bit_cast ( Implementierung im Link vom Vorschlag verfügbar ), das eine einfache und sichere Möglichkeit zum Eingeben von Wortspielen bietet und in einem constexpr-Kontext verwendet werden kann.
Das folgende Beispiel zeigt, wie Sie mit bit_cast pun ein vorzeichenloses int eingeben, um zu schweben ( siehe live ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
In dem Fall , wo zu und von Arten nicht die gleiche Größe hat, bedarf es uns einen Zwischen struct15 zu verwenden. Wir werden eine Struktur verwenden, die ein Zeichenarray sizeof (unsigned int) enthält ( vorausgesetzt, 4 Byte unsigned int ), um den From- Typ und unsigned int als To- Typ zu sein :.
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
Es ist bedauerlich, dass wir diesen Zwischentyp benötigen, aber das ist die aktuelle Einschränkung von bit_cast .
Wir haben nicht viele gute Tools zum Abfangen von striktem Aliasing in C ++. Die Tools, die wir haben, werden einige Fälle von strengen Aliasing-Verstößen und einige Fälle von falsch ausgerichteten Ladevorgängen und Speichern abfangen.
gcc mit dem Flag -fstrict-aliasing und -Wstrict-aliasing kann einige Fälle abfangen, wenn auch nicht ohne falsch positive / negative Ergebnisse . In den folgenden Fällen wird beispielsweise eine Warnung in gcc generiert ( siehe live ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
obwohl es diesen zusätzlichen Fall nicht erfassen wird ( sehen Sie es live ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Obwohl clang diese Flags zulässt, werden die Warnungen anscheinend nicht implementiert.
Ein weiteres Tool, das uns zur Verfügung steht, ist ASan, mit dem falsch ausgerichtete Lasten und Speicher aufgefangen werden können. Obwohl dies keine direkten strengen Aliasing-Verstöße sind, sind sie ein häufiges Ergebnis strenger Aliasing-Verstöße. In den folgenden Fällen werden beispielsweise Laufzeitfehler generiert, wenn mit clang unter Verwendung von -fsanitize = address erstellt wird
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
Das letzte Tool, das ich empfehlen werde, ist C ++ - spezifisch und nicht ausschließlich ein Tool, sondern eine Codierungspraxis. Lassen Sie keine Casts im C-Stil zu. Sowohl gcc als auch clang erstellen eine Diagnose für Casts im C-Stil unter Verwendung von Cast im -Wold-Stil . Dadurch werden alle undefinierten Wortspiele gezwungen, reinterpret_cast zu verwenden. Im Allgemeinen sollte reinterpret_cast ein Flag für eine genauere Codeüberprüfung sein. Es ist auch einfacher, Ihre Codebasis nach reinterpret_cast zu durchsuchen, um eine Prüfung durchzuführen.
Für C haben wir alle Tools bereits behandelt und wir haben auch tis-interpreter, einen statischen Analysator, der ein Programm für eine große Teilmenge der C-Sprache ausführlich analysiert. Bei einer C-Version des früheren Beispiels, bei der bei Verwendung von -fstrict-aliasing ein Fall übersehen wird ( siehe live )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter kann alle drei abfangen. Im folgenden Beispiel wird tis-kernal als tis-Interpreter aufgerufen (die Ausgabe wird der Kürze halber bearbeitet):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Schließlich gibt es TySan, das derzeit entwickelt wird. Dieses Desinfektionsprogramm fügt Informationen zur Typprüfung in ein Schattenspeichersegment ein und überprüft die Zugriffe, um festzustellen, ob sie gegen Aliasing-Regeln verstoßen. Das Tool sollte möglicherweise in der Lage sein, alle Aliasing-Verstöße zu erkennen, hat jedoch möglicherweise einen hohen Laufzeitaufwand.
reinterpret_cast
zu tun ist oder was cout
zu bedeuten ist. (Es ist in Ordnung, C ++ zu erwähnen, aber die ursprüngliche Frage betraf C und IIUC. Diese Beispiele könnten genauso gültig in C geschrieben werden.)
Striktes Aliasing bezieht sich nicht nur auf Zeiger, sondern auch auf Verweise. Ich habe ein Papier darüber für das Boost-Entwickler-Wiki geschrieben und es wurde so gut aufgenommen, dass ich daraus eine Seite auf meiner Beratungswebsite gemacht habe. Es erklärt vollständig, was es ist, warum es die Menschen so verwirrt und was man dagegen tun kann. Strict Aliasing White Paper . Insbesondere wird erklärt, warum Gewerkschaften für C ++ ein riskantes Verhalten darstellen und warum die Verwendung von memcpy die einzige Lösung ist, die sowohl in C als auch in C ++ portierbar ist. Hoffe das ist hilfreich.
Als Ergänzung zu dem, was Doug T. bereits geschrieben hat, ist hier ein einfacher Testfall, der ihn wahrscheinlich mit gcc auslöst:
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Kompilieren mit gcc -O2 -o check check.c
. Normalerweise (bei den meisten gcc-Versionen, die ich ausprobiert habe) gibt dies ein "striktes Aliasing-Problem" aus, da der Compiler davon ausgeht, dass "h" nicht dieselbe Adresse wie "k" in der Funktion "check" sein kann. Aus diesem Grund optimiert der Compiler die if (*h == 5)
Abwesenheit und ruft immer die printf auf.
Für diejenigen, die hier interessiert sind, ist der x64-Assembler-Code, der von gcc 4.6.3 erstellt wurde und auf Ubuntu 12.04.2 für x64 ausgeführt wird:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Die if-Bedingung ist also vollständig aus dem Assembler-Code verschwunden.
long long*
und int64_t
*), um mehr Spaß zu haben . Man könnte erwarten, dass ein vernünftiger Compiler erkennt, dass a long long*
und int64_t*
auf denselben Speicher zugreifen kann, wenn sie identisch gespeichert sind, aber eine solche Behandlung ist nicht mehr in Mode.
Typ-Punning über Zeiger-Casts (im Gegensatz zur Verwendung einer Union) ist ein wichtiges Beispiel dafür, wie striktes Aliasing gebrochen wird.
fpsync()
Direktive zwischen Schreiben als fp und Lesen als int ausführt oder umgekehrt [bei Implementierungen mit separaten Integer- und FPU-Pipelines und Caches Eine solche Anweisung ist zwar teuer, aber nicht so kostspielig, als wenn der Compiler bei jedem Gewerkschaftszugriff eine solche Synchronisierung durchführt. Oder eine Implementierung könnte angeben, dass der resultierende Wert nur unter Umständen verwendet werden kann, die gemeinsame Anfangssequenzen verwenden.
Gemäß der C89-Begründung wollten die Autoren des Standards nicht verlangen, dass Compiler Code wie folgt erhalten:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
sollte erforderlich sein, um den Wert von x
zwischen der Zuweisungs- und der return-Anweisung neu zu laden , um die Möglichkeit zu berücksichtigen, auf die p
möglicherweise hingewiesen wird x
, und die Zuweisung zu *p
kann folglich den Wert von ändern x
. Die Vorstellung, dass ein Compiler das Recht haben sollte anzunehmen, dass es in Situationen wie den oben genannten kein Aliasing gibt, war unumstritten.
Leider haben die Autoren des C89 ihre Regel so geschrieben, dass im wörtlichen Sinne sogar die folgende Funktion Undefiniertes Verhalten aufruft:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
weil es einen l-Wert vom Typ verwendet, int
um auf ein Objekt vom Typ zuzugreifen struct S
, und int
nicht zu den Typen gehört, die für den Zugriff auf a verwendet werden können struct S
. Da es absurd wäre, die Verwendung von Mitgliedern von Strukturen und Gewerkschaften, die keine Zeichen sind, als undefiniertes Verhalten zu behandeln, erkennt fast jeder, dass es zumindest einige Umstände gibt, unter denen ein Wert eines Typs verwendet werden kann, um auf ein Objekt eines anderen Typs zuzugreifen . Leider hat das C-Normungskomitee diese Umstände nicht definiert.
Ein Großteil des Problems ist auf den Fehlerbericht Nr. 028 zurückzuführen, in dem nach dem Verhalten eines Programms gefragt wurde, z.
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
Der Fehlerbericht Nr. 28 besagt, dass das Programm undefiniertes Verhalten aufruft, da beim Schreiben eines Gewerkschaftsmitglieds vom Typ "double" und beim Lesen eines Mitglieds vom Typ "int" implementierungsdefiniertes Verhalten aufgerufen wird. Eine solche Argumentation ist unsinnig, bildet jedoch die Grundlage für die Regeln für den effektiven Typ, die die Sprache unnötig komplizieren und nichts tun, um das ursprüngliche Problem anzugehen.
Der beste Weg, um das ursprüngliche Problem zu lösen, besteht wahrscheinlich darin, die Fußnote über den Zweck der Regel so zu behandeln, als ob sie normativ wäre, und die Regel nicht durchsetzbar zu machen, außer in Fällen, in denen tatsächlich widersprüchliche Zugriffe mithilfe von Aliasen auftreten. Gegeben etwas wie:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Es gibt keinen Konflikt innerhalb, inc_int
da alle Zugriffe auf den Speicher, auf den über zugegriffen *p
wird, mit einem Wert vom Typ l erfolgen int
, und es gibt keinen Konflikt darin, test
weil alle Zugriffe auf diesen Speicher, der jemals erfolgen p
wird, sichtbar von a abgeleitet struct S
sind und bei der nächsten s
Verwendung verwendet werden durch p
wird schon passiert sein.
Wenn der Code leicht geändert wurde ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Hier besteht ein Aliasing-Konflikt zwischen p
und dem Zugriff s.x
auf die markierte Zeile, da zu diesem Zeitpunkt in der Ausführung eine andere Referenz vorhanden ist , die für den Zugriff auf denselben Speicher verwendet wird .
Hätte der Fehlerbericht 028 gesagt, dass das ursprüngliche Beispiel UB aufgrund der Überlappung zwischen der Erstellung und Verwendung der beiden Zeiger aufgerufen hat, hätte dies die Dinge viel klarer gemacht, ohne dass "Effektive Typen" oder andere solche Komplexität hinzugefügt werden müssten.
Nachdem ich viele der Antworten gelesen habe, habe ich das Bedürfnis, etwas hinzuzufügen:
Striktes Aliasing (das ich gleich beschreiben werde) ist wichtig, weil :
Der Speicherzugriff kann teuer sein (in Bezug auf die Leistung), weshalb Daten in CPU-Registern bearbeitet werden, bevor sie in den physischen Speicher zurückgeschrieben werden.
Wenn Daten in zwei verschiedenen CPU-Registern in denselben Speicherplatz geschrieben werden, können wir nicht vorhersagen, welche Daten beim Codieren in C "überleben" werden.
In der Assembly, in der wir das Laden und Entladen von CPU-Registern manuell codieren, wissen wir, welche Daten intakt bleiben. Aber C abstrahiert (zum Glück) dieses Detail weg.
Da zwei Zeiger auf dieselbe Stelle im Speicher verweisen können, kann dies zu komplexem Code führen, der mögliche Kollisionen behandelt .
Dieser zusätzliche Code ist langsam und beeinträchtigt die Leistung, da er zusätzliche Lese- / Schreibvorgänge für den Speicher ausführt, die sowohl langsamer als auch (möglicherweise) unnötig sind.
Die strenge Aliasing - Regel erlaubt es uns , redundanten Maschinencode zu vermeiden , in Fällen , in denen es sollte sicher davon ausgehen , dass zwei Zeiger weisen nicht auf den gleichen Speicherblock (siehe auch restrict
Stichwort).
Das strikte Aliasing besagt, dass davon ausgegangen werden kann, dass Zeiger auf verschiedene Typen auf verschiedene Speicherorte im Speicher verweisen.
Wenn ein Compiler feststellt, dass zwei Zeiger auf unterschiedliche Typen verweisen (z. B. an int *
und a float *
), geht er davon aus, dass die Speicheradresse unterschiedlich ist, und schützt nicht vor Kollisionen mit Speicheradressen, was zu einem schnelleren Maschinencode führt.
Zum Beispiel :
Nehmen wir folgende Funktion an:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Um den Fall zu behandeln, in dem a == b
(beide Zeiger zeigen auf denselben Speicher), müssen wir die Art und Weise, wie wir Daten aus dem Speicher in die CPU-Register laden, sortieren und testen, damit der Code wie folgt endet:
laden a
und b
aus dem Speicher.
hinzufügen a
zu b
.
Speichern b
und neu laden a
.
(Speichern Sie aus dem CPU-Register in den Speicher und laden Sie aus dem Speicher in das CPU-Register).
hinzufügen b
zu a
.
Speichern a
(aus dem CPU-Register) in den Speicher.
Schritt 3 ist sehr langsam, da er auf den physischen Speicher zugreifen muss. Allerdings ist es erforderlich , gegen Instanzen zu schützen , wo a
und b
auf die gleiche Speicheradresse.
Durch striktes Aliasing können wir dies verhindern, indem wir dem Compiler mitteilen, dass diese Speicheradressen deutlich unterschiedlich sind (was in diesem Fall eine weitere Optimierung ermöglicht, die nicht durchgeführt werden kann, wenn die Zeiger eine Speicheradresse gemeinsam nutzen).
Dies kann dem Compiler auf zwei Arten mitgeteilt werden, indem verschiedene Typen verwendet werden, auf die verwiesen wird. dh:
void merge_two_numbers(int *a, long *b) {...}
Verwenden Sie das restrict
Schlüsselwort. dh:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Durch Erfüllen der Strict Aliasing-Regel kann Schritt 3 vermieden werden und der Code wird erheblich schneller ausgeführt.
Tatsächlich könnte durch Hinzufügen des restrict
Schlüsselworts die gesamte Funktion optimiert werden, um:
laden a
und b
aus dem Speicher.
hinzufügen a
zu b
.
Ergebnis sowohl an a
als auch an speichern b
.
Diese Optimierung hätte aufgrund der möglichen Kollision (wo a
und b
würde verdreifacht statt verdoppelt) vorher nicht durchgeführt werden können .
b
(nicht neu laden) und laden neu a
. Ich hoffe es ist jetzt klarer.
restrict
, aber ich würde denken, dass letzteres in den meisten Fällen effektiver wäre, und eine Lockerung einiger Einschränkungen register
würde es ermöglichen, einige der Fälle auszufüllen, in denen restrict
dies nicht helfen würde. Ich bin mir nicht sicher, ob es jemals "wichtig" war, den Standard so zu behandeln, dass er alle Fälle vollständig beschreibt, in denen Programmierer erwarten sollten, dass Compiler Beweise für Aliasing erkennen, anstatt nur Orte zu beschreiben, an denen Compiler Aliasing voraussetzen müssen, selbst wenn keine besonderen Beweise dafür vorliegen .
restrict
minimiert das Schlüsselwort nicht nur die Geschwindigkeit der Operationen, sondern auch deren Anzahl, was von Bedeutung sein könnte ... Ich meine, schließlich ist die schnellste Operation überhaupt keine Operation :)
Durch striktes Aliasing können keine unterschiedlichen Zeigertypen auf dieselben Daten zugelassen werden.
Dieser Artikel soll Ihnen helfen, das Problem im Detail zu verstehen.
int
eine Struktur, die eine enthält int
).
Technisch gesehen ist in C ++ die strenge Aliasing-Regel wahrscheinlich nie anwendbar.
Beachten Sie die Definition der Indirektion ( * Operator ):
Der unäre * -Operator führt eine Indirektion durch: Der Ausdruck, auf den er angewendet wird, muss ein Zeiger auf einen Objekttyp oder ein Zeiger auf einen Funktionstyp sein, und das Ergebnis ist ein Wert, der sich auf das Objekt oder die Funktion bezieht, auf die der Ausdruck zeigt .
Auch aus der Definition von glvalue
Ein glvalue ist ein Ausdruck, dessen Auswertung die Identität eines Objekts bestimmt, (... snip)
In jeder genau definierten Programmablaufverfolgung bezieht sich ein gl-Wert auf ein Objekt. Die so genannte strenge Aliasing-Regel gilt also niemals. Dies ist möglicherweise nicht das, was die Designer wollten.
int foo;
, worauf greift der lvalue-Ausdruck zu *(char*)&foo
? Ist das ein Objekt vom Typ char
? Entsteht dieses Objekt gleichzeitig mit foo
? Würde das Schreiben foo
den gespeicherten Wert dieses oben genannten Objekts vom Typ ändern char
? Wenn ja, gibt es eine Regel, nach der auf den gespeicherten Wert eines Objekts vom Typ char
mit einem Wert vom Typ zugegriffen werden kann int
?
int i;
vier Objekte jedes Zeichentyps in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` und i
. Schließlich gibt es im Standard nichts, was es selbst einem volatile
qualifizierten Zeiger erlaubt , auf Hardwareregister zuzugreifen, die nicht der Definition von "Objekt" entsprechen.
c
undc++faq
.