it-swarm.com.de

Was ist die strenge Aliasing-Regel?

Bei Fragen zu häufig undefiniertes Verhalten in C wird manchmal auf die strikte Aliasing-Regel verwiesen.
Worüber reden sie?

759
Benoit

Eine typische Situation, in der es zu starken Aliasing-Problemen kommt, ist das Überlagern einer Struktur (z. B. einer Geräte-/Netzwerk-Nachricht) auf einen Puffer der Word-Größe Ihres Systems (z. B. ein Zeiger auf uint32_ts oder uint16_ts). Wenn Sie eine Struktur über einen solchen Puffer oder einen Puffer über eine Zeigerumwandlung auf eine solche Struktur legen, können Sie leicht strenge Aliasing-Regeln verletzen.

Wenn ich also in einer solchen Konfiguration eine Nachricht an etwas senden möchte, müssen zwei inkompatible Zeiger auf denselben Speicherblock verweisen. Ich könnte dann naiv so etwas codieren:

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 strenge Aliasing-Regel macht dieses Setup ungültig: Die Dereferenzierung eines Zeigers, der ein Objekt aliasiert, das nicht von einem kompatiblen Typ oder einem der anderen in C 2011 6.5 Absatz 7 zulässigen Typen ist1 ist undefiniertes Verhalten. Leider können Sie immer noch auf diese Weise codieren. Vielleicht Einige Warnungen erhalten, die Kompilierung funktioniert einwandfrei, nur um seltsames unerwartetes Verhalten zu verursachen, wenn Sie den Code ausführen.

(GCC scheint etwas inkonsistent in seiner Fähigkeit zu sein, Aliasing-Warnungen zu geben, manchmal freundlich und manchmal nicht.)

Um zu sehen, warum dieses Verhalten undefiniert ist, müssen wir uns überlegen, was die strikte Aliasing-Regel dem Compiler bringt. Grundsätzlich muss bei dieser Regel nicht daran gedacht werden, Anweisungen einzufügen, um den Inhalt von buff bei jedem Durchlauf der Schleife zu aktualisieren. Stattdessen kann es bei der Optimierung mit einigen ärgerlich nicht erzwungenen Annahmen zum Aliasing diese Anweisungen weglassen, load buff[0] und buff[1] einmal in CPU-Register ein, bevor die Schleife ausgeführt wird, und beschleunigen den Hauptteil der Schleife. Bevor striktes Aliasing eingeführt wurde, musste der Compiler in einem Zustand der Paranoia leben, in dem sich der Inhalt von buff jederzeit und von jedem Ort aus ändern konnte. Um einen zusätzlichen Leistungsvorteil zu erzielen und davon auszugehen, dass die meisten Benutzer keine Tippfehler verwenden, wurde die strenge Aliasing-Regel eingeführt.

Denken Sie daran, wenn Sie der Meinung sind, dass das Beispiel erfunden wurde, kann dies sogar passieren, wenn Sie einen Puffer an eine andere Funktion übergeben, die das Senden für Sie übernimmt, wenn Sie stattdessen haben.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Und schreiben Sie unsere frühere Schleife um, 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 kann oder kann nicht in der Lage oder klug genug sein, um zu versuchen, SendMessage zu integrieren, und er kann entscheiden, Buff erneut zu laden oder nicht zu laden. Wenn SendMessage Teil einer anderen API ist, die separat kompiliert wurde, enthält sie wahrscheinlich Anweisungen zum Laden des Buff-Inhalts. Andererseits, vielleicht sind Sie in C++ und dies ist eine reine Header-Implementierung, die der Compiler für inline hält. Oder vielleicht ist es nur etwas, was Sie zu Ihrer eigenen Bequemlichkeit in Ihre .c-Datei geschrieben haben. Trotzdem kann es immer noch zu undefiniertem Verhalten kommen. Selbst wenn wir wissen, was unter der Haube passiert, verstößt es immer noch gegen die Regel, sodass kein genau definiertes Verhalten garantiert ist. Es hilft also nicht unbedingt, eine Funktion einzuschließen, die unseren durch Word getrennten Puffer benötigt.

Wie komme ich also darum herum?

  • Verwenden Sie eine Gewerkschaft. Die meisten Compiler unterstützen dies, ohne sich über striktes Aliasing zu beschweren. Dies ist in C99 erlaubt und explizit in C11 erlaubt.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Sie können striktes Aliasing in Ihrem Compiler deaktivieren ( f [no-] strict-aliasing in gcc))

  • Sie können char* für Aliasing anstelle von Word Ihres Systems. Die Regeln erlauben eine Ausnahme für char* (einschließlich signed char und unsigned char). Es wird immer angenommen, dass char* aliasiert andere Typen. Dies funktioniert jedoch nicht andersherum: Es ist nicht anzunehmen, dass Ihre Struktur einen Zeichenpuffer enthält.

Anfänger aufgepasst

Dies ist nur ein mögliches Minenfeld, wenn zwei Typen übereinander gelegt werden. Sie sollten auch etwas über Endianness , Word Alignment und den richtigen Umgang mit Alignment-Problemen durch Packstrukturen lernen.

Fußnote

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

  • einen 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 umfasst (einschließlich rekursiv eines Mitglieds einer Untergemeinschaft oder einer geschlossenen Vereinigung), oder
  • ein Zeichentyp.
545
Doug T.

Die beste Erklärung, die ich gefunden habe, ist von Mike Acton, nderstanding 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 Dereferenzierungszeiger auf Objekte unterschiedlichen Typs niemals auf denselben Speicherort verweisen (d. H. Sich gegenseitig aliasen)."

Wenn Sie also einen int* Haben, der auf einen Speicher mit einem int zeigt, und dann einen float* Auf diesen Speicher zeigen und ihn als float Sie verwenden die Regel brechen. Wenn Ihr Code dies nicht beachtet, wird der Optimierer des Compilers höchstwahrscheinlich Ihren Code beschädigen.

Die Ausnahme von der Regel ist ein char*, Der auf einen beliebigen Typ verweisen darf.

228
Niall

Dies ist die strikte Aliasing-Regel, die in Abschnitt 3.10 des C++ -Standards zu finden ist (andere Antworten liefern eine gute Erklärung, aber keine lieferte 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 Objekttyps,
  • ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der dem dynamischen Typ des Objekts entspricht,
  • ein Typ, der der mit oder ohne Vorzeichen versehene Typ ist, der einer von cv qualifizierten Version des dynamischen Typs des Objekts entspricht,
  • ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen unter seinen Mitgliedern umfasst (einschließlich rekursiv eines Mitglieds einer Untergemeinschaft oder einer geschlossenen Vereinigung),
  • ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
  • ein char oder unsigned char Art.

C++ 11 und C++ 14 Formulierung (Änderungen hervorgehoben):

Wenn ein Programm versucht, über einen gl-Wert eines anderen als eines 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 Objekttyps,
  • 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 mit oder ohne Vorzeichen versehene Typ ist, der einer von cv qualifizierten Version des dynamischen Typs des Objekts entspricht,
  • ein Aggregat- oder Vereinigungstyp, der einen der oben genannten Typen in seine Elemente oder nicht statischen Datenelemente einschließt (einschließlich rekursiv eines Element oder nicht statisches Datenelement eines Teilverbandes oder einer enthaltenen Union),
  • ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
  • ein char oder unsigned char Art.

Zwei Änderungen waren gering: GlWert anstelle von LWert und Verdeutlichung der Aggregat/Gewerkschaftsfall.

Die dritte Änderung bewirkt eine stärkere Garantie (lockert die starke Aliasing-Regel): Das neue Konzept von ähnlichen Typen , die jetzt für Alias ​​sicher sind.


Auch der Wortlaut C (C99; ISO/IEC 9899: 1999 6.5/7; der exakt gleiche Wortlaut wird in ISO/IEC 9899: 2011 §6.5 verwendet. ¶ 7):

Auf den gespeicherten Wert eines Objekts darf nur mit einem Werteausdruck eines der folgenden Typen zugegriffen werden 73) oder 88):

  • einen 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 umfasst (einschließlich rekursiv eines Mitglieds einer Untergemeinschaft oder einer geschlossenen Vereinigung), oder
  • ein Zeichentyp.

73) oder 88) Mit dieser Liste sollen die Umstände angegeben werden, unter denen ein Objekt einen Alias ​​aufweisen kann oder nicht.

131
Ben Voigt

Hinweis

Dies ist ein Auszug aus meinem "Was ist die strenge Aliasing-Regel und warum interessiert es uns?" Aufschreiben.

Was ist striktes Aliasing?

In C und C++ hat Aliasing damit zu tun, über welche Ausdruckstypen auf gespeicherte Werte zugegriffen werden darf. Sowohl in C als auch in C++ gibt der Standard an, welche Ausdruckstypen welche Typen als Alias ​​verwenden dürfen. Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln genau befolgen, daher der Begriff strenge Aliasing-Regel. Wenn wir versuchen, mit einem nicht zulässigen Typ auf einen Wert zuzugreifen, wird dieser als ndefiniertes Verhalten ( [~ # ~] ub [~ # ~] ) klassifiziert. . Sobald wir undefiniertes Verhalten haben, sind alle Wetten deaktiviert, 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 von uns für gültig gehaltenen Code beschädigt. 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 erfahren, warum es uns wichtig ist, werden wir Probleme diskutieren, die auftreten, wenn strenge Aliasing-Regeln verletzt werden, Typ-Punning, da beim Typ-Punning häufig verwendete Techniken gegen strenge Aliasing-Regeln verstoßen und wie Wortspiele korrekt eingegeben werden.

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 von uns übersehene Verstöße abgefangen werden können. 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 und stellen Sie beide Parameter so ein, dass sie auf denselben Speicherort verweisen, der in diesem Beispiel ein int ​​enthält. Beachten Sie, dass der Compiler durch reinterpret_cast angewiesen wird, den Ausdruck so zu behandeln, als ob er den durch den template-Parameter angegebenen Typ hätte. In diesem Fall soll der Ausdruck & x so behandelt werden, als hätte er den Typ float *. Wir können naiv erwarten, dass das Ergebnis des zweiten cout 0 ist, aber mit aktivierter Optimierung mit - O2 sowohl gcc als auch clang ergeben folgendes Ergebnis:

0
1

Was möglicherweise nicht erwartet wird, aber vollkommen gültig ist, da wir undefiniertes Verhalten hervorgerufen haben. Ein float ​​kann ein int ​​-Objekt nicht gültig aliasieren. Daher kann der Optimierer annehmen, dass Konstante 1 gespeichert ist, wenn die Dereferenzierung i der Rückgabewert ist, da ein Speichern durch f konnte ein int ​​Objekt nicht gültig beeinflussen. Das Einstecken des Codes in den Compiler Explorer zeigt genau das, was gerade passiert ( 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 Type-Based Alias ​​Analysis (TBAA) verwendet, geht davon aus, dass 1 zurückgegeben wird, und verschiebt den konstanten Wert direkt in das Register . eax , der den Rückgabewert enthält. TBAA verwendet die Sprachregeln darüber, welche Typen Aliasnamen dürfen, um Lasten und Speicher zu optimieren. In diesem Fall weiß TBAA, dass ein float ​​kein Alias ​​sein kann und int ​​und optimiert die Last von i .

Nun zum Regelbuch

Was genau sagt der Standard, dass wir erlaubt sind und nicht dürfen? Die Standardsprache ist nicht einfach, daher werde ich versuchen, für jedes Element Codebeispiele bereitzustellen, die die Bedeutung veranschaulichen.

Was sagt der C11-Standard?

Der C11 Standard sagt folgendes in Abschnitt 6.5 Ausdrücke Absatz 7:

Auf den gespeicherten Wert eines Objekts darf nur mit einem Wertausdruck eines der folgenden Typen zugegriffen werden: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 also , mit denen nsigned int *int * zugewiesen werden kann, obwohl dies nicht der Fall ist kompatible Typen.

- ein Typ, der der vorzeichenbehaftete oder nicht vorzeichenbehaftete 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 Vereinigungstyp, der einen der vorgenannten Typen unter seinen Mitgliedern umfasst (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer geschlossenen Vereinigung), 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] paragraph 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 cv-qualifizierte 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 mit oder ohne Vorzeichen versehene 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 in seine Elemente oder nicht statischen Datenelemente (einschließlich rekursiv eines Elements oder nicht statischen Datenelements eines Unteraggregats oder einer enthaltenen Vereinigung) einschließt,

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
}

Bemerkenswerterweise ist vorzeichenbehaftetes Zeichen nicht in der obigen Liste enthalten. Dies ist ein bemerkenswerter Unterschied zu C , in dem a Zeichentyp.

Was ist Type Punning?

Wir sind an diesem Punkt angelangt und wundern uns vielleicht, warum wir einen Alias ​​für möchten. Die Antwort lautet normalerweise Typ Wortspiel, häufig verstoßen die verwendeten Methoden gegen strenge Aliasing-Regeln.

Manchmal möchten wir das Typsystem umgehen und ein Objekt als einen anderen Typ interpretieren. Dies wird Typ punning genannt, um ein Speichersegment als einen anderen Typ zu interpretieren. Typ punning ist nützlich für Aufgaben, die auf die zugrunde liegende Darstellung eines Objekts zugreifen möchten, um es anzuzeigen, zu transportieren oder zu manipulieren. Typische Bereiche, in denen das Punning von Typen zum Einsatz kommt, sind Compiler, Serialisierung, Netzwerkcode usw.

Traditionell wurde dies erreicht, indem die Adresse des Objekts auf einen Zeiger des Typs gewandelt wurde, den wir 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, handelt es sich nicht um ein gültiges Aliasing, daher rufen wir undefiniertes Verhalten auf. Aber traditionell haben Compiler strenge Aliasing-Regeln nicht ausgenutzt und diese Art von Code hat normalerweise nur funktioniert. Entwickler haben sich leider daran gewöhnt, Dinge auf diese Weise zu tun. Eine gebräuchliche alternative Methode für das Punning von Typen ist die Verwendung von unions, die in C gültig ist, aber ndefiniertes Verhalten in C++ ( 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 sehen den Zweck von Gewerkschaften ausschließlich in der Implementierung von Variantentypen und halten die Verwendung von Gewerkschaften zum Typ-Punning für einen Missbrauch.

Wie tippen wir Pun richtig ein?

Die Standardmethode für Typ punning in C und C++ ist memcpy . Dies mag etwas umständlich erscheinen, aber der Optimierer sollte die Verwendung von memcpy für Typ punning erkennen und es wegoptimieren und ein Register zum Registrieren von move erstellen. Wenn wir zum Beispiel wissen, dass int64_t ​​dieselbe Größe hat wie double:

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 nion -Methode für type punning . Wenn wir den generierten Code untersuchen, sehen wir, dass er nur register mov ( live Compiler Explorer Example ) verwendet.

C++ 20 und bit_cast

In C++ 20 können wir bit_cast ( Implementierung im Link von Proposal verfügbar ) erhalten, was einen einfachen und sicheren Weg zur Eingabe von Wortspielen bietet im Kontext von constexpr verwendbar sein.

Das folgende Beispiel zeigt, wie Sie mit bit_cast pun a nsigned int ​​to float, ( siehe es live ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

In dem Fall, dass To und From Typen nicht die gleiche Größe haben, müssen wir eine Zwischenstruktur verwenden15. Wir verwenden eine Struktur, die ein sizeof (unsigned int) Zeichenarray enthält (setzt 4 Byte unsigned int ​​voraus), um From zu sein = type und nsigned int ​​als To type .:

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 .

Erkennen strenger Aliasing-Verstöße

Wir haben nicht viele gute Tools, um strenges Aliasing in C++ zu erkennen. 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 feststellen.

gcc mit dem Flag - fstrict-aliasing und - Wstrict-aliasing kann einige Fälle erfassen, wenn auch nicht ohne falsch positive/negative. Zum Beispiel erzeugen die folgenden Fälle eine Warnung in gcc ( 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 auffängt ( siehe es live ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Obwohl clang diese Flags zulässt, implementiert es die Warnungen anscheinend nicht.

Ein anderes Werkzeug, das uns zur Verfügung steht, ist ASan, das fehlausgerichtete Lasten und Lager aufnehmen kann. Obwohl es sich nicht direkt um strenge Aliasing-Verstöße handelt, sind sie ein häufiges Ergebnis strenger Aliasing-Verstöße. Zum Beispiel erzeugen die folgenden Fälle Laufzeitfehler, wenn sie mit clang unter Verwendung von - fsanitize = address erstellt werden.

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 C-Besetzungen mit - Wold-style-cast . Dies zwingt undefinierte Wortspiele dazu, 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 bereits behandelten Tools und tis-interpreter, einen statischen Analysator, der ein Programm für eine große Teilmenge der C-Sprache ausführlich analysiert. Bei einer gegebenen C-Version des früheren Beispiels, bei der die Verwendung von - fstrict-aliasing einen Fall verfehlt ( live sehen )

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 ist in der Lage, alle drei zu erfassen. Das folgende Beispiel ruft tis-kernal als tis-Interpreter auf (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 sich derzeit in der Entwicklung befindet. Dieses Desinfektionsprogramm fügt Typprüfungsinformationen in ein Schattenspeichersegment ein und überprüft Zugriffe, um festzustellen, ob sie gegen Aliasing-Regeln verstoßen. Das Tool sollte möglicherweise in der Lage sein, alle Aliasing-Verstöße abzufangen, hat jedoch möglicherweise einen hohen Laufzeit-Overhead.

59
Shafik Yaghmour

Striktes Aliasing bezieht sich nicht nur auf Zeiger, sondern auch auf Referenzen. Ich habe einen Artikel darüber für das Boost-Entwickler-Wiki geschrieben und er wurde so gut angenommen, dass ich ihn in eine Seite auf meiner Beratungswebsite verwandelt 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.

43
Patrick

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

Übersetzen Sie mit gcc -O2 -o check check.c. Normalerweise (mit den meisten gcc-Versionen, die ich ausprobiert habe) gibt dies ein "striktes Aliasing-Problem" aus, da der Compiler annimmt, dass "h" nicht dieselbe Adresse wie "k" in der Funktion "check" sein kann. Aus diesem Grund optimiert der Compiler das if (*h == 5) und ruft immer das printf auf.

Für diejenigen, die interessiert sind, ist hier der x64-Assembler-Code von gcc 4.6.3, der unter 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 entfernt.

34
Ingo Blackman

Typ punning über Zeiger-Casts (im Gegensatz zur Verwendung einer Union) ist ein wichtiges Beispiel für die Aufhebung des strikten Aliasing.

16

Gemäß der Begründung von C89 wollten die Autoren des Standards nicht, dass Compiler Code wie den folgenden eingeben:

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

es sollte erforderlich sein, den Wert von x zwischen der Zuweisungs- und der Rückgabeanweisung neu zu laden, um die Möglichkeit zu berücksichtigen, dass p auf x verweist, und die Zuweisung zu _*p_ könnte folglich den Wert von x ändern. Die Vorstellung, dass ein Compiler berechtigt sein sollte anzunehmen, dass es kein Aliasing geben wird in Situationen wie den oben genannten war nicht kontrovers.

Leider haben die Autoren des C89 ihre Regel so geschrieben, dass bei einer wörtlichen Lesart sogar die folgende Funktion Undefined Behaviour aufruft:

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

da ein Wert vom Typ int für den Zugriff auf ein Objekt vom Typ _struct S_ verwendet wird und int nicht zu den Typen gehört, die für den Zugriff auf einen _struct S_ verwendet werden können. Da es absurd wäre, die Verwendung von Nicht-Zeichentyp-Mitgliedern von Strukturen und Vereinigungen als undefiniertes Verhalten zu behandeln, erkennt fast jeder, dass es zumindest einige Umstände gibt, in denen ein Wert eines Typs verwendet werden kann, um auf ein Objekt eines anderen Typs zuzugreifen . Leider hat das C-Normenkomitee diese Umstände nicht definiert.

Ein Großteil des Problems ist das Ergebnis des Fehlerberichts Nr. 028, 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 ein undefiniertes Verhalten aufruft, da das Schreiben eines Gewerkschaftsmitglieds vom Typ "double" und das Lesen eines Gewerkschaftsmitglieds vom Typ "int" ein implementierungsdefiniertes Verhalten aufruft. Eine solche Argumentation ist unsinnig, bildet aber die Grundlage für die Effective Type-Regeln, 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, wäre wahrscheinlich, die Fußnote über den Zweck der Regel als normativ zu behandeln und die Regel nicht durchsetzbar zu machen, es sei denn, es handelt sich tatsächlich um widersprüchliche Zugriffe unter Verwendung von Aliasen. Gegeben so 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 von _inc_int_, da alle Zugriffe auf den Speicher, auf den über _*p_ zugegriffen wird, mit einem Wert vom Typ int erfolgen und es keinen Konflikt in test gibt, weil p wird sichtbar von einem _struct S_ abgeleitet, und bei der nächsten Verwendung von s sind alle Zugriffe auf diesen Speicher, die jemals über p erfolgen, bereits erfolgt.

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 p und dem Zugriff auf _s.x_ in der markierten 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 wegen der Überschneidung zwischen der Erstellung und Verwendung der beiden Zeiger aufgerufen hätte, hätte dies die Dinge viel klarer gemacht, ohne dass "Effektive Typen" oder eine andere solche Komplexität hinzugefügt werden müssten.

15
supercat

Nachdem ich viele der Antworten gelesen habe, habe ich das Bedürfnis, etwas hinzuzufügen:

Striktes Aliasing (was ich gleich beschreiben werde) ist wichtig, weil:

  1. Der Speicherzugriff kann teuer sein (in Bezug auf die Leistung), weshalb Daten werden in CPU-Registern bearbeitet 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", wenn wir in C codieren.

    In Assembly, wo wir das Laden und Entladen von CPU-Registern manuell codieren, wissen wir, welche Daten intakt bleiben. Aber C (zum Glück) abstrahiert dieses Detail weg.

Da zwei Zeiger auf dieselbe Stelle im Speicher verweisen können, kann dies zu komplexem Code, der mögliche Kollisionen behandelt führen.

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

Die strenge Aliasing-Regel ermöglicht es uns, redundanten Maschinencode zu vermeiden in Fällen, in denen es sicher sein sollte anzunehmen, dass zwei Zeiger nicht vorhanden sind Zeigen Sie nicht auf denselben Speicherblock (siehe auch das Schlüsselwort restrict).

Das strikte Aliasing besagt, dass Zeiger auf verschiedene Typen sicher auf verschiedene Stellen im Speicher verweisen.

Wenn ein Compiler feststellt, dass zwei Zeiger auf unterschiedliche Typen verweisen (z. B. ein int * und ein float *) wird angenommen, dass die Speicheradresse unterschiedlich ist und wird nicht vor Speicheradressenkollisionen schützen, was zu einem schnelleren Maschinencode führt.

Zum Beispiel:

Nehmen wir die 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 den gleichen Speicher), müssen wir die Art und Weise, in der wir Daten aus dem Speicher in die CPU-Register laden, anordnen und testen, damit der Code wie folgt endet:

  1. lade a und b aus dem Speicher.

  2. a zu b hinzufügen.

  3. saveb und reloada.

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

  4. b zu a hinzufügen.

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

Schritt 3 ist sehr langsam, da auf den physischen Speicher zugegriffen werden muss. Es muss jedoch vor Instanzen geschützt werden, in denen a und b auf dieselbe Speicheradresse verweisen.

Striktes 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 eine noch weitere Optimierung ermöglicht, die nicht durchgeführt werden kann, wenn die Zeiger eine gemeinsame Speicheradresse haben).

  1. Dies kann dem Compiler auf zwei Arten mitgeteilt werden, indem verschiedene Typen verwendet werden, auf die verwiesen wird. d. h .:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Verwenden Sie das Schlüsselwort restrict. d. h .:

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

Durch das Erfüllen der Regel für striktes Aliasing kann Schritt 3 vermieden werden, und der Code wird erheblich schneller ausgeführt.

Tatsächlich könnte durch Hinzufügen des Schlüsselworts restrict die gesamte Funktion optimiert werden, um:

  1. lade a und b aus dem Speicher.

  2. a zu b hinzufügen.

  3. speichern Sie das Ergebnis sowohl in a als auch in b.

Diese Optimierung konnte wegen der möglichen Kollision vorher nicht durchgeführt werden (wobei a und b verdreifacht statt verdoppelt würden).

10
Myst

Striktes Aliasing lässt nicht zu, dass verschiedene Zeigertypen auf dieselben Daten verweisen.

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

6
Jason Dagit