Frage

Bei der Frage nach häufiges undefiniertes Verhalten in C, aufgeklärtere Seelen als ich, verwiesen auf die strenge Aliasing-Regel.
Worüber reden sie?

War es hilfreich?

Lösung

Eine typische Situation, die Sie strengen Aliasing Probleme auftreten, wenn eine Struktur (wie ein Gerät / Netzwerk msg) auf einen Puffer der Wort Größe Ihres Systems (wie ein Zeiger auf uint32_ts oder uint16_ts) überlagert. Wenn Sie eine Struktur auf einen solchen Puffer überlagern, oder einen Puffer auf eine solche Struktur durch Zeiger Gießen Sie einfach strenge Aliasing Regeln verletzen kann.

So in dieser Art von Setup, wenn ich eine Nachricht an etwas schicken wollen würde ich habe zwei inkompatible Zeiger haben, auf den gleichen Teil des Speichers zeigen. Ich könnte dann naiv so etwas wie dieser Code:

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 diese Einstellung illegal: dereferencing einen Zeiger, der ein Objekt Aliase, die nicht von einem kompatibeler Typ oder einer der anderen Typen erlaubt durch C 2011 6.5 Absatz 7 1 ist nicht definiertes Verhalten. Leider kann man immer noch auf diese Weise kodieren, vielleicht einige Warnungen erhalten, hat es in Ordnung kompilieren, nur seltsam unerwartetes Verhalten zu haben, wenn Sie den Code ausführen.

(GCC scheint etwas widersprüchlich in seiner Fähigkeit, Aliasing-Warnungen zu geben, uns manchmal eine freundliche Warnung zu geben und manchmal nicht.)

Um zu sehen, warum dieses Verhalten nicht definiert ist, müssen wir darüber nachdenken, was die strenge Aliasing Regel die Compiler kauft. Grundsätzlich mit dieser Regel ist es nicht denken Anweisungen zum Einfügen des Inhalts von buff jeden Durchlauf der Schleife zu aktualisieren. Stattdessen, wenn sie mit einigen annoyingly unenforced Annahmen über Aliasing, kann es diese Anweisungen, Last buff[0] und buff[1] in CPU-Register auslassen einmal, bevor die Schleife ausgeführt wird, und beschleunigt den Körper der Schleife zu optimieren. Vor strengem Aliasing eingeführt wurde, hatte der Compiler in einem Zustand der Paranoia zu leben, dass der Inhalt von buff jederzeit von jedem Ort von jedem ändern könnte. So eine zusätzliche Leistungsvorsprung zu erhalten, und unter der Annahme, die meisten Leute geben-pun keine Zeiger, die strenge Aliasing-Regel eingeführt wurde.

Beachten Sie, wenn Sie denken, das Beispiel gekünstelt ist, könnte dies sogar passieren, wenn Sie einen Puffer an eine andere Funktion vorbei sind dabei das Senden für Sie, wenn Sie stattdessen haben.

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

Und umschrieb unsere frühere Schleife Vorteil dieser praktischen Funktion zu übernehmen

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Der Compiler sein oder nicht in der Lage oder intelligent genug, um zu versuchen, Sendmessage Inline und es kann oder auch nicht entscheiden, wieder zu laden oder nicht laden Buff. Wenn SendMessage Teil einer anderen API ist, die separat kompiliert, hat es wahrscheinlich Anweisungen buff Inhalte zu laden. Dann wieder, vielleicht sind Sie in C ++ und das ist einige Templat-Header nur Implementierung, dass der Compiler denkt, dass es inline kann. Oder vielleicht ist es nur etwas, das man für die eigene Bequemlichkeit in Ihrer C-Datei geschrieben. Auf jeden Fall könnte nicht definiertes Verhalten noch erfolgen. Auch wenn wir einige von dem, was geschieht unter der Haube wissen, es ist immer noch eine Verletzung der Regel so nicht gut definiertes Verhalten gewährleistet ist. So einfach, indem sie in einer Funktion Einwickeln, dass unser Wort begrenzt Puffer nimmt nicht unbedingt helfen.

Wie komme ich um dieses?

  • eine Vereinigung verwenden. Die meisten Compiler unterstützen dies, ohne sich um strenges Aliasing zu beklagen. Dies ist zulässig, in C99 und explizit in C11 erlaubt.

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

  • Sie können char* anstelle Ihres Systems Wort für Aliasing. Die Regeln erlauben eine Ausnahme für char* (einschließlich signed char und unsigned char). Es wird immer davon ausgegangen, dass char* Aliase anderen Typen. Dies wird jedoch nicht in die andere Richtung funktionieren: EsIst keine Annahme, dass Ihr struct einen Puffer von Zeichen Aliase.

Anfänger aufgepasst

Dies ist nur ein Potential Minenfeld, wenn zwei Typen aufeinander überlagert. //web.archive: Sie sollten auch über endianness , Wortausrichtung und wie zu behandeln Ausrichtungsprobleme durch Verpackung structs richtig.

Fußnote

1 Die Typen, die C 2011 6.5 7 einen L-Wert für den Zugriff erlaubt sind:

  • eine Art kompatibel mit der effektiven Art des Objekts,
  • eine qualifizierte Version eines Typ kompatibel mit der effektiven Art des Objekts,
  • ein Typ, der mit oder ohne Vorzeichen-Typ entsprechend dem wirksamen Typ des Objekts ist,
  • ein Typ, der mit oder ohne Vorzeichen Typ entsprechend eine qualifizierten Version der effektiven Art des Objekts ist,
  • ein Aggregat oder Union Typ, die eine der vorher erwähnten Typen unter den Mitgliedern enthält (einschließlich, rekursiv, ein Mitglied einer Unteraggregat oder enthaltenen Union) oder
  • ein Zeichentyp.

Andere Tipps

Die beste Erklärung, die ich gefunden habe, stammt von Mike Acton: Striktes Aliasing verstehen.Es konzentriert sich ein wenig auf die PS3-Entwicklung, aber das ist im Grunde nur GCC.

Aus dem Artikel:

„Strenges Aliasing ist eine Annahme des C- (oder C++-)Compilers, dass Dereferenzierungszeiger auf Objekte unterschiedlichen Typs niemals auf denselben Speicherort verweisen (d. h.einander aliasen.)“

Also grundsätzlich, wenn Sie eine haben int* zeigt auf eine Erinnerung, die eine enthält int und dann zeigst du a float* zu dieser Erinnerung und verwende sie als float Du brichst 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*, das auf jeden Typ zeigen darf.

Dies ist die strenge Aliasing Regel im Abschnitt 3.10 des C ++ 03 Standard (andere Antworten gute Erklärung liefern, aber keine vorgesehen, um die Regel selbst):

  

Wenn ein Programm versucht, den gespeicherten Wert eines Objekts durch einen L-Wert von anderen zugreifen als einer der folgenden Typen ist das Verhalten nicht definiert ist:

     
      
  • der dynamische Typ des Objekts,
  •   
  • eine cv-qualifizierte Version des dynamischen Typ des Objekts,
  •   
  • ein Typ, der mit oder ohne Vorzeichen-Typ entsprechend dem dynamischen Typ des Objekts ist,
  •   
  • ein Typ, der mit oder ohne Vorzeichen Typ ist mit einer cv-qualifizierter Version des dynamischen Typs des Objekts entspricht,
  •   
  • ein Aggregat oder Union Typ, die eine der vorher erwähnten Typen unter den Mitgliedern (einschließlich, rekursiv, ein Mitglied einer Unteraggregat oder enthaltenen Union) umfasst,
  •   
  • ein Typ, der ein (möglicherweise cv-qualifiziert) Basisklassentyp des dynamischen Typs des Objekts ist,
  •   
  • a char oder unsigned char Art.
  •   

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

  

Wenn ein Programm versucht, den gespeicherten Wert eines Objekts durch eine glvalue von anderen zugreifen als einer der folgenden Typen ist das Verhalten nicht definiert ist:

     
      
  • der dynamische Typ des Objekts,
  •   
  • eine cv-qualifizierte Version des dynamischen Typ des Objekts,
  •   
  • eine Art ähnlich (wie in 4.4 definiert) an den dynamischen Typ des Objekts,
  •   
  • ein Typ, der mit oder ohne Vorzeichen-Typ entsprechend dem dynamischen Typ des Objekts ist,
  •   
  • ein Typ, der mit oder ohne Vorzeichen Typ ist mit einer cv-qualifizierter Version des dynamischen Typs des Objekts entspricht,
  •   
  • ein Aggregat oder Union Typ, die eine der vorher erwähnten Typen unter seinen Elementen oder nicht-statische Datenelemente enthält (einschließlich, rekursiv, ein Element oder nicht-statisches Datenelement einer Unteraggregat oder enthalten Vereinigung),
  •   
  • ein Typ, der ein (möglicherweise cv-qualifiziert) Basisklassentyp des dynamischen Typs des Objekts ist,
  •   
  • a char oder unsigned char Art.
  •   

Zwei Änderungen waren klein. glvalue statt L-Wert und Klärung des Aggregats / union Fall

Die dritte Änderung macht eine stärkere Garantie (entspannt die starke Aliasing-Regel). Das neue Konzept der ähnliche Typen , die jetzt sicher alias sind


Auch das C Wortlaut (C99; ISO / IEC 9899: 1999 6.5 / 7, die genau die gleiche Formulierung in ISO / IEC 9899 verwendet wird: 2011 § 6.5 ¶7):

  

Ein Objekt hat seinen gespeicherten Wert hat nur durch einen L-Wert zugegriffen   Ausdruck, der einen der folgenden Typen 73) oder 88) hat:

     
      
  • eine Art kompatibel mit der effektiven Art des Objekts,
  •   
  • eine quali fi zierte Version eines Typ kompatibel mit der effektiven Art von   das Objekt,
  •   
  • ein Typ, der mit oder ohne Vorzeichen Typ entspricht, der ist   effektive Art des Objekts,
  •   
  • ein Typ, der mit oder ohne Vorzeichen Typ entsprechend a   quali fi zierte Version der effektiven Art des Objekts,
  •   
  • ein Aggregat oder Union-Typ, die eine der oben genannten enthält   Typen unter den Mitgliedern (einschließlich, rekursiv, ein Mitglied einer   Unteraggregat oder enthalten Vereinigung) oder
  •   
  • ein Zeichentyp.
  •   
     

73) oder 88) Die Absicht dieser Liste ist es, die Umstände, unter denen ein Objekt angeben kann oder nicht aliased werden.

Notiz

Dies ist ein Auszug aus meinem „Was ist die strenge Aliasing-Regel und warum interessiert uns das?“ 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 aliasen dürfen.Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln strikt befolgen, daher der Begriff strenge Aliasing-Regel.Wenn wir versuchen, mit einem nicht zulässigen Typ auf einen Wert zuzugreifen, wird er als klassifiziert undefiniertes Verhalten(UB).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 strikten Aliasing-Verstößen oft die erwarteten Ergebnisse, so dass die Möglichkeit besteht, dass eine zukünftige Version eines Compilers mit einer neuen Optimierung Code kaputt macht, den wir für gültig hielten.Dies ist unerwünscht und es ist ein lohnenswertes Ziel, die strengen Aliasing-Regeln zu verstehen und zu wissen, wie man deren Verletzung vermeidet.

Um besser zu verstehen, warum uns das wichtig ist, besprechen wir Probleme, die bei Verstößen gegen strenge Aliasing-Regeln auftreten, das Typ-Wortspiel, da gängige Techniken beim Typ-Wortspiel oft gegen strenge Aliasing-Regeln verstoßen, und wie man Wortspiele richtig eingibt.

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 sehen, wie wir striktes Aliasing und Catch-Verstöße vermeiden können, die wir übersehen 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* zeigt auf den Speicher, der von einem belegt ist int und das ist ein gültiges Aliasing.Der Optimierer muss davon ausgehen, dass Zuweisungen durchgehen IP könnte den von belegten Wert aktualisieren X.

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 wir nehmen ein int* und ein schweben*, in diesem Beispiel rufen wir auf foo und stellen Sie beide Parameter so ein, dass sie auf denselben Speicherort verweisen, der in diesem Beispiel eine enthält int.Beachten Sie das reinterpret_cast weist den Compiler an, den Ausdruck so zu behandeln, als ob er den durch seinen Vorlagenparameter angegebenen Typ hätte.In diesem Fall weisen wir es an, den Ausdruck zu behandeln &X als ob es Typ hätte schweben*.Wir können naiverweise das Ergebnis des zweiten erwarten cout zu sein 0 aber mit aktivierter Optimierung mit -O2 Sowohl gcc als auch clang erzeugen das folgende Ergebnis:

0
1

Das ist vielleicht nicht zu erwarten, aber vollkommen gültig, da wir undefiniertes Verhalten hervorgerufen haben.A schweben kann kein gültiger Alias ​​sein int Objekt.Daher kann der Optimierer davon ausgehen Konstante 1 beim Dereferenzieren gespeichert ich wird der Rückgabewert seit einem Speichervorgang sein F konnte einen nicht gültig beeinflussen int Objekt.Das Einfügen des Codes in den Compiler-Explorer zeigt, dass genau das 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 verwendet Typbasierte Alias-Analyse (TBAA) geht davon aus 1 wird zurückgegeben und verschiebt den konstanten Wert direkt in das Register eax welches den Rückgabewert trägt.TBAA verwendet die Sprachregeln darüber, welche Typen als Alias ​​zulässig sind, um Lade- und Speichervorgänge zu optimieren.In diesem Fall weiß TBAA, dass a schweben kann nicht alias und int und optimiert die Belastung ich.

Nun zum Regelwerk

Was genau besagt die Norm, was wir tun dürfen und was nicht?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 im Abschnitt Folgendes 6.5 Ausdrücke Absatz 7:

Der Zugriff auf den gespeicherten Wert eines Objekts darf nur durch einen L-Wert-Ausdruck erfolgen, 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 Auch das erlaubt eine Zuordnung unsigned int* Zu int* obwohl es sich nicht um kompatible Typen handelt.

– ein Typ, bei dem es sich um den signierten oder nicht signierten Typ handelt, 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 Union-Typ, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich, rekursiv, ein Mitglied eines Unteraggregats oder einer enthaltenen Union), 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 im Abschnitt [basic.lval] Absatz 11 sagt:

Wenn ein Programm versucht, über einen GL-Wert eines anderen als einem 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 Typs des Objekts,

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 ähnlich (wie in 7.5 definiert) dem dynamischen Typ des Objekts,

(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 Unionstyp, der einen der oben genannten Typen in seinen Elementen oder nicht statischen Datenmembern enthält (einschließlich rekursiv ein Element oder nicht statisches Datenmember eines Unteraggregats oder einer enthaltenen Union),

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 cv-qualifizierter) 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
}

Nichts wert signiert char nicht in der obigen Liste enthalten ist, ist dies ein bemerkenswerter Unterschied C was sagt ein Zeichentyp.

Was ist Type Punning?

Wir sind an diesem Punkt angelangt und fragen uns vielleicht, warum wir einen Alias ​​verwenden wollen?Die Antwort lautet normalerweise: Typ Wortspiel, oft verstoßen die verwendeten Methoden gegen strenge Aliasing-Regeln.

Manchmal möchten wir das Typsystem umgehen und ein Objekt als einen anderen Typ interpretieren.Das nennt man Typ Wortspiel, um ein Speichersegment als einen anderen Typ neu zu interpretieren. Geben Sie Wortspiele ein ist nützlich für Aufgaben, die Zugriff auf die zugrunde liegende Darstellung eines Objekts benötigen, um es anzuzeigen, zu transportieren oder zu bearbeiten.Typische Einsatzbereiche von Type Punning sind Compiler, Serialisierung, Netzwerkcode usw.

Traditionell wird dies dadurch erreicht, dass man die Adresse des Objekts nimmt, sie in einen Zeiger des Typs umwandelt, als den wir es uminterpretieren wollen, und dann auf den Wert zugreift, 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 hierbei nicht um ein gültiges Aliasing, daher rufen wir undefiniertes Verhalten hervor.Aber traditionell machten sich Compiler keine Vorteile aus strengen Aliasing-Regeln und diese Art von Code funktionierte normalerweise einfach; Entwickler haben sich leider daran gewöhnt, Dinge auf diese Weise zu tun.Eine gängige alternative Methode für das Typ-Wortspiel sind Unions, was in C jedoch gültig ist undefiniertes 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 betrachten den Zweck von Gewerkschaften ausschließlich in der Implementierung von Variantentypen und halten die Verwendung von Gewerkschaften für Typ-Wortspiele für einen Missbrauch.

Wie tippen wir Wortspiele richtig?

Die Standardmethode für Typ Wortspiel sowohl in C als auch in C++ ist memcpy.Das mag etwas schwerfällig erscheinen, aber der Optimierer sollte die Verwendung von erkennen memcpy für Typ Wortspiel und optimieren Sie es und generieren Sie einen Register-zu-Register-Umzug.Zum Beispiel, wenn wir es wissen int64_t hat die gleiche Größe wie doppelt:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

wir können benutzen memcpy:

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Bei einem ausreichenden Optimierungsgrad generiert jeder anständige moderne Compiler identischen Code wie den zuvor genannten reinterpret_cast Methode bzw Union Methode für Typ Wortspiel.Wenn wir den generierten Code untersuchen, stellen wir fest, dass er „just register mov“ verwendet (Live-Compiler-Explorer-Beispiel).

C++20 und bit_cast

In C++20 können wir gewinnen bit_cast (Die Implementierung ist im Link des Vorschlags verfügbar), was eine einfache und sichere Möglichkeit für Wortspiele bietet und in einem constexpr-Kontext verwendet werden kann.

Im Folgenden finden Sie ein Beispiel für die Verwendung bit_cast Wortspiel a eingeben unsigned int Zu schweben, (Sehen Sie es live):

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

Für den Fall, dass Zu Und Aus Da Typen nicht die gleiche Größe haben, müssen wir eine Zwischenstruktur 15 verwenden.Wir werden eine Struktur verwenden, die a enthält sizeof( unsigned int ) Zeichenarray (geht von 4 Byte unsigned int aus) zu sein Aus Typ und unsigned int als die Zu Typ.:

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.

Strikte Aliasing-Verstöße erkennen

Wir verfügen nicht über viele gute Tools zum Erkennen von striktem Aliasing in C++. Mit den Tools, die wir haben, können wir jedoch einige Fälle von Verstößen gegen das strikte Aliasing und einige Fälle von falsch ausgerichteten Lade- und Speichervorgängen erkennen.

gcc mit der Flagge -fstrict-aliasing Und -Wstrict-Aliasing kann einige Fälle abfangen, allerdings nicht ohne falsch positive/negative Ergebnisse.In den folgenden Fällen wird beispielsweise eine Warnung in gcc generiert (Sehen Sie es 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 dieser zusätzliche Fall nicht erfasst 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 offenbar nicht tatsächlich implementiert.

Ein weiteres Tool, das uns zur Verfügung steht, ist ASan, das falsch ausgerichtete Ladungen und Lager erkennen kann.Obwohl es sich hierbei nicht direkt um strikte Aliasing-Verstöße handelt, sind sie doch eine häufige Folge strikter Aliasing-Verstöße.In den folgenden Fällen werden beispielsweise Laufzeitfehler generiert, wenn mit clang using erstellt wird -fsanitize=Adresse

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 kein reines Tool, sondern eine Codierungspraxis. Erlauben Sie keine Umwandlungen im C-Stil.Sowohl gcc als auch clang erstellen eine Diagnose für Umwandlungen im C-Stil mit -Wold-Stil-Besetzung.Dadurch werden alle undefinierten Wortspiele dazu 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 abgedeckt und wir haben auch tis-interpreter, einen statischen Analysator, der ein Programm umfassend für eine große Teilmenge der C-Sprache analysiert.Angenommen, es wurden C-Versionen des früheren Beispiels verwendet -fstrict-aliasing übersieht einen Fall (Sehen Sie es 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. 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.

Endlich gibt es sie TySan welches sich derzeit in der Entwicklung befindet.Dieser Sanitizer fügt Informationen zur Typprüfung in einem Schattenspeichersegment hinzu 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 zu erkennen, verursacht jedoch möglicherweise einen großen Laufzeitaufwand.

Strikter Aliasing bezieht sich nicht nur auf Zeiger, es Verweise sowie betrifft, habe ich ein Papier über sie für den Boost-Entwickler Wiki und es kam so gut an, dass ich es in einer Seite auf meiner Beratungs Webseite gedreht. Es wird erläutert, vollständig, was es ist, warum es Menschen verwirrt so viel und was dagegen zu tun. strenge Aliasing White Paper . Insbesondere erklärt es, warum Gewerkschaften riskantes Verhalten für C ++ sind und warum memcpy verwenden ist die einzige fix tragbaren über beide C und C ++. Hoffe, das ist hilfreich.

Als Ergänzung zu dem, was Doug T. bereits geschrieben, hier ist ein einfacher Testfall, der vermutlich 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. In der Regel (mit den meisten gcc Versionen habe ich versucht) diese gibt „strenge Aliasing-Problem“, da der Compiler geht davon aus, dass „h“ kann nicht die gleiche Adresse wie „k“ werden in der „check“ Funktion. Aufgrund der, dass der Compiler optimiert die if (*h == 5) weg und immer ruft die printf.

Für diejenigen, die hier interessiert sind, ist der x64-Assembler-Code, produziert von gcc 4.6.3, läuft auf Ubuntu 12.04.2 für x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

So ist das, wenn die Bedingung vollständig aus dem Assembler-Code ist weg.

Typ punning über Zeiger Abgüsse (im Gegensatz zur Verwendung einer Gewerkschaft) ist ein wichtiges Beispiel für brechen strenge Aliasing.

Nach der C89 Begründung, die Autoren der Norm wollten nicht verlangen, dass Compiler gegeben Code wie:

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

sollte verpflichtet werden, den Wert des x zwischen der Zuordnung neu zu laden und Anweisung zurückzukehren, um die Möglichkeit zu ermöglichen, dass p könnte x zeigen und die Zuordnung *p folglich den Wert von x verändern könnte. Die Vorstellung, dass ein Compiler sollte berechtigt sein, zu vermuten, dass es nicht werden Aliasing in Situationen wie die oben war nicht umstritten.

Leider schreiben die Autoren der C89 ihre Herrschaft in einer Weise, dass, wenn wörtlich zu lesen, würde auch die folgende Funktion nicht definiertes Verhalten aufrufen:

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

, weil es einen L-Wert vom Typ int verwendet ein Objekt vom Typ struct S zuzugreifen und int gehört nicht zu den Typen, die verwendet werden können, ein struct S zugreifen. Weil es absurd wäre, jede Verwendung von nicht-zeichenartigen Elementen von Strukturen und Gewerkschaften als nicht definiertes Verhalten zu behandeln, erkennt fast jeder, dass es zumindest einige Situationen, in denen ein L-Wert eines Typs verwendet werden kann, um ein Objekt eines anderen Typs zuzugreifen . Leider hat die C Normenausschuss gescheitert zu definieren, was die Umstände sind.

Ein großer Teil des Problems ist ein Ergebnis der Fehlerbericht # 028, die über das Verhalten eines Programms gefragt wie:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Defect Report # 28 besagt, dass das Programm nicht definiertes Verhalten aufruft, weil die Wirkung eines Gewerkschaftsmitglied vom Typ „double“ zu schreiben und eine vom Typ „int“ ruft die Implementierung definiert Verhalten zu lesen. Eine solche Argumentation ist unsinnig, sondern bildet die Grundlage für die effektive Art Regeln, die unnötig die Sprache erschweren, während nichts, das ursprüngliche Problem zu lösen tun.

Der beste Weg, das ursprüngliche Problem zu lösen wäre wahrscheinlich die zur Behandlung von Fußnote über den Zweck der Regel als ob es normative war, und machte die Regel nicht durchsetzbar, außer in Fällen, die eigentlich widersprüchliche Zugriffe mit Aliase einbeziehen. Angesichts 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 inc_int weil alle auf den Speicher über *p zugegriffen Zugriffe wird mit einem L-Wert vom Typ int getan, und es gibt keinen Konflikt in test weil p sichtbar von einem struct S abgeleitet wird, und durch das nächste Mal s verwendet wird, alle Zugriffe auf diese Speicher, die jemals durch p gemacht werden wird bereits geschehen ist.

Wenn der Code wurde leicht verändert ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Hier gibt es ein Aliasing-Konflikt zwischen p und den Zugriff auf die markierte Linie s.x weil an diesem Punkt in der Ausführung eine andere Referenz existiert , die verwendet wird, um den gleichen Speicher zugreifen .

028 Defect Bericht hat gesagt, das ursprüngliche Beispiel UB aufgerufen wegen der Überlappung zwischen der Erstellung und Verwendung der beiden Zeiger, die Dinge gemacht hätte viel mehr klar, ohne „Effektives Typen“ oder andere derartige Komplexität zu müssen.

Nach vielen der Antworten zu lesen, habe ich das Bedürfnis, etwas hinzuzufügen:

Striktes Aliasing (was ich in einem wenig beschreiben würde) ist wichtig, weil :

  1. Der Speicherzugriff kann teuer sein (Leistung klug), weshalb Daten in CPU-Register manipuliert wird, bevor er sich wieder auf den physikalischen Speicher geschrieben werden.

  2. Wenn Daten in zwei verschiedenen CPU-Register wird auf den gleichen Speicherplatz geschrieben werden, können wir nicht, welche Daten "überleben" , wenn wir Code in C vorhersagen.

    Bei der Montage, wo wir das Be- und Entladen von CPU-Register manuell codieren, werden wir wissen, welche Daten intakt bleibt. Aber C (zum Glück) abstrahiert dieses Detail weg.

Da zwei Zeiger auf die gleiche Stelle im Speicher verweisen können, könnte dies zur Folge hat in komplexem Code, mögliche Kollisionen Griffe .

Dieser zusätzliche Code ist langsam und schmerzt Leistung , da es zusätzlichen Speicher-Lese / Schreib-Operationen durchführt, die sowohl langsamer und (möglicherweise) nicht erforderlich.

Die strenge Aliasing-Regel erlaubt es uns, redundanten Maschinencode in den Fällen zu vermeiden, in denen es sollte sicher davon ausgehen, dass zwei Zeiger weisen nicht auf den gleichen Speicherblock (siehe auch restrict Schlüsselwort).

Das strenge Aliasing heißt, es ist sicher anzunehmen, dass Verweise auf verschiedene Arten an verschiedenen Orten im Speicher verweisen.

Wenn ein Compiler bemerkt, dass zwei Zeiger auf verschiedene Arten zeigen (zum Beispiel ein int * und ein float *), werden sie übernehmen die Speicheradresse ist anders und es nicht zum Schutz vor Speicheradresse Kollisionen, was zu einer schnelleren Maschinencode.

Beispiel: :

Hier können Sie die folgende Funktion übernehmen:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Um den Fall, dass a == b (beiden Zeiger auf demselben Speicher) zu handhaben, müssen wir die Art und Weise bestellen und testen wir Daten aus dem Speicher in das CPU-Register zu laden, so dass der Code könnte am Ende wie folgt:

  1. Last a und b aus dem Speicher.

  2. hinzufügen a b.

  3. Speichern b und reload a.

    (Speichern von CPU-Register in den Speicher und die Last aus dem Speicher der CPU-Register).

  4. hinzufügen b a.

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

Schritt 3 ist sehr langsam, weil es den physischen Speicher zugreifen muss. Allerdings ist es erforderlich, gegen Instanzen zu schützen, wo a und b auf die gleiche Speicheradresse.

Strikte Aliasing würde uns erlauben, dies zu verhindern, indem sie den Compiler zu sagen, dass diese Speicheradressen deutlich unterschiedlich sind (was in diesem Fall wird auch eine weitere Optimierung ermöglichen, die durchgeführt werden können, wenn die Zeiger eine Speicheradresse teilen).

  1. Dies kann auf zwei Arten an den Compiler zu sagen, durch verschiedene Arten verwendet, um zu zeigen. das heißt:.

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Mit dem restrict Schlüsselwort. das heißt:.

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

Nun, durch die strenge Aliasing-Regel erfüllt, 3 Schritt vermieden werden kann und der Code läuft deutlich schneller.

In der Tat, durch das restrict Schlüsselwort hinzufügen, könnte die ganze Funktion optimiert werden:

  1. Last a und b aus dem Speicher.

  2. hinzufügen a b.

  3. speichern führen sowohl a und b.

kann diese Optimierung nicht vorher gemacht worden ist, wegen der möglichen Kollision (wo a und b würden statt verdoppelt verdreifacht werden).

Striktes Aliasing ermöglicht es nicht, verschiedene Zeigertypen auf die gleichen Daten.

Dieser Artikel sollten Sie helfen zu verstehen, die Problem in allen Einzelheiten.

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

Beachten Sie die Definition von Dereferenzierung ( * Operator ):

  

Der unäre Operator * führt indirection: den Ausdruck, auf die sie   wird angelegt, um einen Zeiger auf einen Objekttyp, oder ein Zeiger auf eine WIRD   Funktionstyp und das Ergebnis wird ein L-Wert mit Bezug auf das Objekt oder   Funktion , auf die die Expression Punkte .

Auch von der Definition von glvalue

  

Ein glvalue ist ein Ausdruck, dessen Auswertung bestimmt die Identität der   ein Objekt, (... schnipp)

So in jedem gut definierten Programm-Trace bezieht sich ein glvalue auf ein Objekt. So ist die so genannte strenge Aliasing-Regel gilt nicht, niemals. Das kann nicht sein, was die Designer wollten.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top