Wenn nach häufigem undefiniertem Verhalten in C gefragt wird , wird manchmal auf die strenge Aliasing-Regel verwiesen.
Worüber reden sie?
804
Wenn nach häufigem undefiniertem Verhalten in C gefragt wird , wird manchmal auf die strenge Aliasing-Regel verwiesen.
Worüber reden sie?
c
undc++faq
.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 oderuint16_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
):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, geladenbuff[0]
undbuff[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 Inhaltbuff
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.
Und haben Sie unsere frühere Schleife neu geschrieben, um diese praktische Funktion zu nutzen
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.
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ürchar*
(einschließlichsigned char
undunsigned char
). Es wird immer angenommen, dasschar*
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.
Fußnote
1 Die Typen, auf die C 2011 6.5 7 einen l-Wert zulässt, sind:
quelle
unsigned char*
weitchar*
verwendet werden? Ich neige dazu,unsigned char
eher alschar
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öchteunsigned char *
in Ordnung.uint32_t* buff = malloc(sizeof(Msg));
und die nachfolgenden Unionunsigned int asBuffer[sizeof(Msg)];
Buffer-Deklarationen haben unterschiedliche Größen und keine ist korrekt. Dermalloc
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:
Wenn Sie also
int*
auf einen Speicher zeigen, der ein enthält,int
und dannfloat*
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.quelle
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):
Wortlaut von C ++ 11 und C ++ 14 (Änderungen hervorgehoben):
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):
quelle
wow(&u->s1,&u->s2)
wäre, müsste er auch dann legal sein, wenn ein Zeiger zum Ändern verwendet wirdu
, und dies würde die meisten Optimierungen negieren, die der Die Aliasing-Regel wurde entwickelt, um die Arbeit zu erleichtern.Hinweis
Dies ist ein Auszug aus meinem "Was ist die strikte Aliasing-Regel und warum interessiert es uns?" Aufschreiben.
Was ist striktes Aliasing?
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.
Vorläufige Beispiele
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 ):
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 ):
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:
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 ):
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 .
Nun zum Regelbuch
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.
Was sagt der C11-Standard?
Der C11- Standard schreibt in Abschnitt 6.5 Ausdrücke Absatz 7 Folgendes vor :
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.
Was der C ++ 17 Draft Standard sagt
Der C ++ 17-Standardentwurf in Abschnitt [basic.lval] Absatz 11 lautet:
Bemerkenswert ist, dass signiertes Zeichen nicht in der obigen Liste enthalten ist. Dies ist ein bemerkenswerter Unterschied zu C, das einen Zeichentyp angibt .
Was ist Type Punning?
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:
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 ):
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.
Wie geben wir Pun richtig ein?
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 :
wir können memcpy verwenden :
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 ).
C ++ 20 und bit_cast
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 ):
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 :.
Es ist bedauerlich, dass wir diesen Zwischentyp benötigen, aber das ist die aktuelle Einschränkung von bit_cast .
Auffangen strenger Aliasing-Verstöße
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 ):
obwohl es diesen zusätzlichen Fall nicht erfassen wird ( sehen Sie es live ):
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
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 )
tis-interpeter kann alle drei abfangen. Im folgenden Beispiel wird tis-kernal als tis-Interpreter aufgerufen (die Ausgabe wird der Kürze halber bearbeitet):
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.
quelle
reinterpret_cast
zu tun ist oder wascout
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.
quelle
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
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 dieif (*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:
Die if-Bedingung ist also vollständig aus dem Assembler-Code verschwunden.
quelle
long long*
undint64_t
*), um mehr Spaß zu haben . Man könnte erwarten, dass ein vernünftiger Compiler erkennt, dass along long*
undint64_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.
quelle
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:
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 diep
möglicherweise hingewiesen wirdx
, und die Zuweisung zu*p
kann folglich den Wert von ändernx
. 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:
weil es einen l-Wert vom Typ verwendet,
int
um auf ein Objekt vom Typ zuzugreifenstruct S
, undint
nicht zu den Typen gehört, die für den Zugriff auf a verwendet werden könnenstruct 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.
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:
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 erfolgenint
, und es gibt keinen Konflikt darin,test
weil alle Zugriffe auf diesen Speicher, der jemals erfolgenp
wird, sichtbar von a abgeleitetstruct S
sind und bei der nächstens
Verwendung verwendet werden durchp
wird schon passiert sein.Wenn der Code leicht geändert wurde ...
Hier besteht ein Aliasing-Konflikt zwischen
p
und dem Zugriffs.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.
quelle
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 afloat *
), 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:
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
undb
aus dem Speicher.hinzufügen
a
zub
.Speichern
b
und neu ladena
.(Speichern Sie aus dem CPU-Register in den Speicher und laden Sie aus dem Speicher in das CPU-Register).
hinzufügen
b
zua
.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
undb
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:
Verwenden Sie das
restrict
Schlüsselwort. dh: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
undb
aus dem Speicher.hinzufügen
a
zub
.Ergebnis sowohl an
a
als auch an speichernb
.Diese Optimierung hätte aufgrund der möglichen Kollision (wo
a
undb
würde verdreifacht statt verdoppelt) vorher nicht durchgeführt werden können .quelle
b
(nicht neu laden) und laden neua
. 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änkungenregister
würde es ermöglichen, einige der Fälle auszufüllen, in denenrestrict
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.
quelle
int
eine Struktur, die eine enthältint
).Technisch gesehen ist in C ++ die strenge Aliasing-Regel wahrscheinlich nie anwendbar.
Beachten Sie die Definition der Indirektion ( * Operator ):
Auch aus der Definition von glvalue
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.
quelle
int foo;
, worauf greift der lvalue-Ausdruck zu*(char*)&foo
? Ist das ein Objekt vom Typchar
? Entsteht dieses Objekt gleichzeitig mitfoo
? Würde das Schreibenfoo
den gespeicherten Wert dieses oben genannten Objekts vom Typ ändernchar
? Wenn ja, gibt es eine Regel, nach der auf den gespeicherten Wert eines Objekts vom Typchar
mit einem Wert vom Typ zugegriffen werden kannint
?int i;
vier Objekte jedes Zeichentypsin 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` undi
. Schließlich gibt es im Standard nichts, was es selbst einemvolatile
qualifizierten Zeiger erlaubt , auf Hardwareregister zuzugreifen, die nicht der Definition von "Objekt" entsprechen.