Was ist die strenge Aliasing-Regel?


804

Wenn nach häufigem undefiniertem Verhalten in C gefragt wird , wird manchmal auf die strenge Aliasing-Regel verwiesen.
Worüber reden sie?


12
@ Ben Voigt: Die Aliasing-Regeln unterscheiden sich für c ++ und c. Warum ist diese Frage mit dem Tag cund c++faq.
MikeMB

6
@MikeMB: Wenn Sie den Verlauf überprüfen, werden Sie feststellen, dass ich die Tags so beibehalten habe, wie sie ursprünglich waren, obwohl einige andere Experten versucht haben, die Frage unter den vorhandenen Antworten zu ändern. Außerdem ist die Sprachabhängigkeit und Versionsabhängigkeit ein sehr wichtiger Teil der Antwort auf "Was ist die strenge Aliasing-Regel?" Das Wissen um die Unterschiede ist wichtig für Teams, die Code zwischen C und C ++ migrieren oder Makros für beide verwenden.
Ben Voigt

6
@ Ben Voigt: Eigentlich beziehen sich die meisten Antworten - soweit ich das beurteilen kann - nur auf c und nicht auf c ++. Auch der Wortlaut der Frage weist auf einen Fokus auf die C-Regeln hin (oder das OP war sich einfach nicht bewusst, dass es einen Unterschied gibt ). Die Regeln und die allgemeine Idee sind natürlich größtenteils dieselben, aber insbesondere bei Gewerkschaften gelten die Antworten nicht für c ++. Ich mache mir ein wenig Sorgen, dass einige C ++ - Programmierer nach der strengen Aliasing-Regel suchen und einfach davon ausgehen, dass alles, was hier angegeben ist, auch für C ++ gilt.
MikeMB

Andererseits stimme ich zu, dass es problematisch ist, die Frage zu ändern, nachdem viele gute Antworten veröffentlicht wurden und das Problem ohnehin geringfügig ist.
MikeMB

1
@MikeMB: Ich denke, Sie werden sehen, dass der C-Fokus auf die akzeptierte Antwort, was sie für C ++ falsch macht, von einem Drittanbieter bearbeitet wurde. Dieser Teil sollte wahrscheinlich noch einmal überarbeitet werden.
Ben Voigt

Antworten:


562

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_ts oder uint16_ts) 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 buffjedes 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 buffvon 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 SendMessagees 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 charund 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.

Fußnote

1 Die Typen, auf die C 2011 6.5 7 einen l-Wert zulässt, sind:

  • 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.

16
Ich komme nach dem Kampf, wie es scheint. Kann stattdessen unsigned char*weit char*verwendet werden? Ich neige dazu, unsigned chareher als charden zugrunde liegenden Typ zu verwenden, byteweil meine Bytes nicht signiert sind und ich nicht die Verrücktheit des signierten Verhaltens (insbesondere zum Überlaufen) möchte
Matthieu M.

30
@Matthieu: Signedness macht keinen Unterschied zu Alias-Regeln, daher ist die Verwendung unsigned char *in Ordnung.
Thomas Eding

22
Ist es nicht undefiniertes Verhalten, von einem Gewerkschaftsmitglied zu lesen, das sich von dem letzten unterscheidet, an das geschrieben wurde?
R. Martinho Fernandes

23
Blödsinn, diese Antwort ist völlig rückwärts . Das Beispiel, das als illegal angezeigt wird, ist tatsächlich legal, und das Beispiel, das als legal angezeigt wird, ist tatsächlich illegal.
R. Martinho Fernandes

7
Ihre 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 mallocAnruf 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 ...
unsinnig

233

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, intund dann float*auf diesen Speicher zeigen und ihn verwenden, floatbrechen 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.


6
Was ist also die kanonische Methode, um denselben Speicher mit Variablen von zwei verschiedenen Typen legal zu verwenden? oder kopiert jeder nur?
Jiggunjer

4
Mike Actons Seite ist fehlerhaft. Zumindest der Teil von "Casting through a union (2)" ist geradezu falsch; Der Code, den er für legal hält, ist nicht legal.
Davmac

11
@davmac: Die Autoren von C89 hatten nie vor, Programmierer dazu zu zwingen, durch Reifen zu springen. Ich finde die Vorstellung, dass eine Regel, die nur zum Zweck der Optimierung existiert, so interpretiert werden sollte, dass Programmierer Code schreiben müssen, der Daten redundant kopiert, in der Hoffnung, dass ein Optimierer den redundanten Code entfernt.
Supercat

1
@curiousguy: "Kann keine Gewerkschaften haben"? Erstens hängt der ursprüngliche / primäre Zweck der Gewerkschaften in keiner Weise mit Aliasing zusammen. Zweitens erlaubt die moderne Sprachspezifikation ausdrücklich die Verwendung von Gewerkschaften für Aliasing. Der Compiler muss beachten, dass eine Union verwendet wird, und die Situation auf besondere Weise behandeln.
Am

5
@curiousguy: Falsch. Erstens war die ursprüngliche konzeptionelle Idee hinter Gewerkschaften, dass zu jedem Zeitpunkt nur ein Mitgliedsobjekt in dem gegebenen Gewerkschaftsobjekt "aktiv" ist, während die anderen einfach nicht existieren. Es gibt also keine "unterschiedlichen Objekte an derselben Adresse", wie Sie zu glauben scheinen. Zweitens geht es bei Aliasing-Verstößen, von denen alle sprechen, darum, auf ein Objekt als anderes Objekt zuzugreifen , und nicht nur darum , zwei Objekte mit derselben Adresse zu haben. Solange es keinen Typ-Punning- Zugriff gibt, gibt es kein Problem. Das war die ursprüngliche Idee. Später war es erlaubt, durch Gewerkschaften zu tippen.
AnT

133

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 charoder unsigned charTyp.

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 charoder unsigned charTyp.

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.


7
Ben, da die Leute hier oft Regie führen, habe ich mir erlaubt, der Vollständigkeit halber auch den Verweis auf den C-Standard hinzuzufügen.
Kos

1
Schauen Sie sich die C89-Begründung cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf Abschnitt 3.3 an, in dem es darum geht.
Phorgan1

2
Wenn man einen Wert eines Strukturtyps hat, die Adresse eines Mitglieds nimmt und diese an eine Funktion übergibt, die sie als Zeiger auf den Mitgliedstyp verwendet, würde dies als Zugriff auf ein Objekt des Mitgliedstyps (legal) angesehen. oder ein Objekt vom Strukturtyp (verboten)? Eine Menge von Code geht davon aus, es zu Zugriffsstrukturen in einer solchen Art und Weise legal ist, und ich denke , eine Menge Leute in der Regel kreischen würde , die als Verbot solcher Handlungen verstanden wurde, aber es ist unklar , was die genauen Regeln sind. Darüber hinaus werden Gewerkschaften und Strukturen gleich behandelt, aber vernünftige Regeln für jede sollten unterschiedlich sein.
Supercat

2
@supercat: So wie die Regel für Strukturen formuliert ist, erfolgt der tatsächliche Zugriff immer auf den primitiven Typ. Der Zugriff über einen Verweis auf den primitiven Typ ist dann zulässig, da die Typen übereinstimmen, und der Zugriff über einen Verweis auf den enthaltenen Strukturtyp ist zulässig, da dies speziell zulässig ist.
Ben Voigt

2
@ BenVoigt: Ich denke nicht, dass die übliche Anfangssequenz funktioniert, es sei denn, die Zugriffe erfolgen über die Union. Sehen Sie unter goo.gl/HGOyoK nach, was gcc tut. Wenn der Zugriff auf einen lWert vom Typ Union über einen lWert eines Mitgliedstyps (ohne Verwendung des Operators für den Zugriff auf Gewerkschaftsmitglieder) legal 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.
Supercat

81

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 ):

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 .

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 :

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.

Was der C ++ 17 Draft Standard sagt

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 .

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:

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.

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 :

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 ).

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 ):

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 .

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 ):

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.


Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Bhargav Rao

3
Wenn ich, +10, gut geschrieben und erklärt könnte, auch von beiden Seiten, Compiler-Autoren und Programmierer ... die einzige Kritik: Es wäre schön, oben Gegenbeispiele zu haben, um zu sehen, was durch den Standard verboten ist, es ist nicht offensichtlich Art von :-)
Gabriel

2
Sehr gute Antwort. Ich bedaure nur, dass die ersten Beispiele in C ++ gegeben sind, was es für Leute wie mich schwierig macht, zu folgen, die nur C kennen oder sich um C kümmern und keine Ahnung haben, was reinterpret_castzu tun ist oder was coutzu 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.)
Gro-Tsen

In Bezug auf Typ-Puning: Wenn ich also ein Array vom Typ X in eine Datei schreibe und dann aus dieser Datei dieses Array in den mit void * gezeigten Speicher lese, dann setze ich diesen Zeiger auf den tatsächlichen Datentyp, um ihn zu verwenden - das ist undefiniertes Verhalten?
Michael IV

44

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.


3
" Striktes Aliasing bezieht sich nicht nur auf Zeiger, sondern auch auf Referenzen. " Tatsächlich bezieht es sich auf l-Werte . "Die Verwendung von memcpy ist das einzige tragbare Update " Hear!
Neugieriger

5
Gutes Papier. Meine Meinung: (1) Dieses Aliasing-Problem ist eine Überreaktion auf schlechte Programmierung - der Versuch, den schlechten Programmierer vor seinen schlechten Gewohnheiten zu schützen. Wenn der Programmierer gute Gewohnheiten hat, ist dieses Aliasing nur ein Ärgernis und die Überprüfungen können sicher deaktiviert werden. (2) Die compilerseitige Optimierung sollte nur in bekannten Fällen durchgeführt werden und im Zweifelsfall strikt dem Quellcode folgen. Es ist einfach falsch, den Programmierer zu zwingen, Code zu schreiben, um den Eigenheiten des Compilers Rechnung zu tragen. Noch schlimmer, es zum Standard zu machen.
Slashmais

4
@slashmais (1) " ist eine Überreaktion auf schlechte Programmierung " Unsinn. Es ist eine Ablehnung der schlechten Gewohnheiten. Du machst das? Sie zahlen den Preis: keine Garantie für Sie! (2) Bekannte Fälle? Welche? Die strenge Aliasing-Regel sollte "bekannt" sein!
Neugieriger

5
@curiousguy: Nachdem einige Verwirrungspunkte beseitigt wurden, ist klar, dass die C-Sprache mit den Aliasing-Regeln es Programmen unmöglich macht, typunabhängige Speicherpools zu implementieren. Einige Arten von Programmen können mit malloc / free auskommen, andere benötigen eine Speicherverwaltungslogik, die besser auf die jeweiligen Aufgaben zugeschnitten ist. Ich frage mich, warum die C89-Begründung ein so mieses Beispiel für den Grund für die Aliasing-Regel verwendet hat, da ihr Beispiel den Anschein erweckt, dass die Regel keine großen Schwierigkeiten bei der Ausführung einer vernünftigen Aufgabe darstellt.
Superkatze

5
@curiousguy, die meisten Compiler-Suiten enthalten standardmäßig -fstrict-Aliasing für -O3, und dieser versteckte Vertrag wird den Benutzern aufgezwungen, die noch nie von der TBAA gehört und Code geschrieben haben, wie es ein Systemprogrammierer tun könnte. Ich möchte für Systemprogrammierer nicht unaufrichtig klingen, aber diese Art der Optimierung sollte außerhalb der Standardoption von -O3 bleiben und eine Opt-In-Optimierung für diejenigen sein, die wissen, was TBAA ist. Es macht keinen Spaß, sich den Compiler-Fehler anzusehen, der sich als Benutzercode herausstellt, der gegen TBAA verstößt, insbesondere um die Verletzung der Quellenebene im Benutzercode aufzuspüren.
Kchoi

34

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.


Wenn Sie ein zweites kurzes * j hinzufügen, um () zu überprüfen und zu verwenden (* j = 7), verschwindet die Optimierung, da ggc dies nicht tut, wenn h und j nicht tatsächlich auf denselben Wert zeigen. Ja, Optimierung ist wirklich klug.
Philip Lhardy

2
Verwenden Sie Zeiger auf Typen, die nicht kompatibel sind, aber dieselbe Größe und Darstellung haben (auf einigen Systemen gilt dies für eg 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.
Supercat

Grr ... x64 ist eine Microsoft-Konvention. Verwenden Sie stattdessen amd64 oder x86_64.
SS Anne

Grr ... x64 ist eine Microsoft-Konvention. Verwenden Sie stattdessen amd64 oder x86_64.
SS Anne

17

Typ-Punning über Zeiger-Casts (im Gegensatz zur Verwendung einer Union) ist ein wichtiges Beispiel dafür, wie striktes Aliasing gebrochen wird.


1
Siehe meine Antwort hier für die relevanten Zitate, insbesondere die Fußnoten, aber das Schreiben von Texten durch Gewerkschaften war in C immer erlaubt, obwohl es anfangs schlecht formuliert war. Sie möchten meine Antwort klarstellen.
Shafik Yaghmour

@ShafikYaghmour: Mit C89 konnten Implementierer eindeutig die Fälle auswählen, in denen sie Typ-Punning durch Gewerkschaften sinnvoll erkennen würden oder nicht. Eine Implementierung könnte zum Beispiel spezifizieren, dass für ein Schreiben in einen Typ, gefolgt von einem Lesen eines anderen, als Typ-Punning erkannt wird, wenn der Programmierer zwischen dem Schreiben und dem Lesen eine der folgenden Aktionen ausführt: (1) Auswerten eines enthaltenden l-Werts der Gewerkschaftstyp [die Anschrift eines Mitglieds würde sich qualifizieren, wenn dies an der richtigen Stelle in der Sequenz erfolgt]; (2) Konvertieren eines Zeigers auf einen Typ in einen Zeiger auf den anderen und Zugriff über diesen ptr.
Supercat

@ShafikYaghmour: Eine Implementierung könnte auch angeben, dass ein Typ, der zwischen Ganzzahl- und Gleitkommawerten punktet, nur dann zuverlässig funktioniert, wenn Code eine 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.
Supercat

@ShafikYaghmour: Unter C89, Implementierungen könnten die meisten Formen des Typ punning verbieten, über Gewerkschaften , einschließlich, aber die Äquivalenz zwischen Zeigern auf Gewerkschaften und Zeiger auf ihre Mitglieder angedeutet , dass Typ punning in Implementierungen erlaubt wurde , die nicht ausdrücklich es verbieten.
Supercat

17

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 xzwischen der Zuweisungs- und der return-Anweisung neu zu laden , um die Möglichkeit zu berücksichtigen, auf die pmöglicherweise hingewiesen wird x, und die Zuweisung zu *pkann 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, intum auf ein Objekt vom Typ zuzugreifen struct S, und intnicht 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_intda alle Zugriffe auf den Speicher, auf den über zugegriffen *pwird, mit einem Wert vom Typ l erfolgen int, und es gibt keinen Konflikt darin, testweil alle Zugriffe auf diesen Speicher, der jemals erfolgen pwird, sichtbar von a abgeleitet struct Ssind und bei der nächsten sVerwendung verwendet werden durch pwird 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 pund dem Zugriff s.xauf 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.


Gut ausgedrückt, es wäre interessant, eine Art Vorschlag zu lesen, der mehr oder weniger "das war, was das Normungsgremium hätte tun können", um seine Ziele zu erreichen, ohne so viel Komplexität einzuführen.
jrh

1
@jrh: Ich denke es wäre ziemlich einfach. Beachten Sie Folgendes: 1. Damit während einer bestimmten Ausführung einer Funktion oder Schleife ein Aliasing auftritt, müssen während dieser Ausführung zwei verschiedene Zeiger oder Werte verwendet werden , um denselben Speicher in widersprüchlicher Mode zu adressieren. 2. Erkennen Sie, dass in Kontexten, in denen ein Zeiger oder Wert frisch sichtbar von einem anderen abgeleitet ist, ein Zugriff auf den zweiten ein Zugriff auf den ersten ist; 3. Erkennen Sie, dass die Regel nicht in Fällen gelten soll, in denen kein Aliasing erforderlich ist.
Supercat

1
Die genauen Umstände, unter denen ein Compiler einen frisch abgeleiteten Wert erkennt, können ein Problem mit der Implementierungsqualität sein, aber jeder entfernt anständige Compiler sollte in der Lage sein, Formulare zu erkennen, die gcc und clang absichtlich ignorieren.
Supercat

11

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 :

  1. 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.

  2. 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 restrictStichwort).

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:

  1. laden aund baus dem Speicher.

  2. hinzufügen azu b.

  3. 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).

  4. hinzufügen bzu a.

  5. 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 aund bauf 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).

  1. 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) {...}
  2. Verwenden Sie das restrictSchlü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 restrictSchlüsselworts die gesamte Funktion optimiert werden, um:

  1. laden aund baus dem Speicher.

  2. hinzufügen azu b.

  3. Ergebnis sowohl an aals auch an speichern b.

Diese Optimierung hätte aufgrund der möglichen Kollision (wo aund bwürde verdreifacht statt verdoppelt) vorher nicht durchgeführt werden können .


Sollte das Ergebnis mit dem Schlüsselwort "Einschränken" in Schritt 3 nicht nur auf "b" gespeichert werden? Es hört sich so an, als würde das Ergebnis der Summierung auch in 'a' gespeichert. Muss es 'b' erneut geladen werden?
NeilB

1
@NeilB - Ja, du hast recht. Wir speichern nur b(nicht neu laden) und laden neu a. Ich hoffe es ist jetzt klarer.
Myst

Typbasiertes Aliasing mag zuvor einige Vorteile geboten haben restrict, aber ich würde denken, dass letzteres in den meisten Fällen effektiver wäre, und eine Lockerung einiger Einschränkungen registerwürde es ermöglichen, einige der Fälle auszufüllen, in denen restrictdies 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 .
Supercat

Beachten Sie, dass das Laden aus dem Hauptspeicher zwar sehr langsam ist (und den CPU-Kern für eine lange Zeit blockieren kann, wenn die folgenden Vorgänge vom Ergebnis abhängen), das Laden aus dem L1-Cache jedoch ziemlich schnell ist, ebenso wie das Schreiben in eine Cache-Zeile, die kürzlich geschrieben wurde durch den gleichen Kern. Alle außer dem ersten Lesen oder Schreiben an eine Adresse sind normalerweise relativ schnell: Der Unterschied zwischen dem Zugriff auf reg / mem addr ist kleiner als der Unterschied zwischen zwischengespeicherten und nicht zwischengespeicherten mem addr.
Neugieriger

@curiousguy - obwohl du richtig bist, ist "schnell" in diesem Fall relativ. Der L1-Cache ist wahrscheinlich immer noch eine Größenordnung langsamer als die CPU-Register (ich denke mehr als zehnmal langsamer). Darüber hinaus restrictminimiert 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 :)
Myst

6

Durch striktes Aliasing können keine unterschiedlichen Zeigertypen auf dieselben Daten zugelassen werden.

Dieser Artikel soll Ihnen helfen, das Problem im Detail zu verstehen.


4
Sie können auch zwischen Referenzen und zwischen einer Referenz und einem Zeiger einen Alias ​​erstellen. Siehe mein Tutorial dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
Es ist zulässig, unterschiedliche Zeigertypen auf dieselben Daten zu haben. Ein striktes Aliasing tritt auf, wenn derselbe Speicherort durch einen Zeigertyp geschrieben und durch einen anderen gelesen wird. Es sind auch einige verschiedene Typen zulässig (z. B. inteine Struktur, die eine enthält int).
MM

-3

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.


4
Der C-Standard verwendet den Begriff "Objekt", um sich auf eine Reihe verschiedener Konzepte zu beziehen. Unter ihnen ist eine Folge von Bytes , die ausschließlich zu einem bestimmten Zweck zugeordnet sind, eine nicht-notwendigerweise ausschließliche Bezugnahme auf eine Folge von Bytes zu / von dem ein Wert eines bestimmten Typs werden könnten geschrieben oder gelesen, oder einen Hinweis , dass tatsächlich hat wurde oder wird in einem bestimmten Kontext zugegriffen. Ich glaube nicht, dass es einen vernünftigen Weg gibt, den Begriff "Objekt" zu definieren, der mit der Art und Weise übereinstimmt, wie der Standard ihn verwendet.
Supercat

@supercat Falsch. Trotz Ihrer Vorstellungskraft ist es tatsächlich ziemlich konsistent. In ISO C ist es 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 noch irrelevanter als die Antwort, da alles, was Sie erwähnt haben, Darstellungsweisen sind, um auf den Inhalt von Objekten zu verweisen , während die Antwort das C ++ - Konzept (glvalue) einer Art von Ausdrücken veranschaulicht, die eng mit der Identität von Objekten zusammenhängen. Und alle Aliasing-Regeln sind grundsätzlich für die Identität relevant, nicht aber für den Inhalt.
FrankHB

1
@FrankHB: Wenn man deklariert 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 fooden 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 charmit einem Wert vom Typ zugegriffen werden kann int?
Supercat

@FrankHB: In Abwesenheit von 6.5p7 könnte man einfach sagen, dass jeder Speicherbereich gleichzeitig alle Objekte jedes Typs enthält, die in diesen Speicherbereich passen könnten, und dass der Zugriff auf diesen Speicherbereich gleichzeitig auf alle von ihnen zugreift. Eine solche Interpretation der Verwendung des Begriffs "Objekt" in 6.5p7 würde jedoch viel von allem verbieten, was nicht mit lichtartigen Werten zu tun hat, was eindeutig ein absurdes Ergebnis wäre und den Zweck der Regel völlig zunichte macht. Ferner hat das Konzept des "Objekts", das überall außer 6.5p6 verwendet wird, einen statischen Typ zur Kompilierungszeit, aber ...
Supercat

1
sizeof (int) ist 4, erstellt die Deklaration 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 volatilequalifizierten Zeiger erlaubt , auf Hardwareregister zuzugreifen, die nicht der Definition von "Objekt" entsprechen.
Supercat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.