(ODER streiten tippen, vages Verhalten und Ausrichtung, oh mein Gott!)Hallo allerseits, in ein paar Wochen starten wir einen neuen Thread im Kurs
"C ++ Developer" . Diese Veranstaltung ist unserem heutigen Material gewidmet.
Was ist striktes Aliasing? Zuerst beschreiben wir, was Aliasing ist, und dann finden wir heraus, wofür Strenge ist.
In C und C ++ hängt Aliasing davon ab, auf welche Arten von Ausdrücken wir auf gespeicherte Werte zugreifen dürfen. Sowohl in C als auch in C ++ definiert der Standard, welche Namensausdrücke für welche Typen gültig sind. Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln strikt befolgen, daher ist der Begriff die strikte Aliasing-Regel. Wenn wir versuchen, mit einem ungültigen Typ auf einen Wert zuzugreifen, wird dieser als undefiniertes Verhalten (UB) klassifiziert. Wenn wir unsicheres Verhalten haben, werden alle Wetten abgeschlossen 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 des Compilers mit neuer Optimierung den von uns als gültig erachteten Code verletzt. Dies ist unerwünscht. Es lohnt sich, die strengen Regeln des Aliasing zu verstehen und zu vermeiden, dass sie verletzt werden.

Um besser zu verstehen, warum wir uns darüber Sorgen machen sollten, werden wir die Probleme diskutieren, die bei Verstößen gegen die strengen Aliasing-Regeln auftreten, Typ Punning, wie es häufig in strengen Aliasing-Regeln verwendet wird, sowie das korrekte Erstellen eines Wortspiels zusammen mit Einige mögliche Hilfen mit C ++ 20, um das Wortspiel zu vereinfachen und die Fehlerwahrscheinlichkeit zu verringern. Wir werden die Diskussion zusammenfassen, indem wir einige Methoden zum Erkennen von Verstößen gegen strenge Aliasing-Regeln betrachten.
Vorläufige BeispieleSchauen wir uns einige Beispiele an, und dann können wir diskutieren, was genau in den Standards angegeben ist, einige zusätzliche Beispiele betrachten und dann herausfinden, wie striktes Aliasing vermieden und Verstöße identifiziert werden können, die wir verpasst haben. Hier ist
ein Beispiel , das Sie nicht überraschen sollte:
int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n";
Wir haben int *, das auf den von int belegten Speicher zeigt, und dies ist ein gültiges Aliasing. Der Optimierer sollte davon ausgehen, dass Zuweisungen über IP den von x belegten Wert aktualisieren können.
Das folgende
Beispiel zeigt Aliasing, das zu undefiniertem Verhalten führt:
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 foo-Funktion nehmen wir int * und float *. In diesem Beispiel rufen wir foo auf und setzen beide Parameter so, dass sie auf denselben Speicherort verweisen, der in diesem Beispiel ein int enthält. Beachten Sie, dass
reinterpret_cast den Compiler anweist, den Ausdruck so zu behandeln, als hätte er den vom Vorlagenparameter angegebenen Typ. In diesem Fall weisen wir ihn an, den & x-Ausdruck so zu verarbeiten, als wäre er vom Typ float *. Wir können naiv erwarten, dass das Ergebnis des zweiten Couts 0 ist, aber wenn die Optimierung mit -O2 und gcc aktiviert ist und clang das folgende Ergebnis erhält:
0
1
Das mag unerwartet sein, aber völlig richtig, da wir undefiniertes Verhalten verursacht haben. Float kann kein gültiger Alias eines int-Objekts sein. Daher kann der Optimierer annehmen, dass die während der Dereferenzierung i gespeicherte Konstante 1 der Rückgabewert ist, da das Speichern durch f das int-Objekt nicht korrekt beeinflussen kann. Das Verbinden des Codes im Compiler Explorer zeigt, dass genau dies geschieht (
Beispiel ):
foo(float*, int*):
Ein Optimierer, der die typbasierte Alias-Analyse (TBAA) verwendet, geht davon aus, dass 1 zurückgegeben wird, und verschiebt den konstanten Wert direkt in das eax-Register, in dem der Rückgabewert gespeichert ist. TBAA verwendet Sprachregeln, welche Typen für Aliasing zulässig sind, um das Laden und Speichern zu optimieren. In diesem Fall weiß TBAA, dass float kein Alias von int sein kann, und optimiert das Laden zu Tode.
Nun zur ReferenzWas genau sagt der Standard darüber aus, was 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.
Was sagt der C11-Standard?
Der C11-Standard schreibt im Abschnitt „6.5 Ausdrücke“ von Absatz 7 Folgendes vor:
Das Objekt muss über einen eigenen gespeicherten Wert verfügen, auf den nur mit dem Ausdruck lvalue zugegriffen werden kann, 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 lvalue- int, int
- eine qualifizierte Version eines Typs, der mit dem aktuellen Objekttyp kompatibel ist;
int x = 1; const int *p = &x; printf("%d\n", *p); // * p lvalue- const int, int
- ein Typ, der ein Typ mit oder ohne Vorzeichen ist, der einem qualifizierten Objekttyp entspricht;
int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- unsigned int,
In Fußnote 12 finden Sie die Erweiterung gcc / clang , mit der Sie unsigned int * int * zuweisen können, auch wenn es sich nicht um kompatible Typen handelt.
- ein Typ, der ein Typ mit oder ohne Vorzeichen ist, der einer qualifizierten Version des aktuellen Objekttyps entspricht;
int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- const unsigned int, ,
- ein aggregierter oder kombinierter Typ, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds einer subaggregierten oder enthaltenen Assoziation), oder
struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo - , int , *ip // foo f; foobar( &f, &f.x );
- Zeichentyp.
int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p lvalue- char, . // - .
Was C ++ 17 Standardentwurf sagtDer C ++ 17-Projektstandard in Abschnitt 11 [basic.lval] besagt: Wenn ein Programm versucht, über einen anderen Wert als einen der folgenden Typen auf einen gespeicherten Wert eines Objekts zuzugreifen, ist das Verhalten undefiniert: 63 (11.1) ist ein dynamischer Objekttyp.
void *p = malloc( sizeof(int) ); // , int *ip = new (p) int{0}; // placement new int std::cout << *ip << "\n"; // * ip glvalue- int,
(11.2) - cv-qualifizierte (cv - const und volatile) Version des dynamischen Typs eines Objekts,
int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip glvalue const int, cv- x
(11.3) - ein Typ, der dem dynamischen Typ eines Objekts ähnlich ist (wie in 7.5 definiert),
//
(11.4) - ein Typ, der ein Typ mit oder ohne Vorzeichen ist, der dem dynamischen Typ eines Objekts entspricht;
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .
signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; }
(11.5) - ein Typ, der ein Typ mit oder ohne Vorzeichen ist, der der cv-qualifizierten Version des dynamischen Typs eines Objekts entspricht;
signed int foo( const signed int &si1, int &si2); // ,
(11.6) - ein aggregierter oder kombinierter Typ, 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 mit Assoziationen),
struct foo { int x; };
// Compiler Explorer (https://godbolt.org/g/z2wJTC)
int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx );
(11.7) - ein Typ, der (möglicherweise lebenslaufqualifiziert) ein Basisklassentyp eines dynamischen Objekttyps ist,
struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; }
(11.8) - Geben Sie char, unsigned char oder std :: byte ein.
int foo( std::byte &b, uint32_t &ui ) { b = static_cast<std::byte>('a'); ui = 0xFFFFFFFF; return std::to_integer<int>( b ); // b glvalue- std::byte, uint32_t }
Es ist erwähnenswert, dass
signed char
Zeichen nicht in der obigen Liste enthalten ist. Dies ist ein spürbarer Unterschied zu C, das über die Art des Zeichens spricht.
Subtile UnterschiedeObwohl wir sehen können, dass C und C ++ ähnliche Aussagen zum Aliasing machen, gibt es einige Unterschiede, die wir beachten sollten. C ++ hat kein C-Konzept eines
gültigen oder
kompatiblen Typs, und C hat kein C ++ - Konzept eines
dynamischen oder ähnlichen Typs. Obwohl beide Ausdrücke lvalue und rvalue haben, hat C ++ auch Ausdrücke glvalue, prvalue und xvalue. Diese Unterschiede liegen weitgehend außerhalb des Geltungsbereichs dieses Artikels. Ein interessantes Beispiel ist jedoch, wie ein Objekt aus dem von malloc verwendeten Speicher erstellt wird. In C können wir einen gültigen Typ festlegen, z. B. das Schreiben über lvalue oder memcpy in den Speicher.
// C, C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); // *p - float C // float *fp = p; *fp = 1.0f; // *p - float C
Keine dieser Methoden ist in C ++ ausreichend, was die Platzierung neuer erfordert:
float *fp = new (p) float{1.0f} ; // *p float
Sind int8_t und uint8_t Zeichentypen?Theoretisch sollten weder int8_t noch uint8_t Zeichentypen sein, aber in der Praxis werden sie auf diese Weise implementiert. Dies ist wichtig, denn wenn es sich wirklich um Zeichentypen handelt, handelt es sich auch um Aliase wie Zeichentypen. Wenn Sie sich dessen nicht bewusst sind, kann
dies zu unerwarteten Leistungseinbußen führen . Wir sehen, dass
glibc typedef
int8_t
und
uint8_t
für
signed char
int8_t
bzw.
uint8_t
signed char
glibc typedef
.
Dies wäre schwer zu ändern, da es für C ++ eine ABI-Lücke wäre. Dies würde die Namensverzerrung ändern und jede API unter Verwendung eines dieser Typen in ihrer Schnittstelle beschädigen.
Das Ende des ersten Teils. Und wir werden in ein paar Tagen über das Wortspiel für Eingabe und Ausrichtung sprechen.
Schreiben Sie Ihre Kommentare und verpassen Sie nicht das
offene Webinar , das am 6. März vom Leiter der Technologieentwicklung bei Rambler & Co,
Dmitry Shebordaev ,
abgehalten wird .