2014-05-07 7 views
6

Di seguito sono disponibili due versioni di spinlock. Il primo usa il default che è memory_order_cst mentre il secondo usa memory_order_acquire/memory_order_release. Dal momento che quest'ultimo è più rilassato, mi aspetto che abbia prestazioni migliori. Tuttavia non sembra essere il caso.Perché ho prestazioni peggiori per la mia implementazione di spinlock quando utilizzo un modello di memoria non cst?

class SimpleSpinLock 
{ 
public: 

    inline SimpleSpinLock(): mFlag(ATOMIC_FLAG_INIT) {} 

    inline void lock() 
    { 
     int backoff = 0; 
     while (mFlag.test_and_set()) { DoWaitBackoff(backoff); } 
    } 

    inline void unlock() 
    { 
     mFlag.clear(); 
    } 

private: 

    std::atomic_flag mFlag = ATOMIC_FLAG_INIT; 
}; 

class SimpleSpinLock2 
{ 
public: 

    inline SimpleSpinLock2(): mFlag(ATOMIC_FLAG_INIT) {} 

    inline void lock() 
    { 
     int backoff = 0; 
     while (mFlag.test_and_set(std::memory_order_acquire)) { DoWaitBackoff(backoff); } 
    } 

    inline void unlock() 
    { 
     mFlag.clear(std::memory_order_release); 
    } 

private: 

    std::atomic_flag mFlag = ATOMIC_FLAG_INIT; 
}; 

const int NUM_THREADS = 8; 
const int NUM_ITERS = 5000000; 

const int EXPECTED_VAL = NUM_THREADS * NUM_ITERS; 

int val = 0; 
long j = 0; 

SimpleSpinLock spinLock; 

void ThreadBody() 
{ 
    for (int i = 0; i < NUM_ITERS; ++i) 
    { 
     spinLock.lock(); 

     ++val; 

     j = i * 3.5 + val; 

     spinLock.unlock(); 
    } 
} 

int main() 
{ 
    vector<thread> threads; 

    for (int i = 0; i < NUM_THREADS; ++i) 
    { 
     cout << "Creating thread " << i << endl; 
     threads.push_back(std::move(std::thread(ThreadBody))); 
    } 

    for (thread& thr: threads) 
    { 
     thr.join(); 
    } 

    cout << "Final value: " << val << "\t" << j << endl; 
    assert(val == EXPECTED_VAL); 

    return 1; 
} 

Sono in esecuzione su Ubuntu 12.04 con gcc 4.8.2 in esecuzione ottimizzazione O3.

- Spinlock con memory_order_cst:

Run 1: 
real 0m1.588s 
user 0m4.548s 
sys 0m0.052s 

Run 2: 
real 0m1.577s 
user 0m4.580s 
sys 0m0.032s 

Run 3: 
real 0m1.560s 
user 0m4.436s 
sys 0m0.032s 

- Spinlock con memory_order_acquire/release:

Run 1: 

real 0m1.797s 
user 0m4.608s 
sys 0m0.100s 

Run 2: 

real 0m1.853s 
user 0m4.692s 
sys 0m0.164s 

Run 3: 
real 0m1.784s 
user 0m4.552s 
sys 0m0.124s 

Run 4: 
real 0m1.475s 
user 0m3.596s 
sys 0m0.120s 

Con il modello più rilassato, vedo molto più variabilità. A volte è meglio. Spesso è peggio, qualcuno ha una spiegazione per questo?

+0

Cosa succede se si rimuove il backoff? (Come regola generale, ti consigliamo di eseguire la lettura anziché di un'opzione atomica). – kec

+0

Per GCC su Intel, mi aspetto che si comportino in modo identico se non generano esattamente lo stesso codice. Hai confrontato l'output di assembly di entrambe le versioni di 'ThreadBody'? – Casey

+0

@Casey: si scopre che c'è un recinto aggiuntivo nel modello CST. Dovrei pensare molto seriamente per avere un'opinione sul fatto che sia davvero necessario. – kec

risposta

6

Il codice di sblocco generato è diverso. Il modello di memoria CST (con g ++ 4.9.0) genera:

movb %sil, spinLock(%rip) 
    mfence 

per lo sblocco. Acquisiscono/rilascio genera:

movb %sil, spinLock(%rip) 

il codice di blocco è lo stesso. Qualcun altro dirà qualcosa sul perché è meglio con la recinzione, ma se dovessi indovinare, direi che riduce la contrapposizione bus/cache-coerenza, possibilmente riducendo le interferenze sul bus. A volte più rigoroso è più ordinato e quindi più veloce.

ADDENDUM: in base a this, mfence costa circa 100 cicli. Quindi forse stai riducendo la contesa del bus, perché quando un thread finisce il corpo del loop, si ferma un po 'prima di provare a riacquisire il blocco, lasciando che l'altro thread finisca. Potresti provare a fare la stessa cosa inserendo un breve ciclo di delay dopo lo sblocco, anche se dovresti assicurarti che non si sia ottimizzato.

ADDENDUM2: Sembra essere causato da interferenze/contese del bus causate dal looping troppo veloce. Ho aggiunto un ciclo breve ritardo come:

spinLock.unlock(); 
    for (int i = 0; i < 5; i++) { 
     j = i * 3.5 + val; 
    } 

Ora, l'acquisiscono/release esegue la stessa.

+0

Penso che la risposta sia la contesa del bus. Questo ha senso per me. –