2016-01-07 16 views
7

Il codice seguente dimostra le curiosità della programmazione multi-thread. In particolare le prestazioni di incremento std::memory_order_relaxed rispetto a incremento regolare in un singolo thread. Cosa non capisco perché fetch_add (relaxed) single-threaded è due volte più lento di un normale incremento.atomic fetch_add vs add performance

static void BM_IncrementCounterLocal(benchmark::State& state) { 
    volatile std::atomic_int val2; 

    while (state.KeepRunning()) { 
    for (int i = 0; i < 10; ++i) { 
     DoNotOptimize(val2.fetch_add(1, std::memory_order_relaxed)); 
    } 
    } 
} 
BENCHMARK(BM_IncrementCounterLocal)->ThreadRange(1, 8); 

static void BM_IncrementCounterLocalInt(benchmark::State& state) { 
    volatile int val3 = 0; 

    while (state.KeepRunning()) { 
    for (int i = 0; i < 10; ++i) { 
     DoNotOptimize(++val3); 
    } 
    } 
} 
BENCHMARK(BM_IncrementCounterLocalInt)->ThreadRange(1, 8); 

uscita:

 
     Benchmark        Time(ns) CPU(ns) Iterations 
     ---------------------------------------------------------------------- 
    BM_IncrementCounterLocal/threads:1   59   60 11402509         
     BM_IncrementCounterLocal/threads:2   30   61 11284498         
     BM_IncrementCounterLocal/threads:4   19   62 11373100         
     BM_IncrementCounterLocal/threads:8   17   62 10491608 

    BM_IncrementCounterLocalInt/threads:1  31   31 22592452         
     BM_IncrementCounterLocalInt/threads:2   15   31 22170842         
     BM_IncrementCounterLocalInt/threads:4   8   31 22214640         
     BM_IncrementCounterLocalInt/threads:8   9   31 21889704 
+1

Hai confrontato il codice macchina? –

+0

Devi dire al compilatore che sei in modalità single threaded. –

+1

Sei su un sistema in cui 'memory_order_relaxed' fa qualcosa? Su un x86 no. 'fetch_add' probabilmente emetterà un blocco del bus per rendere l'operazione di aggiunta atomica. 'operator ++' non ha bisogno di farlo. L'istruzione –

risposta

0

La versione locale non utilizza Atomics. (Il fatto che stia usando volatile è una falsariga - volatile non ha sostanzialmente alcun significato nel codice multi-thread).

La versione atomica è utilizzando atomics (!). Il fatto che solo un thread verrà effettivamente utilizzato per accedere alla variabile è invisibile alla CPU, e non sono sorpreso che il compilatore non l'abbia nemmeno notato. (Non ha senso sprecare sforzo sviluppatore capire se è sicuro di trasformare std::atomic_int in int, quando quasi mai sarà. Nessuno scriverà atomic_int se non hanno bisogno di accedervi da più thread.)

Come tale , la versione atomica si prenderà il disturbo di assicurarsi che l'incremento sia effettivamente atomico, e francamente, sono sorpreso che sia solo 2x più lento - mi sarei aspettato più come 10x.

+0

Ho chiesto perché fetch_add è più lento del normale incremento. Penso che tu abbia risposto qualcos'altro :) – Roman

+0

"la versione atomica si prenderà il disturbo di assicurarsi che l'incremento sia effettivamente atomico". 'memory_order_relaxed' dice" Non ci sono vincoli di sincronizzazione o di ordinamento, solo l'atomicità è richiesta per questa operazione. " A seconda del processore (x86 ad esempio), questo può essere costoso come qualsiasi altro ordine di memoria. –

2

Con il volatile int, il compilatore deve assicurarsi di non ottimizzare e/o riordinare le letture/scritture della variabile.

Con fetch_add, la CPU deve prendere precauzioni che l'operazione di lettura-modifica-scrittura sia atomica.

Questi sono due requisiti completamente diversi: il requisito di atomicità indica che la CPU deve comunicare con altre CPU sulla macchina, assicurando che non leggano/scrivano la posizione di memoria specificata tra la propria lettura e scrittura. Se il compilatore compila lo fetch_add usando un'istruzione di confronto e swap, in realtà emetterà un breve ciclo per catturare il caso che qualche altra CPU abbia modificato il valore nel mezzo.

Per la volatile int nessuna comunicazione è necessaria. Al contrario, volatile richiede che il compilatore non inventi letture: volatile è stato progettato per la comunicazione a thread singolo con registri hardware, in cui il semplice atto di lettura del valore potrebbe avere effetti collaterali.

+0

Ho usato std :: memory_order_relaxed quindi non sono state poste barriere di memoria e ho avuto l'impressione che sugli incrementi delle parole dei processori Intel sia atomico così com'è. fetch_add e CAS sono operazioni diverse. – Roman

+0

Perché gli incrementi delle parole dovrebbero essere atomici così come sono? La CPU deve prima recuperare il valore corrente dalla memoria, quindi modificarlo, quindi ripristinarlo. Qualsiasi vincolo di atomicità su questo richiede un notevole sforzo aggiuntivo. La CPU può persino riordinare sia la lettura che la scrittura con altre operazioni di memoria a suo piacimento, a condizione che i risultati * a thread singolo * rimangano gli stessi. Le istruzioni atomiche specializzate sono state aggiunte all'insieme di istruzioni specificamente per consentire l'operazione atomica 'fetch_add' ecc. L'unica cosa che è naturalmente atomica è una singola scrittura o lettura allineata. – cmaster