985 Stimmen

Was ist die strenge Aliasing-Regel?

Bei der Frage nach allgemeines undefiniertes Verhalten in C wird manchmal auf die strenge Aliasing-Regel verwiesen.
Wovon reden sie?

8 Stimmen

Vielleicht interessieren Sie sich auch für einen Artikel, den ich kürzlich geschrieben habe Was ist die Strict Aliasing Rule und warum ist sie wichtig? . Es deckt eine Menge Material ab, das hier nicht behandelt wird, oder bietet in einigen Bereichen einen moderneren Ansatz.

24voto

supercat Punkte 72939

Nach der Begründung von C89 wollten die Autoren der Norm nicht verlangen, dass Compiler einen Code wie diesen ausgeben:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

sollte erforderlich sein, um den Wert von x zwischen der Zuweisung und der Rückgabeanweisung, um die Möglichkeit zu berücksichtigen, dass p könnte darauf hinweisen x und die Zuordnung zu *p könnte folglich den Wert von x . Die Vorstellung, dass ein Compiler das Recht haben sollte, davon auszugehen, dass es kein Aliasing geben wird in Situationen wie der oben genannten war nicht umstritten.

Leider haben die Autoren der C89 ihre Regel so formuliert, dass, wenn man sie wörtlich liest, sogar die folgende Funktion ein nicht definiertes Verhalten hervorrufen würde:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

weil es einen l-Wert vom Typ int um auf ein Objekt des Typs struct S そして int gehört nicht zu den Typen, die beim Zugriff auf eine struct S . Da es absurd wäre, jede Verwendung von Nicht-Zeichen-Typ-Mitgliedern von Structs und Unions als unbestimmtes Verhalten zu behandeln, erkennt fast jeder an, dass es zumindest einige Umstände gibt, unter denen ein L-Wert eines Typs verwendet werden kann, um auf ein Objekt eines anderen Typs zuzugreifen. Leider hat es das C-Standardisierungskomitee versäumt, zu definieren, was diese Umstände sind.

Ein Großteil des Problems ist auf den Mängelbericht Nr. 028 zurückzuführen, in dem nach dem Verhalten eines Programms wie diesem gefragt wurde:

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);
}

Defect Report #28 besagt, dass das Programm Undefiniertes Verhalten hervorruft, weil die Aktion des Schreibens eines Unionsmitglieds vom Typ "double" und des Lesens eines vom Typ "int" implementierungsdefiniertes Verhalten hervorruft. Eine solche Argumentation ist unsinnig, bildet aber die Grundlage für die Effective-Type-Regeln, die die Sprache unnötig verkomplizieren und nichts zur Lösung des ursprünglichen Problems beitragen.

Der beste Weg, das ursprüngliche Problem zu lösen, wäre wahrscheinlich eine Behandlung der Fußnote über den Zweck der Regel so zu behandeln, als wäre sie normativ, und die die Regel nicht durchsetzbar zu machen, außer in Fällen, in denen es tatsächlich um widersprüchliche Zugriffe unter Verwendung von Aliasen geht. Gegeben wäre 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 weil alle Zugriffe auf den Speicher, die über *p werden mit einem l-Wert des Typs int und es gibt keinen Konflikt in test denn p ist sichtbar abgeleitet von einer struct S und beim nächsten Mal s verwendet wird, werden alle Zugriffe auf diesen Speicher, die jemals über p wird bereits geschehen sein.

Wenn der Code leicht geändert würde...

 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 gibt es einen Aliasing-Konflikt zwischen p und den Zugang zu s.x in der markierten Zeile, weil zu diesem Zeitpunkt der Ausführung ein anderer Verweis existiert die für den Zugriff auf denselben Speicher verwendet werden .

Hätte der Mängelbericht 028 besagt, dass das ursprüngliche Beispiel UB wegen der Überschneidung zwischen der Erstellung und der Verwendung der beiden Zeiger aufruft, wäre die Sache viel klarer gewesen, ohne dass "Effektive Typen" oder eine andere derartige Komplexität hinzugefügt werden müssten.

0 Stimmen

Es wäre interessant, eine Art Vorschlag zu lesen, der mehr oder weniger "das ist, was der Normenausschuss hätte tun können", der seine Ziele erreicht, ohne so viel Komplexität einzuführen.

1 Stimmen

@jrh: Ich denke, es wäre ziemlich einfach. Erkennen Sie, dass 1. damit Aliasing während einer bestimmten Ausführung einer Funktion oder Schleife auftritt, zwei verschiedene Zeiger oder L-Werte verwendet werden müssen während dieser Ausführung 2. erkennen, dass in Kontexten, in denen ein Zeiger oder l-Wert frisch sichtbar von einem anderen abgeleitet ist, ein Zugriff auf den zweiten ein Zugriff auf den ersten ist; 3. erkennen, dass die Regel nicht dazu gedacht ist, in Fällen zu gelten, in denen es nicht tatsächlich um Aliasing geht.

1 Stimmen

Die genauen Umstände, unter denen ein Compiler einen frisch abgeleiteten l-Wert erkennt, können eine Frage der Implementierungsqualität sein, aber jeder halbwegs anständige Compiler sollte in der Lage sein, Formen zu erkennen, die gcc und clang absichtlich ignorieren.

21voto

Chris Jester-Young Punkte 212385

Typenschreibweise über Pointer Casts (im Gegensatz zur Verwendung einer Union) ist ein wichtiges Beispiel für die Verletzung des strengen Aliasing.

1 Stimmen

Siehe meine Hier finden Sie die relevanten Zitate, insbesondere die Fußnoten aber type punning durch unions war in C schon immer erlaubt, auch wenn es anfangs schlecht formuliert war. Vielleicht möchten Sie Ihre Antwort präzisieren.

1 Stimmen

@ShafikYaghmour: C89 erlaubte es den Implementierern eindeutig, die Fälle auszuwählen, in denen sie Typ-Punning durch Unions sinnvollerweise anerkennen würden oder nicht. Eine Implementierung könnte zum Beispiel festlegen, dass ein Schreibvorgang in einen Typ gefolgt von einem Lesevorgang in einem anderen Typ als Typ-Punning erkannt wird, wenn der Programmierer eine der folgenden Aktionen durchführt zwischen dem Schreiben und dem Lesen (1) einen lWert auswerten, der den Unionstyp enthält [die Adresse eines Mitglieds zu nehmen, würde genügen, wenn es an der richtigen Stelle in der Sequenz geschieht]; (2) einen Zeiger auf einen Typ in einen Zeiger auf den anderen umwandeln und über diesen ptr zugreifen.

1 Stimmen

@ShafikYaghmour: Eine Implementierung könnte z.B. auch festlegen, dass Typ-Punning zwischen Integer- und Gleitkommawerten nur dann zuverlässig funktioniert, wenn der Code eine fpsync() Direktive zwischen dem Schreiben als fp und dem Lesen als int oder umgekehrt [bei Implementierungen mit getrennten Integer- und FPU-Pipelines und Caches könnte eine solche Direktive teuer sein, aber nicht so teuer, wie wenn der Compiler eine solche Synchronisation bei jedem Union-Zugriff durchführen würde]. Oder eine Implementierung könnte festlegen, dass der resultierende Wert niemals verwendbar ist, außer unter Umständen, die Common Initial Sequences verwenden.

16voto

Myst Punkte 17326

Nachdem ich viele der Antworten gelesen habe, ist es mir ein Bedürfnis, etwas hinzuzufügen:

Strenges Aliasing (das ich gleich noch beschreiben werde) ist wichtig, weil :

  1. Der Zugriff auf den Speicher kann teuer sein (in Bezug auf die Leistung), weswegen Daten werden in CPU-Registern manipuliert bevor sie in den physischen Speicher zurückgeschrieben werden.

  2. Wenn Daten in zwei verschiedenen CPU-Registern in denselben Speicherbereich geschrieben werden, wir können nicht vorhersagen, welche Daten "überleben" werden wenn wir in C programmieren.

    In Assembler, wo wir das Laden und Entladen von CPU-Registern manuell codieren, wissen wir, welche Daten intakt bleiben. Aber C abstrahiert (glücklicherweise) von diesem Detail.

Da zwei Zeiger auf dieselbe Stelle im Speicher zeigen können, könnte dies dazu führen, dass komplexer Code, der mögliche Kollisionen behandelt .

Dieser zusätzliche Code ist langsam und schadet der Leistung da es zusätzliche Lese- und Schreibvorgänge im Speicher ausführt, die sowohl langsamer als auch (möglicherweise) unnötig sind.

があります。 Strenge Aliasing-Regel ermöglicht die Vermeidung von redundantem Maschinencode in Fällen, in denen sie sollte sein sicher davon ausgehen, dass zwei Zeiger nicht auf denselben Speicherblock zeigen (siehe auch die restrict Schlüsselwort).

Das strikte Aliasing besagt, dass es sicher ist anzunehmen, dass Zeiger auf verschiedene Typen auf verschiedene Stellen im Speicher zeigen.

Wenn ein Compiler feststellt, dass zwei Zeiger auf unterschiedliche Typen zeigen (zum Beispiel ein int * und eine float * ), nimmt es an, dass die Speicheradresse anders ist und es wird nicht Schutz vor Speicheradressenkollisionen, was zu schnellerem Maschinencode führt.

Zum Beispiel :

Gehen wir von der folgenden Funktion aus:

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 die Daten aus dem Speicher in die CPU-Register laden, ordnen und testen, so dass der Code wie folgt aussehen könnte:

  1. laden a y b aus dem Gedächtnis.

  2. つける a a b .

  3. speichern b y neu laden a .

    (Speichern aus dem CPU-Register in den Speicher und Laden aus dem Speicher in das CPU-Register).

  4. つける b a a .

  5. speichern a (aus dem CPU-Register) in den Speicher.

Schritt 3 ist sehr langsam, da er auf den physischen Speicher zugreifen muss. Er ist jedoch erforderlich, um sich gegen Fälle zu schützen, in denen a y b zeigen auf dieselbe Speicheradresse.

Strenges Aliasing würde es uns ermöglichen, dies zu verhindern, indem wir dem Compiler mitteilen, dass diese Speicheradressen deutlich unterschiedlich sind (was in diesem Fall sogar eine weitere Optimierung ermöglicht, die nicht durchgeführt werden kann, wenn die Zeiger eine Speicheradresse teilen).

  1. Dies kann dem Compiler auf zwei Arten mitgeteilt werden, indem verschiedene Typen verwendet werden, auf die er verweist, z. B.:

    void merge_two_numbers(int *a, long *b) {...}
  2. Die Verwendung des restrict Schlüsselwort. d.h.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Durch die Einhaltung der Strict-Aliasing-Regel kann nun Schritt 3 vermieden werden, und der Code wird erheblich schneller ausgeführt.

In der Tat, durch das Hinzufügen der restrict Schlüsselwort, könnte die gesamte Funktion darauf optimiert werden:

  1. laden a y b aus dem Gedächtnis.

  2. つける a a b .

  3. speichern Sie das Ergebnis sowohl in a und an b .

Diese Optimierung konnte bisher nicht durchgeführt werden, da es zu Kollisionen kommen kann (wenn a y b würde verdreifacht statt verdoppelt).

0 Stimmen

Mit dem Schlüsselwort restrict in Schritt 3, sollte das Ergebnis nicht nur in 'b' gespeichert werden? Es hört sich so an, als würde das Ergebnis der Summierung auch in 'a' gespeichert werden. Muss 'b' noch einmal neu geladen werden?

1 Stimmen

@NeilB - Ja, du hast recht. Wir sparen nur b (nicht nachladen) und nachladen a . Ich hoffe, es ist jetzt klarer.

1 Stimmen

Typbasiertes Aliasing kann einige Vorteile bieten, bevor restrict Ich denke aber, dass letzteres in den meisten Fällen effektiver wäre, und die Lockerung einiger Beschränkungen für register würde es ermöglichen, einige der Fälle auszufüllen, in denen restrict würde nicht helfen. 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 Anzeichen von Aliasing erkennen, anstatt nur die Stellen zu beschreiben, an denen Compiler Aliasing vermuten müssen auch wenn es keine besonderen Beweise dafür gibt .

14voto

Jason Dagit Punkte 13354

Striktes Aliasing bedeutet, dass unterschiedliche Zeigertypen auf dieselben Daten nicht zulässig sind.

Dieser Artikel sollte Ihnen helfen, das Problem in allen Einzelheiten zu verstehen.

5 Stimmen

Sie können auch zwischen Referenzen und zwischen einer Referenz und einem Zeiger aliasieren. Siehe mein Tutorium dbp-consulting.com/lehrgänge/strictAliasing.html

5 Stimmen

Es ist zulässig, verschiedene Zeigertypen für dieselben Daten zu haben. Striktes Aliasing liegt dann vor, wenn dieselbe Speicherstelle über einen Zeigertyp geschrieben und über einen anderen gelesen wird. Außerdem sind einige unterschiedliche Typen erlaubt (z. B. int und eine Struktur, die eine int ).

-5voto

curiousguy Punkte 7697

Technisch gesehen ist die strenge Aliasing-Regel in C++ wahrscheinlich nie anwendbar.

Beachten Sie die Definition der Indirektion ( * Betreiber ):

Der unäre Operator * führt eine Umleitung durch: Der Ausdruck, auf den er angewendet wird angewendet wird, ist ein Zeiger auf einen Objekttyp oder ein Zeiger auf einen Funktionstyp und das Ergebnis ist ein l-Wert, der auf das Objekt verweist oder Funktion auf die der Ausdruck verweist .

Auch von die Definition von glvalue

Ein glvalue ist ein Ausdruck, dessen Auswertung die Identität von eines Objekts bestimmt, (...snip)

In jedem gut definierten Programmablaufplan bezieht sich ein glvalue also auf ein Objekt. Die so genannte strenge Aliasing-Regel gilt also nicht. Das ist vielleicht nicht das, was die Designer wollten.

4 Stimmen

Der C-Standard verwendet den Begriff "Objekt" für eine Reihe verschiedener Konzepte. Darunter eine Folge von Bytes, die ausschließlich einem bestimmten Zweck zugewiesen sind, ein nicht notwendigerweise ausschließlicher Verweis auf eine Folge von Bytes, in/aus der ein Wert eines bestimmten Typs sein könnte geschrieben oder gelesen wird, oder ein solcher Hinweis, dass eigentlich in irgendeinem Kontext zugegriffen wurde oder wird. Ich glaube nicht, dass es einen vernünftigen Weg gibt, den Begriff "Objekt" so zu definieren, dass er mit der gesamten Verwendung in der Norm vereinbar ist.

1 Stimmen

@supercat Falsch. Entgegen Ihrer Vorstellung ist es eigentlich ziemlich konsistent. In ISO C ist er definiert als "Bereich der Datenspeicherung in der Ausführungsumgebung, dessen Inhalt Werte darstellen kann". In ISO C++ gibt es eine ähnliche Definition. Ihr Kommentar ist sogar noch irrelevanter als die Antwort, denn alles, was Sie erwähnt haben, sind Möglichkeiten der Vertretung auf Objekte verweisen". Inhalt während die Antwort das C++-Konzept (glvalue) einer Art von Ausdrücken veranschaulicht, die eng mit dem Identität von Objekten. Und alle Aliasing-Regeln sind grundsätzlich für die Identität, nicht aber für den Inhalt relevant.

1 Stimmen

@FrankHB: Wenn man deklariert int foo; was durch den Ausdruck lvalue angesprochen wird *(char*)&foo ? Ist das ein Objekt vom Typ char ? Entsteht dieses Objekt zur gleichen Zeit wie foo ? Würde das Schreiben an foo den gespeicherten Wert des vorgenannten Objekts vom Typ char ? Wenn ja, gibt es eine Regel, die es erlaubt, den gespeicherten Wert eines Objekts vom Typ char mit einem l-Wert des Typs int ?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X