Qual è la differenza tra atomic e critical in OpenMP?
posso farlo
#pragma omp atomic
g_qCount++;
ma non è lo stesso di
#pragma omp critical
g_qCount++;
?
Qual è la differenza tra atomic e critical in OpenMP?
posso farlo
#pragma omp atomic
g_qCount++;
ma non è lo stesso di
#pragma omp critical
g_qCount++;
?
Risposte:
L'effetto su g_qCount è lo stesso, ma ciò che viene fatto è diverso.
Una sezione critica di OpenMP è completamente generale: può circondare qualsiasi blocco arbitrario di codice. Tuttavia, si paga per questa generalità incorrendo in un overhead significativo ogni volta che un thread entra ed esce dalla sezione critica (oltre al costo intrinseco della serializzazione).
(Inoltre, in OpenMP tutte le sezioni critiche senza nome sono considerate identiche (se preferisci, c'è solo un blocco per tutte le sezioni critiche senza nome), in modo che se un thread è in una sezione critica [senza nome] come sopra, nessun thread può entrare [sezione critica senza nome]. Come puoi immaginare, puoi aggirare questo problema utilizzando sezioni critiche con nome).
Un'operazione atomica ha un sovraccarico molto inferiore. Dove disponibile, sfrutta l'hardware fornendo (diciamo) un'operazione di incremento atomico; in tal caso non è necessario alcun blocco / sblocco per entrare / uscire dalla riga di codice, esegue solo l'incremento atomico con cui l'hardware dice che non è possibile interferire.
I lati positivi sono che l'overhead è molto più basso e che un thread in un'operazione atomica non blocca alcuna (diversa) operazione atomica che sta per verificarsi. Lo svantaggio è l'insieme limitato di operazioni supportate da atomic.
Ovviamente, in entrambi i casi, dovrai sostenere il costo della serializzazione.
In OpenMP, tutte le sezioni critiche senza nome si escludono a vicenda.
La differenza più importante tra critico e atomico è che atomic può proteggere solo un singolo incarico e puoi usarlo con operatori specifici.
Sezione critica:
Può essere esteso per serializzare gruppi di blocchi con un uso appropriato del tag "name".
Più lentamente!
Operazione atomica:
È molto più veloce!
Assicura solo la serializzazione di una particolare operazione.
Il modo più veloce non è né critico né atomico. Approssimativamente, l'aggiunta con sezione critica è 200 volte più costosa dell'aggiunta semplice, l'aggiunta atomica è 25 volte più costosa dell'aggiunta semplice.
L'opzione più veloce (non sempre applicabile) è quella di dare a ogni thread il proprio contatore e di effettuare un'operazione di riduzione quando è necessaria la somma totale.
I limiti di atomic
sono importanti. Dovrebbero essere dettagliati sul specifiche di OpenMP . MSDN offre un breve cheat sheet in quanto non sarei sorpreso se questo non cambierà. (Visual Studio 2012 ha un'implementazione di OpenMP da marzo 2002.) Per citare MSDN:
La dichiarazione dell'espressione deve avere una delle seguenti forme:
x
binop =expr
x++
++x
x--
--x
Nelle espressioni precedenti:
x
èlvalue
un'espressione di tipo scalare.expr
è un'espressione con tipo scalare e non fa riferimento all'oggetto designato dax
. binop non è un operatore sovraccarico ed è uno dei+
,*
,-
,/
,&
,^
,|
,<<
, o>>
.
Raccomando di usare atomic
quando è possibile e altrimenti denominate sezioni critiche. Denominarli è importante; eviterai il debug dei mal di testa in questo modo.
Già ottime spiegazioni qui. Tuttavia, possiamo immergerci un po 'più a fondo. Per comprendere la differenza fondamentale tra i concetti di sezione atomica e critica in OpenMP, dobbiamo prima comprendere il concetto di blocco . Rivediamo perché è necessario utilizzare i lucchetti .
Un programma parallelo viene eseguito da più thread. I risultati deterministici si verificheranno se e solo se eseguiamo la sincronizzazione tra questi thread. Ovviamente, la sincronizzazione tra i thread non è sempre richiesta. Ci riferiamo a quei casi in cui è necessaria la sincronizzazione .
Per sincronizzare i thread in un programma multi-thread, useremo lock . Quando l'accesso deve essere limitato da un solo thread alla volta, entrano in gioco i lock . L' implementazione del concetto di blocco può variare da processore a processore. Scopriamo insieme come può funzionare una semplice serratura da un punto di vista algoritmico.
1. Define a variable called lock.
2. For each thread:
2.1. Read the lock.
2.2. If lock == 0, lock = 1 and goto 3 // Try to grab the lock
Else goto 2.1 // Wait until the lock is released
3. Do something...
4. lock = 0 // Release the lock
L'algoritmo dato può essere implementato nel linguaggio hardware come segue. Assumeremo un singolo processore e analizzeremo il comportamento dei blocchi in questo. Per questa pratica, supponiamo uno dei seguenti processori: MIPS , Alpha , ARM o Power .
try: LW R1, lock
BNEZ R1, try
ADDI R1, R1, #1
SW R1, lock
Questo programma sembra essere OK, ma non lo è. Il codice precedente soffre del problema precedente; sincronizzazione . Troviamo il problema. Supponiamo che il valore iniziale di lock sia zero. Se due thread eseguono questo codice, uno potrebbe raggiungere il SW R1, bloccarsi prima che l'altro legga la variabile lock . Quindi, entrambi pensano che il lucchetto sia libero. Per risolvere questo problema, viene fornita un'altra istruzione piuttosto che semplici LW e SW . Si chiama istruzione di lettura-modifica-scrittura . È un'istruzione complessa (composta da sottoistruzioni) che assicura che la procedura di acquisizione del blocco venga eseguita da un solothread alla volta. La differenza di lettura-modifica-scrittura rispetto alle semplici istruzioni di lettura e scrittura è che utilizza un modo diverso di caricamento e archiviazione . Utilizza LL (Load Linked) per caricare la variabile di blocco e SC (Store Conditional) per scrivere nella variabile di blocco. Un registro di collegamento aggiuntivo viene utilizzato per garantire che la procedura di acquisizione del blocco venga eseguita da un singolo thread. L'algoritmo è fornito di seguito.
1. Define a variable called lock.
2. For each thread:
2.1. Read the lock and put the address of lock variable inside the Link Register.
2.2. If (lock == 0) and (&lock == Link Register), lock = 1 and reset the Link Register then goto 3 // Try to grab the lock
Else goto 2.1 // Wait until the lock is released
3. Do something...
4. lock = 0 // Release the lock
Quando il registro di collegamento viene ripristinato, se un altro thread ha assunto che il blocco sia libero, non sarà in grado di scrivere nuovamente il valore incrementato nel blocco. Pertanto, viene acquisita la concorrenza di accesso alla variabile lock .
La differenza fondamentale tra critico e atomico deriva dall'idea che:
Perché usare i lock (una nuova variabile) mentre possiamo usare la variabile effettiva (su cui stiamo eseguendo un'operazione), come una variabile di lock?
L'uso di una nuova variabile per i blocchi porterà a una sezione critica , mentre l'uso della variabile effettiva come blocco porterà al concetto atomico . La sezione critica è utile quando eseguiamo molti calcoli (più di una riga) sulla variabile effettiva. Questo perché, se il risultato di quei calcoli non viene scritto sulla variabile effettiva, l'intera procedura dovrebbe essere ripetuta per calcolare i risultati. Ciò può portare a prestazioni scadenti rispetto all'attesa del rilascio del blocco prima di entrare in una regione altamente computazionale. Pertanto, si consiglia di utilizzare la direttiva atomic ogni volta che si desidera eseguire un singolo calcolo (x ++, x--, ++ x, --x, ecc.) E utilizzare una direttiva critica quando una regione più complessa dal punto di vista computazionale viene eseguita dalla sezione intensiva.
atomic è relativamente efficiente in termini di prestazioni quando è necessario abilitare l'esclusione reciproca solo per una singola istruzione simile non è vero per omp critical.
atomic è una sezione critica di una singola istruzione, ovvero si blocca per l'esecuzione di un'istruzione
la sezione critica è un blocco su un blocco di codice
Un buon compilatore tradurrà il tuo secondo codice nello stesso modo in cui fa il primo
++
e*=
) e che se non sono supportate nell'hardware, potrebbero essere sostituite dacritical
sezioni.