2014-09-15 7 views
48

Supponiamo A, B, a, e b sono tutte le variabili, e gli indirizzi di A, B, a e b sono tutti diversi. Poi, per il seguente codice:Per {A = a; B = b; }, "A = a" sarà eseguito rigorosamente prima di "B = b"?

A = a; 
B = b; 

Non lo standard C e C++ in modo esplicito richiedono A=a essere rigorosamente eseguita prima B=b? Dato che gli indirizzi di A, B, a e sono tutti diversi, i compilatori sono autorizzati a scambiare la sequenza di esecuzione di due istruzioni per scopi quali l'ottimizzazione?

Se la risposta alla mia domanda è diversa in C e C++, mi piacerebbe sapere entrambi.

Modifica: lo sfondo della domanda è il seguente. Nel progetto AI del gioco da tavolo, per l'ottimizzazione le persone usano lock-less shared-hash table, la cui correttezza dipende fortemente dall'ordine di esecuzione se non si aggiunge la limitazione volatile.

+19

Anche se il compilatore è stato garantito per generare il codice in tale ordine, la CPU stessa esegue l'esecuzione dell'ordine. – KitsuneYMG

+9

Non solo i compilatori sono autorizzati a farlo, le CPU possono farlo, i controller di memoria sono autorizzati a farlo, le cache sono autorizzate a farlo e così via. –

+0

Ho appena aggiunto un esempio interessante che mostra questo praticamente e non solo teoricamente. –

risposta

54

Entrambi gli standard consentono di eseguire queste istruzioni fuori servizio, purché ciò non modifichi il comportamento osservabile. Questo è noto come il come-se regola:

Si noti che, come è sottolineato nei commenti, cosa si intende per "comportamento osservabile" è il comportamento osservabile di un programma con un comportamento definito. Se il tuo programma ha un comportamento indefinito, allora il compilatore è esonerato dal ragionamento su questo.

+3

Inoltre, nessuno dei due potrebbe essere eseguito se non influisce sul comportamento osservabile del programma. (cioè ottimizzato completamente) –

+2

Probabilmente vale la pena sottolineare che l'accesso o la modifica di una variabile conta solo come "comportamento osservabile" se la variabile è volatile. –

+0

@MikeSeymour Suppongo che con l'overloading dell'operatore in C++, 'A = a' potrebbe avere facilmente un comportamento osservabile, anche per variabili non volatili. –

3

Se non c'è dipendenza delle istruzioni, queste possono essere eseguite fuori ordine anche se il risultato finale non è influenzato. Puoi osservarlo mentre esegui il debug di un codice compilato a un livello di ottimizzazione più elevato.

25

Il compilatore è obbligato solo a emulare il comportamento osservabile di un programma, quindi se un riordino non violerebbe tale principio, allora sarebbe consentito. Supponendo che il comportamento sia ben definito, se il tuo programma contiene undefined behavior come una corsa di dati, il comportamento del programma sarà imprevedibile e, come commentato, richiederebbe l'uso di una qualche forma di sincronizzazione per proteggere la sezione critica.

un utile riferimento

Un interessante articolo che copre questo è Memory Ordering at Compile Time e dice:

La regola cardinale di riordino della memoria, che è universalmente seguita dagli sviluppatori del compilatore e fornitori di CPU, può essere formulato come segue:

Non modificare il comportamento di un programma a thread singolo.

Un esempio

L'articolo fornisce un semplice programma in cui possiamo vedere questo riordino:

int A, B; // Note: static storage duration so initialized to zero 

void foo() 
{ 
    A = B + 1; 
    B = 0; 
} 

e spettacoli a livelli di ottimizzazione più elevati B = 0 è fatto prima A = B + 1, e possiamo riprodurre questo risultato utilizzando godbolt, che durante l'utilizzo di -O3 produce il seguente g (see it live):

movl $0, B(%rip) #, B 
addl $1, %eax #, D.1624 

Perché?

Perché il compilatore riordina? L'articolo spiega è esattamente lo stesso motivo il processore fa, a causa della complessità dell'architettura:

Come accennato all'inizio, il compilatore modifica dell'ordine di memoria interazioni per la stessa ragione che il processore lo fa - ottimizzazione delle prestazioni. Tali ottimizzazioni sono una conseguenza diretta della complessità della CPU moderna.

Standards

Nel progetto C++ standard, questo è coperto nella sezione 1.9 esecuzione programma che dice (sottolineatura mia andare avanti):

Le descrizioni semantiche di questo Lo standard internazionale definisce una macchina astratta non deterministica con parametri . Questo standard internazionale non pone alcun requisito sulla struttura delle implementazioni conformi . In particolare, non è necessario copiare o emulare la struttura della macchina astratta. Piuttosto, le implementazioni conformi sono necessarie per emulare (solo) il comportamento osservabile della macchina astratta come spiegato di seguito.

nota 5 ci dice questo è noto anche come il come-se regola:

Questa disposizione è talvolta chiamato il “come-se” regola, perché un l'implementazione è libera da qualsiasi requisito di questo Standard internazionale finché il risultato è come se il requisito era stato rispettato, per quanto è possibile determinare dal comportamento osservabile del programma.Ad esempio, un'implementazione effettiva necessita di non valuta parte di un'espressione se è possibile dedurre che il suo valore è non utilizzato e che non viene prodotto alcun effetto collaterale sul comportamento osservabile di .

il progetto di C99 e progetto di standard C11 copre questo nella sezione 5.1.2.3 esecuzione programma anche se dobbiamo andare all'indice di vedere che è chiamato il come-se regola nello standard C così:

as-se di regola, 5.1.2.3

Aggiornamento su considerazioni di blocco-free

L'articolo An Introduction to Lock-Free Programming tratta questo argomento bene e per i PO preoccupazioni sulla tavolo lock-meno condiviso-hash implementazione di questa sezione è probabilmente il più rilevante:

di memoria per l'ordinazione

Come il diagramma di flusso suggerisce, ogni volta che si esegue la programmazione senza blocco per il multicore (o qualsiasi symmetric multiprocessor), e il proprio ambiente non garantisce la coerenza sequenziale , è necessario considerare come impedire memory reordering.

Su architetture di oggi, gli strumenti per far rispettare corretta memoria ordinare rientrano generalmente in tre categorie, che impediscono sia compiler reordering e processor reordering:

  • Una sincronizzazione o recinzione di istruzioni leggero, che parlerò in future posts ;
  • Un'istruzione di memoria piena, che ho demonstrated previously;
  • Operazioni di memoria che forniscono semantica di acquisizione o rilascio.

semantica Acquisire impediscono riordino memoria delle operazioni che seguono al fine programma, e rilasciare semantica impediscono memoria riordino delle operazioni precedenti esso. Queste semantiche sono particolarmente adatte allo nei casi in cui esiste una relazione produttore/consumatore, in cui un thread pubblica alcune informazioni e l'altro lo legge. Mi parlerò anche di in un prossimo post.

+0

mi chiedo solo perché GCC asm produce 'addl $ 1,% eax' invece di' incl% eax'? Anche per 'a ++' produce solo 'a + = 1' ... ICC si comporta come previsto. – vaxquis

+1

@vaxquis non mi è chiaro, sembra una qualche forma di ottimizzazione tentata, probabilmente dipendente dalle ipotesi fatte da 'gcc'. –

1

Poiché A = a; e B = b; sono indipendenti in termini di dipendenze dei dati, questo non dovrebbe avere importanza. Se c'era un'uscita/risultato di un'istruzione precedente che influenzava l'input dell'istruzione successiva, quindi ordinare le cose, altrimenti no. questa è un'esecuzione strettamente sequenziale normalmente.

1

La mia lettura è che è necessario per funzionare con lo standard C++; tuttavia, se stai cercando di usarlo per il controllo del multithreading, non funziona in quel contesto perché qui non c'è nulla per garantire che i registri vengano scritti in memoria nell'ordine corretto.

Come indica la modifica, si sta tentando di utilizzarlo esattamente dove non funzionerà.

0

Può essere di interesse che, se si esegue questa operazione:

{ A=a, B=b; /*etc*/ } 

nota la virgola al posto del punto e virgola.

Quindi la specifica C++ e qualsiasi compilatore di conferma dovranno garantire l'ordine di esecuzione poiché gli operandi dell'operatore virgola vengono sempre valutati da sinistra a destra. Questo può essere utilizzato per impedire all'ottimizzatore di sovvertire la sincronizzazione dei thread mediante il riordino. La virgola diventa effettivamente una barriera attraverso la quale non è consentito il riordino.

+0

Oltre all'utilizzo discutibile dell'operatore virgola, questo non indirizza la modifica OP alla domanda. –