Qual è la regola di aliasing rigorosa?


804

Quando si chiede un comportamento indefinito comune in C , le persone a volte fanno riferimento alla rigida regola di aliasing.
Di cosa stanno parlando?


12
@Ben Voigt: le regole di aliasing sono diverse per c ++ e c. Perché questa domanda è taggata con ce c++faq.
MikeMB,

6
@MikeMB: se controlli la cronologia, vedrai che ho mantenuto i tag come erano originariamente, nonostante il tentativo di altri esperti di cambiare la domanda da sotto le risposte esistenti. Inoltre, la dipendenza dalla lingua e la dipendenza dalla versione è una parte molto importante della risposta a "Qual è la regola di aliasing rigorosa?" e conoscere le differenze è importante per i team che migrano il codice tra C e C ++ o scrivono macro da usare in entrambi.
Ben Voigt,

6
@Ben Voigt: In realtà - per quanto ne so - la maggior parte delle risposte si riferisce solo a c e non a c ++ anche la formulazione della domanda indica un focus sulle regole C (o l'OP non era a conoscenza, che c'è una differenza ). Nella maggior parte dei casi, le regole e l'Idea generale sono le stesse, ma soprattutto, per quanto riguarda i sindacati, le risposte non si applicano al c ++. Sono un po 'preoccupato che alcuni programmatori di c ++ cercheranno la rigida regola di aliasing e presumeranno che tutto quanto indicato qui si applichi anche a c ++.
MikeMB,

D'altra parte, sono d'accordo che è problematico cambiare la domanda dopo che sono state pubblicate molte buone risposte e il problema è comunque minore.
MikeMB,

1
@MikeMB: penso che vedrai che l'attenzione di C sulla risposta accettata, rendendola errata per C ++, è stata modificata da una terza parte. Quella parte probabilmente dovrebbe essere rivista di nuovo.
Ben Voigt,

Risposte:


562

Una situazione tipica in cui si verificano problemi di aliasing rigoroso è quando si sovrappone uno struct (come un dispositivo / network msg) su un buffer delle dimensioni della parola del proprio sistema (come un puntatore a uint32_ts o uint16_ts). Quando si sovrappone una struttura a un tale buffer o un buffer a una tale struttura tramite il cast di puntatori, è possibile violare facilmente le rigide regole di aliasing.

Quindi, in questo tipo di installazione, se voglio inviare un messaggio a qualcosa, dovrei avere due puntatori incompatibili che puntano allo stesso pezzo di memoria. Potrei quindi ingenuamente codificare qualcosa del genere (su un sistema con 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]);   
    }
}

La rigorosa regola di aliasing rende illegale questa configurazione: la dereferenziazione di un puntatore che alias un oggetto non di tipo compatibile o uno degli altri tipi consentiti da C 2011 6.5 paragrafo 7 1 è un comportamento indefinito. Sfortunatamente, puoi ancora codificare in questo modo, forse ricevere alcuni avvisi, farlo compilare bene, solo per avere strani comportamenti inattesi quando esegui il codice.

(GCC appare in qualche modo incoerente nella sua capacità di fornire avvisi di aliasing, a volte dandoci un avviso amichevole e talvolta no.)

Per capire perché questo comportamento non è definito, dobbiamo pensare a ciò che la rigida regola di alias acquista il compilatore. Fondamentalmente, con questa regola, non deve pensare di inserire istruzioni per aggiornare il contenuto di buffogni esecuzione del ciclo. Invece, durante l'ottimizzazione, con alcune assunzioni fastidiosamente non forzate sull'aliasing, può omettere quelle istruzioni, caricare buff[0]e buff[1] nei registri della CPU una volta prima che il ciclo venga eseguito e accelerare il corpo del ciclo. Prima che fosse introdotto il rigoroso alias, il compilatore doveva vivere in uno stato di paranoia che i contenuti buffpotevano cambiare in qualsiasi momento e dovunque da chiunque. Quindi, per ottenere un vantaggio in più in termini di prestazioni, e supponendo che la maggior parte delle persone non digiti puntatori di tipo pun, è stata introdotta la rigida regola di aliasing.

Tieni presente che se pensi che l'esempio sia inventato, ciò potrebbe accadere anche se stai passando un buffer a un'altra funzione che esegue l'invio per te, se invece lo hai fatto.

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

E riscritto il nostro ciclo precedente per sfruttare questa comoda funzione

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

Il compilatore può o meno essere in grado o abbastanza intelligente da provare a incorporare SendMessage e può o meno decidere di caricare o non caricare nuovamente buff. Se SendMessagefa parte di un'altra API compilata separatamente, probabilmente ha istruzioni per caricare i contenuti di buff. Poi di nuovo, forse sei in C ++ e questa è un'implementazione basata solo su un'intestazione basata su modelli che il compilatore pensa di poter incorporare. O forse è solo qualcosa che hai scritto nel tuo file .c per tua comodità. In ogni caso potrebbe comunque derivarne un comportamento indefinito. Anche quando sappiamo qualcosa di ciò che accade sotto il cofano, è ancora una violazione della regola, quindi non è garantito alcun comportamento ben definito. Quindi, semplicemente racchiudendo una funzione che accetta il nostro buffer delimitato da parole non aiuta necessariamente.

Quindi, come posso aggirare questo?

  • Usa un'unione. Molti compilatori lo supportano senza lamentarsi di un aliasing rigoroso. Ciò è consentito in C99 e esplicitamente consentito in C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Puoi disabilitare l'aliasing rigoroso nel tuo compilatore ( f [no-] aliasing rigoroso in gcc))

  • È possibile utilizzare char*per l'aliasing anziché la parola del sistema. Le regole consentono un'eccezione per char*(compreso signed chare unsigned char). Si presume sempre che gli char*alias siano di altri tipi. Tuttavia, ciò non funzionerà diversamente: non si presume che la tua struttura alias un buffer di caratteri.

Principiante attenzione

Questo è solo un potenziale campo minato quando si sovrappongono due tipi l'uno sull'altro. Si dovrebbe anche conoscere endianness , l'allineamento di parola , e come affrontare i problemi di allineamento con l'imballaggio le strutture in modo corretto.

Nota

1 I tipi a cui C 2011 6.5 7 consente l'accesso a un valore sono:

  • un tipo compatibile con il tipo effettivo dell'oggetto,
  • una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente a una versione qualificata del tipo effettivo dell'oggetto,
  • un tipo di aggregato o unione che include uno dei tipi di cui sopra tra i suoi membri (incluso, ricorsivamente, un membro di un'unione subaggregata o contenuta), o
  • un tipo di carattere.

16
Sto arrivando dopo la battaglia sembra .. potrebbe unsigned char*essere usato lontano char*invece? Tendo a utilizzare unsigned charpiuttosto che charcome tipo sottostante per byteperché i miei byte non sono firmati e non voglio la stranezza del comportamento firmato (in particolare wrt a overflow)
Matthieu M.

30
@Matthieu: Signedness non fa differenza per le regole di alias, quindi usare unsigned char *va bene.
Thomas Eding,

22
Non è un comportamento indefinito leggere da un membro del sindacato diverso dall'ultimo scritto?
R. Martinho Fernandes,

23
Bollock, questa risposta è completamente all'indietro . L'esempio che mostra come illegale è in realtà legale e l'esempio che mostra come legale è in realtà illegale.
R. Martinho Fernandes,

7
Le dichiarazioni del buffer uint32_t* buff = malloc(sizeof(Msg));dell'Unione e quelle successive unsigned int asBuffer[sizeof(Msg)];avranno dimensioni diverse e nessuna delle due è corretta. La mallocchiamata si basa sull'allineamento di 4 byte sotto il cofano (non farlo) e l'unione sarà 4 volte più grande di quanto deve essere ... Capisco che è per chiarezza ma non mi dà alcun bug meno ...
nonsensickle,

233

La migliore spiegazione che ho trovato è di Mike Acton, Understanding Strict Aliasing . Si concentra un po 'sullo sviluppo di PS3, ma sostanzialmente è solo GCC.

Dall'articolo:

"Il rigoroso aliasing è un presupposto, fatto dal compilatore C (o C ++), che dereferenziare i puntatori a oggetti di tipi diversi non farà mai riferimento alla stessa posizione di memoria (cioè alias a vicenda.)"

Quindi, fondamentalmente, se si ha un int*puntamento a un po 'di memoria contenente un inte quindi si punta float*a quel ricordo e lo si usa come floatinfrangere la regola. Se il tuo codice non lo rispetta, molto probabilmente l'ottimizzatore del compilatore romperà il tuo codice.

L'eccezione alla regola è a char*, che può indicare qualsiasi tipo.


6
Qual è il modo canonico di usare legalmente la stessa memoria con variabili di 2 tipi diversi? o copiano tutti?
jiggunjer,

4
La pagina di Mike Acton è difettosa. La parte di "Casting through a union (2)", almeno, è decisamente sbagliata; il codice che sostiene è legale non lo è.
davmac,

11
@davmac: gli autori di C89 non hanno mai pensato che avrebbe dovuto costringere i programmatori a saltare attraverso i cerchi. Trovo assolutamente bizzarro l'idea che una regola esistente al solo scopo di ottimizzazione debba essere interpretata in modo tale da richiedere ai programmatori di scrivere codice che copi ridondantemente i dati nella speranza che un ottimizzatore rimuova il codice ridondante.
supercat

1
@curiousguy: "Impossibile avere unioni"? In primo luogo, lo scopo originale / primario dei sindacati non è in alcun modo collegato all'aliasing. In secondo luogo, le specifiche del linguaggio moderno consentono esplicitamente di utilizzare i sindacati per l'aliasing. Il compilatore è tenuto a notare che viene utilizzata un'unione e trattare la situazione è un modo speciale.
AnT

5
@curiousguy: False. In primo luogo, l'idea concettuale originale dietro i sindacati era che in qualsiasi momento c'è un solo oggetto membro "attivo" nel dato oggetto sindacale, mentre gli altri semplicemente non esistono. Quindi, non ci sono "oggetti diversi allo stesso indirizzo" come sembra credere. In secondo luogo, aliasare le violazioni di cui tutti parlano riguardano l' accesso a un oggetto come un oggetto diverso, non semplicemente il fatto di avere due oggetti con lo stesso indirizzo. Fintanto che non vi è alcun accesso alla punzonatura , non vi sono problemi. Questa era l'idea originale. Successivamente, è stata autorizzata la punzonatura tramite sindacati.
AnT

133

Questa è la regola di aliasing rigorosa, trovata nella sezione 3.10 dello standard C ++ 03 (altre risposte forniscono una buona spiegazione, ma nessuna ha fornito la regola stessa):

Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un valore diverso da uno dei seguenti tipi, il comportamento non è definito:

  • il tipo dinamico dell'oggetto,
  • una versione qualificata cv del tipo dinamico dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente a una versione con certificazione cv del tipo dinamico dell'oggetto,
  • un tipo aggregato o sindacale che include uno dei tipi summenzionati tra i suoi membri (incluso, ricorsivamente, un membro di un'unione sottogruppo o contenuta),
  • un tipo che è un tipo di classe base (possibilmente qualificato in cv) del tipo dinamico dell'oggetto,
  • a charo unsigned chardigitare.

Formulazione C ++ 11 e C ++ 14 (modifiche enfatizzate):

Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un valore diverso da uno dei seguenti tipi, il comportamento non è definito:

  • il tipo dinamico dell'oggetto,
  • una versione qualificata cv del tipo dinamico dell'oggetto,
  • un tipo simile (come definito in 4.4) al tipo dinamico dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente a una versione con certificazione cv del tipo dinamico dell'oggetto,
  • un tipo aggregato o di unione che include uno dei tipi sopra menzionati tra i suoi elementi o membri di dati non statici (incluso, ricorsivamente, un elemento o un membro di dati non statici di un'unione sottoaggregata o contenuta),
  • un tipo che è un tipo di classe base (possibilmente qualificato in cv) del tipo dinamico dell'oggetto,
  • a charo unsigned chardigitare.

Due cambiamenti erano piccoli: glvalue anziché lvalue e chiarimento del caso aggregato / sindacato.

Il terzo cambiamento fornisce una garanzia più forte (rilassa la forte regola di aliasing): il nuovo concetto di tipi simili che ora sono alias sicuri.


Anche la formulazione C (C99; ISO / IEC 9899: 1999 6.5 / 7; la stessa identica formulazione viene utilizzata in ISO / IEC 9899: 2011 §6.5 ¶7):

Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi 73) o 88) :

  • un tipo compatibile con il tipo effettivo dell'oggetto,
  • una versione quali fi cata di un tipo compatibile con il tipo effettivo dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,
  • un tipo di tipo con o senza segno corrispondente a una versione quali fi cata del tipo effettivo dell'oggetto,
  • un tipo di aggregato o unione che include uno dei tipi di cui sopra tra i suoi membri (incluso, ricorsivamente, un membro di un'unione subaggregata o contenuta), o
  • un tipo di carattere.

73) o 88) L'intento di questo elenco è di specificare le circostanze in cui un oggetto può o meno essere aliasato.


7
Ben, poiché le persone sono spesso dirette qui, mi sono permesso di aggiungere anche il riferimento allo standard C, per completezza.
Kos

1
Guarda la sezione C89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf sezione 3.3 che ne parla.
phorgan1,

2
Se uno ha un valore di un tipo di struttura, prende l'indirizzo di un membro e lo passa a una funzione che lo utilizza come puntatore al tipo di membro, sarebbe considerato come accedere a un oggetto del tipo di membro (legale), o un oggetto del tipo di struttura (vietato)? Un sacco di codice presuppone che sia legale accedere alle strutture in questo modo, e penso che molte persone scricchiolerebbero a una regola che è stata intesa come proibizione di tali azioni, ma non è chiaro quali siano le regole esatte. Inoltre, i sindacati e le strutture sono trattati allo stesso modo, ma le regole sensate per ciascuno dovrebbero essere diverse.
supercat

2
@supercat: il modo in cui è definita la regola per le strutture, l'accesso effettivo è sempre al tipo primitivo. Quindi l'accesso tramite un riferimento al tipo primitivo è legale perché i tipi corrispondono, e l'accesso tramite un riferimento al tipo di struttura contenente è legale perché è appositamente consentito.
Ben Voigt,

2
@BenVoigt: non credo che la sequenza iniziale comune funzioni se gli accessi non vengono effettuati tramite l'unione. Vedi goo.gl/HGOyoK per vedere cosa sta facendo gcc. Se l'accesso a un valore del tipo di unione tramite un valore del tipo di membro (non utilizzando l'operatore di accesso al membro del sindacato) fosse legale, allora wow(&u->s1,&u->s2)dovrebbe essere legale anche quando si utilizza un puntatore per modificare ue ciò annullerebbe la maggior parte delle ottimizzazioni che il la regola di aliasing è stata progettata per facilitare.
supercat

81

Nota

Questo è tratto dal mio "Cos'è la regola di aliasing rigorosa e perché ci preoccupiamo?" Scrivilo.

Che cos'è l'aliasing rigoroso?

In C e C ++ l'aliasing ha a che fare con quali tipi di espressioni siamo autorizzati ad accedere ai valori memorizzati. In C e C ++ lo standard specifica quali tipi di espressioni sono autorizzati ad alias quali tipi. Il compilatore e l'ottimizzatore possono assumere che seguiamo rigorosamente le regole di aliasing, da cui il termine regola di aliasing rigoroso . Se tentiamo di accedere a un valore utilizzando un tipo non consentito, viene classificato come comportamento indefinito ( UB ). Una volta che abbiamo un comportamento indefinito, tutte le scommesse sono disattivate, i risultati del nostro programma non sono più affidabili.

Sfortunatamente con violazioni rigorose dell'aliasing, otterremo spesso i risultati che ci aspettiamo, lasciando la possibilità che una versione futura di un compilatore con una nuova ottimizzazione rompa il codice che ritenevamo valido. Questo è indesiderabile ed è un obiettivo utile comprendere le rigide regole di aliasing e come evitare di violarle.

Per capire di più sul perché ci preoccupiamo, discuteremo dei problemi che sorgono quando si violano le rigide regole di aliasing, la punzonatura di tipo poiché le tecniche comuni utilizzate nella punzonatura di tipo spesso violano le rigide regole di aliasing e come digitare correttamente il gioco di parole.

Esempi preliminari

Diamo un'occhiata ad alcuni esempi, quindi possiamo parlare esattamente di ciò che dicono gli standard, esaminare alcuni ulteriori esempi e quindi vedere come evitare aliasing rigorosi e rilevare le violazioni che abbiamo perso. Ecco un esempio che non dovrebbe sorprendere ( esempio dal vivo ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Abbiamo un int * che punta alla memoria occupata da un int e questo è un alias valido. L'ottimizzatore deve presumere che le assegnazioni tramite ip potrebbero aggiornare il valore occupato da x .

L'esempio seguente mostra l'aliasing che porta a comportamenti indefiniti ( esempio live ):

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

Nella funzione foo prendiamo un int * e un float * , in questo esempio chiamiamo foo e impostiamo entrambi i parametri in modo che puntino alla stessa posizione di memoria che in questo esempio contiene un int . Nota, reinterpret_cast sta dicendo al compilatore di trattare l'espressione come se avesse il tipo specificato dal suo parametro template. In questo caso gli stiamo dicendo di trattare l'espressione & x come se avesse tipo float * . Possiamo aspettarci ingenuamente che il risultato del secondo cout sia 0 ma con l'ottimizzazione abilitata usando -O2 sia gcc che clang producono il seguente risultato:

0
1

Il che non può essere previsto, ma è perfettamente valido poiché abbiamo invocato un comportamento indefinito. Un float non può validamente alias un oggetto int . Pertanto, l'ottimizzatore può assumere la costante 1 memorizzata durante la dereferenziazione i sarà il valore restituito poiché un archivio tramite f non può influire validamente su un oggetto int . Collegando il codice in Compiler Explorer si vede che è esattamente ciò che sta accadendo ( esempio live ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

L'ottimizzatore utilizzando Type-Based Analysis alias (TBAA) assume 1 verrà restituito e sposta direttamente il valore costante nel registro eax che porta il valore restituito. TBAA utilizza le regole delle lingue su quali tipi sono autorizzati all'alias per ottimizzare carichi e negozi. In questo caso TBAA sa che un float non può alias e int e ottimizza il carico di i .

Ora, al Rule-Book

Cosa dice esattamente lo standard che siamo autorizzati e non autorizzati a fare? Il linguaggio standard non è semplice, quindi per ogni articolo cercherò di fornire esempi di codice che dimostrino il significato.

Cosa dice lo standard C11?

Lo standard C11 dice quanto segue nella sezione 6.5 Espressioni paragrafo 7 :

Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi: 88) - un tipo compatibile con il tipo effettivo dell'oggetto,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,

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

- un tipo di tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,

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 ha un'estensione ed inoltre che permette di assegnare unsigned int * a int * anche se non sono tipi compatibili.

- un tipo di tipo con o senza segno corrispondente a una versione qualificata del tipo effettivo dell'oggetto,

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

- un tipo aggregato o sindacale che includa uno dei tipi summenzionati tra i suoi membri (incluso, ricorsivamente, un membro di un'unione sottaggregata o contenuta), o

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

- un tipo di carattere.

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.

Cosa dice lo standard di bozza C ++ 17

La bozza di standard C ++ 17 nella sezione [basic.lval] paragrafo 11 dice:

Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un valore diverso da uno dei seguenti tipi, il comportamento non è definito: 63 (11.1) - il tipo dinamico dell'oggetto,

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) - una versione qualificata cv del tipo dinamico dell'oggetto,

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) - un tipo simile (come definito in 7.5) al tipo dinamico dell'oggetto,

(11.4) - un tipo di tipo con o senza segno corrispondente al tipo dinamico dell'oggetto,

// 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) - un tipo di tipo con o senza segno corrispondente a una versione qualificata cv del tipo dinamico dell'oggetto,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - un tipo di aggregato o unione che include uno dei tipi di cui sopra tra i suoi elementi o membri di dati non statici (incluso, ricorsivamente, un elemento o un membro di dati non statici di un'unione sottaggregata o contenuta),

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) - un tipo che è un tipo di classe base (possibilmente qualificato in cv) del tipo dinamico dell'oggetto,

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) - un carattere char, unsigned char o std :: byte.

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
}

Vale la pena notare che il carattere firmato non è incluso nell'elenco sopra, questa è una notevole differenza da C che indica un tipo di carattere .

Che cos'è il tipo Punning

Siamo arrivati ​​a questo punto e potremmo chiederci, perché dovremmo voler alias? La risposta è in genere digitare pun , spesso i metodi utilizzati violano le rigide regole di aliasing.

A volte vogliamo eludere il sistema dei tipi e interpretare un oggetto come un tipo diverso. Questo si chiama punzonatura , per reinterpretare un segmento di memoria come un altro tipo. La punzonatura del tipo è utile per le attività che desiderano visualizzare, trasportare o manipolare la rappresentazione sottostante di un oggetto. Le aree tipiche che troviamo come tipo di punzonatura utilizzate sono compilatori, serializzazione, codice di rete, ecc ...

Tradizionalmente questo è stato ottenuto prendendo l'indirizzo dell'oggetto, lanciandolo su un puntatore del tipo che vogliamo reinterpretare come e quindi accedendo al valore, o in altre parole con l'aliasing. Per esempio:

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

Come abbiamo visto in precedenza, questo non è un alias valido, quindi stiamo invocando un comportamento indefinito. Ma tradizionalmente i compilatori non sfruttano le rigide regole di aliasing e questo tipo di codice di solito ha appena funzionato, purtroppo gli sviluppatori si sono abituati a fare le cose in questo modo. Un metodo alternativo comune per la punzonatura di tipo è tramite i sindacati, che è valido in C ma comportamento indefinito in C ++ ( vedi esempio dal vivo ):

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

Ciò non è valido in C ++ e alcuni considerano lo scopo dei sindacati esclusivamente per l'implementazione di tipi di varianti e ritengono che l'uso dei sindacati per la punzonatura dei tipi sia un abuso.

Come si digita correttamente Pun?

Il metodo standard per la punzonatura di tipo in C e C ++ è memcpy . Questo può sembrare un po 'pesante ma l'ottimizzatore dovrebbe riconoscere l'uso di memcpy per la punzonatura del tipo e ottimizzarlo e generare un registro per registrare lo spostamento. Ad esempio, se sappiamo che int64_t ha le stesse dimensioni di double :

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

possiamo usare memcpy :

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

A un livello di ottimizzazione sufficiente, qualsiasi compilatore moderno decente genera un codice identico al metodo reinterpret_cast menzionato in precedenza o al metodo union per la punzonatura dei tipi . Esaminando il codice generato, vediamo che utilizza solo il registro mov ( esempio di Explorer compilatore live ).

C ++ 20 e bit_cast

In C ++ 20 potremmo ottenere bit_cast ( implementazione disponibile nel link dalla proposta ) che offre un modo semplice e sicuro per digitare-pun ed essere utilizzabile in un contesto constexpr.

Il seguente è un esempio di come usare bit_cast per digitare pun un int senza segno su float , ( vederlo dal vivo ):

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

Nel caso in cui i tipi To e From non abbiano le stesse dimensioni, è necessario utilizzare una struttura intermedia15. Useremo uno struct contenente un array di caratteri sizeof (unsigned int) ( presuppone che 4 byte unsigned int ) sia il tipo From e un unsigned int come il tipo A .:

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

È un peccato che abbiamo bisogno di questo tipo intermedio, ma questo è l'attuale vincolo di bit_cast .

Catturare violazioni rigorose di alias

Non disponiamo di molti buoni strumenti per rilevare un aliasing rigoroso in C ++, gli strumenti in nostro possesso rileveranno alcuni casi di violazioni rigorose dell'aliasing e alcuni casi di carichi e negozi non allineati.

gcc usando il flag -fstrict-aliasing e -Wstrict-aliasing può catturare alcuni casi anche se non senza falsi positivi / negativi. Ad esempio i seguenti casi genereranno un avviso in gcc ( vederlo dal vivo ):

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

sebbene non rileverà questo caso aggiuntivo ( vederlo dal vivo ):

int *p;

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

Sebbene clang consenta queste bandiere, a quanto pare non implementa effettivamente gli avvisi.

Un altro strumento che abbiamo a disposizione è ASan che può catturare carichi e depositi disallineati. Sebbene non si tratti di violazioni di aliasing direttamente rigide, sono un risultato comune di violazioni di aliasing rigorose. Ad esempio, i seguenti casi genereranno errori di runtime quando generati con clang usando -fsanitize = address

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]

L'ultimo strumento che consiglierò è specifico del C ++ e non strettamente uno strumento ma una pratica di codifica, non consentire cast in stile C. Sia gcc che clang produrranno una diagnostica per i cast in stile C usando -Wold-style-cast . Questo costringerà tutti i giochi di parole di tipo indefinito a usare reinterpret_cast, in generale reinterpret_cast dovrebbe essere un flag per una più stretta revisione del codice. È anche più facile cercare nella base di codici reinterpret_cast per eseguire un controllo.

Per C abbiamo già tutti gli strumenti coperti e abbiamo anche tis-interprete, un analizzatore statico che analizza in modo esauriente un programma per un ampio sottoinsieme del linguaggio C. Dato un verion C dell'esempio precedente in cui l'uso di -fstrict-aliasing manca un caso ( vederlo dal vivo )

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 è in grado di catturare tutti e tre, l'esempio seguente invoca tis-kernal come tis-interprete (l'output viene modificato per brevità):

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

Infine c'è TySan che è attualmente in fase di sviluppo. Questo disinfettante aggiunge informazioni sul controllo del tipo in un segmento di memoria shadow e controlla gli accessi per vedere se violano le regole di aliasing. Lo strumento potenzialmente dovrebbe essere in grado di rilevare tutte le violazioni dell'aliasing ma potrebbe avere un sovraccarico di runtime elevato.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Bhargav Rao

3
Se potessi, +10, ben scritto e spiegato, anche da entrambe le parti, scrittori di compilatori e programmatori ... l'unica critica: sarebbe bello avere esempi contrari sopra, per vedere cosa è proibito dallo standard, non è ovvio tipo di :-)
Gabriel

2
Ottima risposta Mi dispiace solo che gli esempi iniziali siano riportati in C ++, il che rende difficile seguire per persone come me che conoscono o si preoccupano solo di C e non hanno idea di cosa reinterpret_castpotrebbe fare o cosa coutpotrebbe significare. (Va bene menzionare C ++, ma la domanda originale era su C e IIUC, questi esempi potevano essere validamente scritti in C.)
Gro-Tsen,

Per quanto riguarda il tipo di punzonatura: quindi se scrivo un array di qualche tipo X nel file, quindi leggo da quel file questo array in memoria puntato con vuoto *, quindi lancio quel puntatore sul tipo reale dei dati per usarlo - questo è comportamento indefinito?
Michele IV,

44

Il rigoroso aliasing non si riferisce solo ai puntatori, influisce anche sui riferimenti, ne ho scritto un articolo per il wiki dello sviluppatore boost ed è stato così ben accolto che l'ho trasformato in una pagina sul mio sito web di consulenza. Spiega completamente di cosa si tratta, perché confonde così tanto le persone e cosa fare al riguardo. White paper rigoroso di aliasing . In particolare spiega perché i sindacati sono comportamenti rischiosi per C ++ e perché l'uso di memcpy è l'unica soluzione portatile sia in C sia in C ++. Spero sia utile.


3
"Il rigoroso aliasing non si riferisce solo ai puntatori, influisce anche sui riferimenti " In realtà, si riferisce ai valori . "L' uso di memcpy è l'unica soluzione portatile " Ascolta!
curiousguy,

5
Buona carta. La mia opinione: (1) questo aliasing-'problema' è una reazione eccessiva alla cattiva programmazione - cercando di proteggere il cattivo programmatore dalle sue cattive abitudini. Se il programmatore ha buone abitudini, questo aliasing è solo un fastidio e i controlli possono essere tranquillamente disattivati. (2) L'ottimizzazione sul lato compilatore dovrebbe essere effettuata solo in casi noti e, in caso di dubbio, seguire rigorosamente il codice sorgente; costringere il programmatore a scrivere codice per soddisfare le idiosincrasie del compilatore è, semplicemente, sbagliato. Ancora peggio per renderlo parte dello standard.
Slashma,

4
@slashmais (1) " è una reazione eccessiva alla cattiva programmazione " Sciocchezze. È un rifiuto delle cattive abitudini. Lo fai? Paghi il prezzo: nessuna garanzia per te! (2) Casi ben noti? Quale? La rigida regola di aliasing dovrebbe essere "ben nota"!
curioso

5
@curiousguy: dopo aver chiarito alcuni punti di confusione, è chiaro che il linguaggio C con le regole di aliasing rende impossibile per i programmi implementare pool di memoria indipendente dal tipo. Alcuni tipi di programma possono funzionare con malloc / free, ma altri hanno bisogno di una logica di gestione della memoria più adatta alle attività da svolgere. Mi chiedo perché la logica del C89 abbia usato un esempio così grossolano del motivo della regola di aliasing, dal momento che il loro esempio fa sembrare che la regola non crei grosse difficoltà nell'esecuzione di compiti ragionevoli.
supercat

5
@curiousguy, la maggior parte delle suite di compilatori là fuori includono -fstrict-aliasing come predefinito su -O3 e questo contratto nascosto è forzato sugli utenti che non hanno mai sentito parlare del TBAA e hanno scritto codice come potrebbe fare un programmatore di sistema. Non intendo sembrare disonesto per i programmatori di sistema, ma questo tipo di ottimizzazione dovrebbe essere lasciata al di fuori dell'opzione di default di -O3 e dovrebbe essere un'ottimizzazione di opt-in per coloro che sanno cos'è il TBAA. Non è divertente guardare il "bug" del compilatore che risulta essere un codice utente che viola TBAA, in particolare rintracciare la violazione a livello di sorgente nel codice utente.
kchoi,

34

Come addendum a ciò che Doug T. ha già scritto, ecco un semplice caso di prova che probabilmente lo innesca con gcc:

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

Compila con gcc -O2 -o check check.c. Di solito (con la maggior parte delle versioni di gcc che ho provato) questo genera un "problema di aliasing rigoroso", perché il compilatore presuppone che "h" non possa essere lo stesso indirizzo di "k" nella funzione "check". Per questo motivo il compilatore ottimizza il fileif (*h == 5) away e chiama sempre printf.

Per coloro che sono interessati ecco il codice assembler x64, prodotto da gcc 4.6.3, in esecuzione su Ubuntu 12.04.2 per x64:

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

Quindi la condizione if è completamente scomparsa dal codice assembler.


se aggiungi un secondo short * j a check () e lo usi (* j = 7), l'ottimizzazione scompare poiché ggc non lo fa se h e j non puntano effettivamente allo stesso valore. sì, l'ottimizzazione è davvero intelligente.
Philippe Lhardy,

2
Per rendere le cose più divertenti, usa i puntatori a tipi che non sono compatibili ma che hanno le stesse dimensioni e rappresentazione (su alcuni sistemi che è vero per esempio long long*e int64_t*). Ci si potrebbe aspettare che un compilatore sano dovrebbe riconoscere che long long*e int64_t*potrebbe accedere allo stesso archivio se sono archiviati in modo identico, ma tale trattamento non è più di moda.
supercat

Grr ... x64 è una convenzione di Microsoft. Utilizzare invece amd64 o x86_64.
SS Anne,

Grr ... x64 è una convenzione di Microsoft. Utilizzare invece amd64 o x86_64.
SS Anne,

17

La punzonatura di tipo tramite cast di puntatori (invece di utilizzare un sindacato) è un esempio importante di rottura di un aliasing rigoroso.


1
Vedi la mia risposta qui per le citazioni pertinenti, in particolare le note a piè di pagina, ma la punzonatura di tipo attraverso i sindacati è sempre stata consentita in C anche se all'inizio era mal formulata. Tu mio voglio chiarire la tua risposta.
Shafik Yaghmour,

@ShafikYaghmour: C89 ha chiaramente consentito agli implementatori di selezionare i casi in cui avrebbero riconosciuto o meno utilmente il tipo di punzonatura tramite i sindacati. Un'implementazione potrebbe, ad esempio, specificare che per una scrittura su un tipo seguita da una lettura di un altro da riconoscere come punzonatura di tipo, se il programmatore ha effettuato una delle seguenti operazioni tra la scrittura e la lettura : (1) valutare un valore contenente il tipo di unione [prendere l'indirizzo di un membro si qualificherebbe, se fatto al punto giusto nella sequenza]; (2) converti un puntatore in un tipo in un puntatore all'altro e accedi tramite quel ptr.
supercat

@ShafikYaghmour: un'implementazione potrebbe anche specificare, ad esempio, che il tipo di punzonatura tra valori interi e valori in virgola mobile funzionerebbe in modo affidabile solo se il codice eseguisse una fpsync()direttiva tra la scrittura come fp e la lettura come int o viceversa [sulle implementazioni con pipeline e cache separate di interi e FPU , una direttiva del genere potrebbe essere costosa, ma non così costosa come fare in modo che il compilatore esegua tale sincronizzazione su ogni accesso al sindacato]. Oppure un'implementazione potrebbe specificare che il valore risultante non sarà mai utilizzabile se non in circostanze che utilizzano Sequenze iniziali comuni.
supercat

@ShafikYaghmour: Sotto C89, le implementazioni potevano vietare la maggior parte delle forme di punzonatura di tipo, anche tramite i sindacati, ma l'equivalenza tra puntatori a sindacati e puntatori ai loro membri implicava che la punzonatura di tipo era consentita in implementazioni che non lo vietavano espressamente .
supercat

17

Secondo la logica C89, gli autori dello Standard non volevano richiedere ai compilatori un codice come:

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

dovrebbe essere richiesto di ricaricare il valore xtra l'assegnazione e la dichiarazione di reso in modo da consentire la possibilità che ppotrebbe indicare x, e l'incarico *ppotrebbe di conseguenza modificare il valore di x. L'idea che un compilatore dovrebbe avere il diritto di presumere che non ci sarà aliasing in situazioni come quelle sopra è stata controversa.

Sfortunatamente, gli autori del C89 hanno scritto la loro regola in modo che, se letto alla lettera, anche la seguente funzione invocherebbe un comportamento indefinito:

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

perché utilizza un valore di tipo intper accedere a un oggetto di tipo struct Se intnon è tra i tipi che possono essere utilizzati accedendo astruct S . Poiché sarebbe assurdo trattare ogni uso di membri di tipo non carattere di strutture e sindacati come comportamento indefinito, quasi tutti riconoscono che ci sono almeno alcune circostanze in cui un valore di un tipo può essere usato per accedere a un oggetto di un altro tipo . Sfortunatamente, il C Standards Committee non è riuscito a definire quali siano tali circostanze.

Gran parte del problema è il risultato del Rapporto difetti n. 028, che chiedeva informazioni sul comportamento di un programma come:

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

Il rapporto sui difetti n. 28 afferma che il programma richiama comportamenti indefiniti perché l'azione di scrivere un membro del sindacato di tipo "doppio" e leggere uno del tipo "int" invoca il comportamento definito dall'implementazione. Tale ragionamento è privo di senso, ma costituisce la base per le regole del tipo efficace che complicano inutilmente la lingua senza fare nulla per affrontare il problema originale.

Il modo migliore per risolvere il problema originale sarebbe probabilmente quello di trattare la nota a piè di pagina sullo scopo della regola come se fosse normativa e rendere la regola inapplicabile, tranne nei casi che implicano effettivamente accessi contrastanti mediante alias. Dato qualcosa di simile:

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

Non vi sono conflitti all'interno inc_intperché tutti gli accessi alla memoria a cui si accede *pvengono effettuati con un valore di tipo inte non vi sono conflitti testperché pè visibilmente derivato da una struct Se, al successivo sutilizzo, tutti gli accessi a quella memoria che verranno mai effettuati attraverso psarà già successo.

Se il codice fosse leggermente modificato ...

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

Qui, esiste un conflitto di aliasing tra pe l'accesso alla s.xriga contrassegnata perché a quel punto dell'esecuzione esiste un altro riferimento che verrà utilizzato per accedere alla stessa memoria .

Se il rapporto sui difetti 028 affermasse che l'esempio originale aveva invocato UB a causa della sovrapposizione tra la creazione e l'uso dei due puntatori, ciò avrebbe reso le cose molto più chiare senza dover aggiungere "Tipi efficaci" o altra complessità del genere.


In poche parole, sarebbe interessante leggere una sorta di proposta che fosse più o meno "ciò che il comitato per le norme avrebbe potuto fare" che ha raggiunto i suoi obiettivi senza introdurre la stessa complessità.
jrh

1
@jrh: penso che sarebbe abbastanza semplice. Riconoscere che 1. Affinché si verifichi l'aliasing durante una particolare esecuzione di una funzione o di un loop, durante l'esecuzione devono essere utilizzati due puntatori o valori diversi per indirizzare la stessa memoria in modalità in conflitto; 2. Riconoscere che nei contesti in cui un puntatore o un valore è derivato in modo visibile da un altro, un accesso al secondo è un accesso al primo; 3. Riconoscere che la regola non è destinata ad applicarsi nei casi che in realtà non comportano l'aliasing.
supercat

1
Le circostanze esatte in cui un compilatore riconosce un valore derivato di recente possono essere un problema di qualità di implementazione, ma qualsiasi compilatore decente da remoto dovrebbe essere in grado di riconoscere forme che gcc e clang ignorano deliberatamente.
supercat

11

Dopo aver letto molte delle risposte, sento la necessità di aggiungere qualcosa:

Il rigoroso aliasing (che descriverò tra poco) è importante perché :

  1. L'accesso alla memoria può essere costoso (dal punto di vista delle prestazioni), motivo per cui i dati vengono manipolati nei registri della CPU prima di essere riscritti nella memoria fisica.

  2. Se i dati in due diversi registri della CPU verranno scritti nello stesso spazio di memoria, non possiamo prevedere quali dati "sopravviveranno" quando codifichiamo in C.

    In assembly, dove codifichiamo manualmente il caricamento e lo scaricamento dei registri della CPU, sapremo quali dati rimangono intatti. Ma C (per fortuna) estrae questo dettaglio.

Poiché due puntatori possono puntare alla stessa posizione nella memoria, ciò potrebbe comportare un codice complesso che gestisce possibili collisioni .

Questo codice aggiuntivo è lento e danneggia le prestazioni poiché esegue operazioni di lettura / scrittura di memoria aggiuntiva che sono sia più lente che (possibilmente) non necessarie.

La regola di aliasing rigorosa ci consente di evitare il codice macchina ridondante nei casi in cui dovrebbe essere sicuro supporre che due puntatori non puntino allo stesso blocco di memoria (vedere anche la restrictparola chiave).

L'aliasing rigoroso afferma che è sicuro supporre che i puntatori a tipi diversi puntino a posizioni diverse nella memoria.

Se un compilatore nota che due puntatori puntano a tipi diversi (ad esempio, an int *e a float *), supporrà che l'indirizzo di memoria sia diverso e non proteggerà dalle collisioni dell'indirizzo di memoria, con conseguente codice macchina più veloce.

Per esempio :

Assumiamo la seguente funzione:

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

Per gestire il caso in cui a == b(entrambi i puntatori puntano alla stessa memoria), dobbiamo ordinare e testare il modo in cui cariciamo i dati dalla memoria nei registri della CPU, quindi il codice potrebbe finire così:

  1. carica ae bdalla memoria.

  2. aggiungere aa b.

  3. salvare b e ricaricare a .

    (salva dal registro CPU nella memoria e carica dalla memoria al registro CPU).

  4. aggiungere ba a.

  5. salva a(dal registro CPU) nella memoria.

Il passaggio 3 è molto lento perché deve accedere alla memoria fisica. Tuttavia, è necessario per la protezione da istanze in cui ae bpunta allo stesso indirizzo di memoria.

Un aliasing rigoroso ci consentirebbe di impedirlo dicendo al compilatore che questi indirizzi di memoria sono nettamente diversi (che, in questo caso, consentirà un'ulteriore ottimizzazione che non può essere eseguita se i puntatori condividono un indirizzo di memoria).

  1. Questo può essere detto al compilatore in due modi, usando diversi tipi a cui puntare. vale a dire:

    void merge_two_numbers(int *a, long *b) {...}
  2. Usando la restrictparola chiave. vale a dire:

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

Ora, soddisfacendo la regola di aliasing rigoroso, è possibile evitare il passaggio 3 e il codice verrà eseguito in modo significativamente più veloce.

Infatti, aggiungendo la restrictparola chiave, l'intera funzione potrebbe essere ottimizzata per:

  1. carica ae bdalla memoria.

  2. aggiungere aa b.

  3. salva il risultato sia a ache a b.

Questa ottimizzazione non avrebbe potuto essere eseguita prima, a causa della possibile collisione (dove ae bsarebbe triplicata anziché raddoppiata).


con la parola chiave restrittiva, al passaggio 3, non dovrebbe essere salvare il risultato solo in 'b'? Sembra che il risultato della somma sia memorizzato anche in 'a'. Deve essere ricaricato di nuovo?
NeilB,

1
@NeilB - Sì, hai ragione. Stiamo solo salvando b(non ricaricandolo) e ricaricandolo a. Spero sia più chiaro ora.
Myst,

L'aliasing basato sul tipo potrebbe aver offerto alcuni vantaggi prima restrict, ma penso che quest'ultimo nella maggior parte dei casi sarebbe più efficace, e allentando alcuni vincoli registergli permetterebbe di riempire alcuni dei casi in cui restrictnon sarebbe utile. Non sono sicuro che sia mai stato "importante" trattare lo Standard come una descrizione completa di tutti i casi in cui i programmatori dovrebbero aspettarsi che i compilatori riconoscano le prove di aliasing, piuttosto che semplicemente descrivere i luoghi in cui i compilatori devono presumere aliasing anche quando non esistono prove particolari di esso .
supercat

Si noti che sebbene il caricamento dalla RAM principale sia molto lento (e possa arrestare il core della CPU per molto tempo se le seguenti operazioni dipendono dal risultato), il caricamento dalla cache L1 è piuttosto veloce, e così sta scrivendo su una riga della cache che stava scrivendo di recente dallo stesso nucleo. Quindi, tranne la prima lettura o scrittura su un indirizzo, saranno in genere ragionevolmente veloci: la differenza tra l'accesso all'add reg / mem è minore della differenza tra l'aggiunta mem nella cache / non cache.
curiousguy,

@curiousguy - anche se hai ragione, "veloce" in questo caso è relativo. La cache L1 è probabilmente ancora un ordine di grandezza più lento dei registri della CPU (penso più di 10 volte più lento). Inoltre, la restrictparola chiave minimizza non solo la velocità delle operazioni, ma anche il loro numero, il che potrebbe essere significativo ... Voglio dire, dopo tutto, l'operazione più veloce non è affatto un'operazione :)
Myst

6

Alias ​​rigoroso non consente a tipi di puntatore diversi di utilizzare gli stessi dati.

Questo articolo dovrebbe aiutarti a comprendere il problema in dettaglio.


4
È possibile alias tra riferimenti e tra un riferimento e anche un puntatore. Vedi il mio tutorial dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
È consentito avere tipi di puntatore diversi per gli stessi dati. Il punto in cui arriva un aliasing rigoroso è quando la stessa posizione di memoria viene scritta attraverso un tipo di puntatore e letta attraverso un altro. Inoltre, sono ammessi alcuni tipi diversi (ad es. intE una struttura che contiene un int).
MM

-3

Tecnicamente in C ++, la rigida regola di aliasing non è probabilmente mai applicabile.

Nota la definizione di indiretta ( * operatore ):

L'operatore unario * esegue il riferimento indiretto: l'espressione a cui viene applicata deve essere un puntatore a un tipo di oggetto o un puntatore a un tipo di funzione e il risultato è un valore che si riferisce all'oggetto o alla funzione a cui punta l'espressione .

Anche dalla definizione di glvalue

Un glvalue è un'espressione la cui valutazione determina l'identità di un oggetto, (... snip)

Quindi, in qualsiasi traccia di programma ben definita, un valore di valore si riferisce a un oggetto. Quindi la cosiddetta regola di aliasing rigoroso non si applica mai. Questo potrebbe non essere quello che volevano i designer.


4
Lo standard C usa il termine "oggetto" per fare riferimento a una serie di concetti diversi. Tra questi, una sequenza di byte allocati esclusivamente per uno scopo, un riferimento non necessariamente esclusivo a una sequenza di byte da / verso la quale un valore di un determinato tipo potrebbe essere scritto o letto, o un tale riferimento che effettivamente ha stato o sarà accessibile in alcuni contesti. Non credo che esista un modo ragionevole per definire il termine "Oggetto" che sia coerente con il modo in cui lo Standard lo utilizza.
supercat

@supercat Errato. Nonostante la tua immaginazione, in realtà è abbastanza coerente. In ISO C è definito come "regione di archiviazione dei dati nell'ambiente di esecuzione, il cui contenuto può rappresentare valori". In ISO C ++ esiste una definizione simile. Il tuo commento è ancora più irrilevante della risposta perché tutto ciò che hai menzionato sono modi di rappresentazione per riferire il contenuto degli oggetti , mentre la risposta illustra il concetto C ++ (glvalue) di un tipo di espressioni strettamente legate all'identità degli oggetti. E tutte le regole di alias sono sostanzialmente rilevanti per l'identità ma non per il contenuto.
FrankHB,

1
@FrankHB: se uno dichiara int foo;, a cosa accede l'espressione lvalue *(char*)&foo? È un oggetto di tipo char? Quell'oggetto nasce contemporaneamente foo? Scriverebbe per foomodificare il valore memorizzato di quel suddetto oggetto di tipo char? In tal caso, esiste una regola che consenta di characcedere al valore memorizzato di un oggetto di tipo usando un valore di tipo int?
supercat

@FrankHB: in assenza di 6.5p7, si potrebbe semplicemente dire che ogni area di archiviazione contiene simultaneamente tutti gli oggetti di ogni tipo che potrebbero adattarsi a quella regione di archiviazione e che accedendo a quella regione di archiviazione accede simultaneamente a tutti loro. Interpretare in tal modo l'uso del termine "oggetto" in 6.5p7, tuttavia, proibirebbe di fare molto di tutto con valori non di tipo carattere, il che sarebbe chiaramente un risultato assurdo e sconfiggerebbe totalmente lo scopo della regola. Inoltre, il concetto di "oggetto" utilizzato ovunque diverso da 6.5p6 ha un tipo di tempo di compilazione statico, ma ...
supercat

1
sizeof (int) è 4, la dichiarazione int i;crea quattro oggetti di ogni tipo di carattere 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` e i. Infine, non c'è nulla nello standard che consenta persino a un volatilepuntatore qualificato di accedere ai registri hardware che non soddisfano la definizione di "oggetto".
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.