(ODER streiten tippen, vages Verhalten und Ausrichtung, oh mein Gott!)Freunde, es bleibt nur sehr wenig Zeit, bis ein neuer Thread zum Kurs
"C ++ Developer" gestartet wird. Es ist Zeit, eine Übersetzung des zweiten Teils des Materials zu veröffentlichen, die erzählt, was ein Wortspiel tippt.
Was ist eine Wortspiel-Typisierung?Wir haben den Punkt erreicht, an dem wir uns fragen können, warum wir überhaupt Pseudonyme brauchen könnten. Normalerweise für die Implementierung von Wortspielen, tk. häufig verwendete Methoden verstoßen gegen strenge Aliasing-Regeln.

Manchmal möchten wir das Typsystem umgehen und das Objekt als einen anderen Typ interpretieren. Das Neuinterpretieren eines Speichersegments als ein anderer Typ wird als
Wortspiel bezeichnet . Das Eingeben von Wortspielen ist nützlich für Aufgaben, die Zugriff auf die Basisdarstellung eines Objekts erfordern, um die bereitgestellten Daten anzuzeigen, zu transportieren oder zu bearbeiten. Typische Bereiche, in denen wir auf die Verwendung von Wortspielen stoßen können: Compiler, Serialisierung, Netzwerkcode usw.
Traditionell wurde dies erreicht, indem die Adresse des Objekts in einen Zeiger auf den Typ umgewandelt wurde, den wir interpretieren möchten, und dann auf den Wert zugegriffen wurde, oder mit anderen Worten, unter Verwendung von Aliasen. Zum Beispiel:
int x = 1 ; // C float *fp = (float*)&x ; // // C++ float *fp = reinterpret_cast<float*>(&x) ; // printf( “%f\n”, *fp ) ;
Wie wir bereits gesehen haben, ist dies ein inakzeptables Aliasing, das zu undefiniertem Verhalten führt. Aber traditionell verwendeten Compiler keine strengen Aliasing-Regeln, und diese Art von Code funktionierte normalerweise nur, und Entwickler sind es leider gewohnt, solche Dinge zuzulassen. Eine übliche alternative Wortspielmethode ist die Vereinigung, die in C gültig ist, in C ++ jedoch undefiniertes Verhalten verursacht (
siehe Beispiel ):
union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB(undefined behaviour) C++ “n is not the active member”
Dies ist in C ++ nicht akzeptabel, und einige glauben, dass Gewerkschaften ausschließlich zur Implementierung von Variantentypen bestimmt sind, und halten die Verwendung von Gewerkschaften zum Eingeben von Wortspielen für einen Missbrauch.
Wie implementiere ich ein Wortspiel?Die Standardmethode für die Eingabe von Wortspielen in C und C ++ ist memcpy. Dies mag etwas kompliziert erscheinen, aber der Optimierer muss die Verwendung von memcpy für das Wortspiel erkennen, optimieren und ein Register erstellen, 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
Wir können
memcpy
:
void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //…
Mit einem ausreichenden Optimierungsgrad generiert jeder anständige moderne Compiler Code, der mit der zuvor erwähnten Methode reinterpret_cast oder der Join-Methode identisch ist, um ein Wortspiel zu erhalten. Wenn wir den generierten Code studieren, sehen wir, dass er nur das mov-Register verwendet (
Beispiel ).
Wortspielertypen und ArraysWas aber, wenn wir das Wortspiel eines vorzeichenlosen char-Arrays in eine Reihe von vorzeichenlosen int implementieren und dann eine Operation für jeden vorzeichenlosen int-Wert ausführen möchten? Wir können memcpy verwenden, um ein nicht signiertes char-Array in einen temporären nicht gesungenen int-Typ umzuwandeln. Das Optimierungsprogramm kann weiterhin alles über memcpy anzeigen, sowohl das temporäre Objekt als auch die Kopie optimieren und direkt mit den zugrunde liegenden Daten arbeiten (
Beispiel ):
// , int foo( unsigned int x ) { return x ; } // , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
In diesem Beispiel nehmen wir
char*p
, nehmen an, dass es auf mehrere Fragmente der
sizeof(unsigned int)
Daten zeigt, interpretieren jedes Datenfragment als
unsigned int
, berechnen
foo()
für jedes Fragment des Wortspiels, fassen es im Ergebnis zusammen und geben den Endwert zurück .
Die Assembly für den Schleifenkörper zeigt, dass der Optimierer den Körper in direkten Zugriff auf das
unsigned char
base-Array als
unsigned int
verwandelt und es direkt zu
eax
hinzufügt:
add eax, dword ptr [rdi + rcx]
Derselbe Code, jedoch mit
reinterpret_cast
, um ein Wortspiel zu implementieren (verstößt gegen striktes Aliasing):
// , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
C ++ 20 und bit_castIn C ++ 20 haben wir
bit_cast
, das eine einfache und sichere Möglichkeit zur Interpretation bietet und auch im Kontext von
constexpr
.
Das folgende Beispiel zeigt, wie mit
bit_cast
eine vorzeichenlose Ganzzahl in einem
float
interpretiert wird (
Beispiel ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //, sizeof(float) == sizeof(unsigned int)
Für den Fall, dass die Typen An und Von nicht dieselbe Größe haben, müssen wir eine Zwischenstruktur verwenden. Wir werden eine Struktur verwenden, die ein Zeichenarray-Vielfaches von
sizeof(unsigned int)
(ein 4-Byte-vorzeichenloses int wird angenommen) als From-Typ und
unsigned int
als To-Typ enthält:
struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // sizeof( unsigned int ) == 4 }; // len 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 ; }
Leider benötigen wir diesen Zwischentyp - dies ist die aktuelle
bit_cast
Einschränkung.
AusrichtungIn früheren Beispielen haben wir gesehen, dass ein Verstoß gegen strenge Aliasing-Regeln zum Ausschluss von Speicher während der Optimierung führen kann. Verstöße gegen striktes Aliasing können auch zu Verstößen gegen die Ausrichtungsanforderungen führen. Sowohl die C-Standards als auch C ++ besagen, dass Objekte Ausrichtungsanforderungen unterliegen, die den Ort einschränken, an dem Objekte platziert werden können (im Speicher) und daher zugänglich sind.
C11 Abschnitt 6.2.8 Ausrichtung von Objektzuständen :
Für vollständige Objekttypen gelten Ausrichtungsanforderungen, die die Adressen einschränken, an denen Objekte dieses Typs platziert werden können. Die Ausrichtung ist ein implementierungsdefinierter ganzzahliger Wert, der die Anzahl der Bytes zwischen aufeinanderfolgenden Adressen darstellt, an denen dieses Objekt platziert werden kann. Der Typ des Objekts stellt für jedes Objekt dieses Typs eine Ausrichtungsanforderung: Mit dem
_Alignas
kann eine strengere Ausrichtung angefordert werden.
Der C ++ 17-Projektstandard in Abschnitt 1 [basic.align] :
Für Objekttypen gelten Ausrichtungsanforderungen (6.7.1, 6.7.2), die die Adressen einschränken, an denen ein Objekt dieses Typs platziert werden kann. Die Ausrichtung ist ein implementierungsdefinierter ganzzahliger Wert, der die Anzahl der Bytes zwischen aufeinanderfolgenden Adressen darstellt, an denen ein bestimmtes Objekt platziert werden kann. Ein Objekttyp stellt für jedes Objekt dieses Typs eine Ausrichtungsanforderung. Eine strengere Ausrichtung kann mit dem Ausrichtungsspezifizierer (10.6.2) angefordert werden.
Sowohl C99 als auch C11 weisen ausdrücklich darauf hin, dass eine Konvertierung, die zu einem nicht ausgerichteten Zeiger führt, ein undefiniertes Verhalten ist (Abschnitt 6.3.2.3).
Zeiger sagt:
Ein Zeiger auf ein Objekt oder einen Teiltyp kann in einen Zeiger auf ein anderes Objekt oder einen Teiltyp konvertiert werden. Wenn der resultierende Zeiger für den Zeigertyp nicht korrekt ausgerichtet ist, ist das Verhalten undefiniert. ...
Obwohl C ++ nicht so offensichtlich ist, glaube ich, dass dieser Satz aus Absatz 1
[basic.align]
ausreicht:
... Der Objekttyp stellt für jedes Objekt dieses Typs eine Ausrichtungsanforderung. ...
BeispielNehmen wir also an:
- alignof (char) und alignof (int) sind 1 bzw. 4
- sizeof (int) ist 4
Das Interpretieren eines char-Arrays der Größe 4 als
int
verstößt somit gegen striktes Aliasing und kann auch gegen die Ausrichtungsanforderungen verstoßen, wenn das Array eine Ausrichtung von 1 oder 2 Bytes aufweist.
char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 1 2 int x = *reinterpret_cast<int*>(arr); // Undefined behavior
Dies kann in einigen Situationen zu Leistungseinbußen oder Busfehlern führen. Während die Verwendung von Alignas zum Erzwingen der gleichen Ausrichtung für ein Array in int verhindert, dass die Ausrichtungsanforderungen nicht erfüllt werden:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
AtomizitätEine weitere unerwartete Strafe für unausgeglichenen Zugriff besteht darin, dass die Atomizität einiger Architekturen verletzt wird. Atomspeicher werden für andere Threads in x86 möglicherweise nicht atomar angezeigt, wenn sie nicht ausgerichtet sind.
Auffangen strenger Aliasing-VerstößeWir haben nicht viele gute Tools, um striktes Aliasing in C ++ zu verfolgen. Die Tools, die wir haben, werden einige Fälle von Verstößen und einige Fälle von unsachgemäßem Laden und Speichern auffangen.
gcc mit den
-fstrict-aliasing
und
-Wstrict-aliasing
kann einige Fälle abfangen, allerdings nicht ohne Fehlalarme / Probleme. In den folgenden Fällen wird beispielsweise eine Warnung in gcc generiert (
Beispiel ):
int a = 1; short j; float f = 1.f; // , TIS , printf("%i\n", j = *(reinterpret_cast<short*>(&a))); printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
obwohl er diesen zusätzlichen Fall nicht erfassen wird (
Beispiel ):
int *p; p=&a; printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Obwohl
clang
diese Flags auflöst, scheint es die Warnung nicht tatsächlich zu implementieren.
Ein weiteres Tool, das wir haben, ist ASan, das falsch ausgerichtete Aufzeichnungen und Speicherungen erkennen kann. Obwohl es sich nicht um direkte Verstöße gegen striktes Aliasing handelt, ist dies ein recht häufiges Ergebnis. In den folgenden Fällen werden beispielsweise Laufzeitfehler während der Assemblierung mithilfe von clang mit
-fsanitize=address
int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); // x *u = 1; // [6-9] printf( "%d\n", *u ); // [6-9]
Das letzte Tool, das ich empfehle, ist spezifisch für C ++ und in der Tat nicht nur ein Tool, sondern auch eine Codierungspraxis, die kein Casting im C-Stil erlaubt. Sowohl
gcc
als auch
clang
führen eine Diagnose für
-Wold-style-cast
mit
-Wold-style-cast
. Dadurch werden alle undefinierten Wortspiele gezwungen, reinterpret_cast zu verwenden. Im Allgemeinen sollte
reinterpret_cast
ein Signal für eine gründlichere Codeanalyse sein.
Es ist auch einfacher, die Codebasis nach
reinterpret_cast
zu durchsuchen, um eine Prüfung durchzuführen.
Für C verfügen wir über alle bereits beschriebenen Tools und über einen
tis-interpreter
, einen statischen Analysator, der das Programm ausführlich auf eine große Teilmenge von C analysiert. In den C-Versionen des vorherigen Beispiels wird bei Verwendung von -fstrict-aliasing ein Fall übersprungen (
Beispiel )
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));
Der TIS-Interpreter kann alle drei abfangen. Im folgenden Beispiel wird der TIS-Kernel als TIS-Interpreter bezeichnet (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.
Und schließlich
TySan , das sich in der Entwicklung befindet. Dieses Desinfektionsprogramm fügt dem Schattenspeichersegment Informationen zur Typprüfung hinzu und überprüft die Zugriffe, um festzustellen, ob sie gegen die Aliasing-Regeln verstoßen. Das Tool sollte möglicherweise in der Lage sein, alle Aliasing-Verstöße zu verfolgen, kann jedoch zur Laufzeit einen hohen Overhead haben.
FazitWir haben etwas über Aliasing-Regeln in C und C ++ gelernt, was bedeutet, dass der Compiler von uns erwartet, dass wir diese Regeln strikt befolgen und die Konsequenzen akzeptieren, wenn wir sie nicht erfüllen. Wir haben einige Tools kennengelernt, mit denen wir Pseudonymmissbrauch identifizieren können. Wir haben gesehen, dass die übliche Verwendung von Aliasing ein Wortspiel der Typisierung ist. Wir haben auch gelernt, wie man es richtig implementiert.
Optimierer verbessern schrittweise die typbasierte Alias-Analyse und brechen bereits Code, der auf strengen Aliasing-Verstößen basiert. Wir können erwarten, dass Optimierungen besser werden und noch mehr Code brechen, der gerade funktioniert hat.
Wir haben vorgefertigte kompatible Standardmethoden für die Interpretation von Typen. Manchmal sollten diese Methoden für Debug-Builds freie Abstraktionen sein. Wir haben verschiedene Tools zum Erkennen schwerwiegender Aliasing-Verstöße, aber für C ++ werden sie nur einen kleinen Teil der Fälle erfassen, und für C können wir mit dem tis-Interpreter die meisten Verstöße verfolgen.
Vielen Dank an diejenigen, die diesen Artikel kommentiert haben: JF Bastien, Christopher Di Bella, Pascal Quoc, Matt P. Dziubinski, Patrice Roy und Olafur Vaage
Am Ende gehören natürlich alle Fehler dem Autor.
Damit ist die Übersetzung eines ziemlich großen Materials beendet, dessen erster Teil
hier gelesen
werden kann . Und wir laden Sie traditionell zum
Tag der offenen Tür ein , der am 14. März vom Leiter der Abteilung für Technologieentwicklung bei Rambler & Co -
Dmitry Shebordaev abgehalten wird.