Quanto è efficace il blocco di un mutex sbloccato? Qual è il costo di un mutex?


149

In un linguaggio di basso livello (C, C ++ o altro): ho la scelta tra avere un mucchio di mutex (come quello che mi dà pthread o qualunque cosa la libreria di sistema nativa fornisca) o uno singolo per un oggetto.

Quanto è efficace bloccare un mutex? Vale a dire quante istruzioni dell'assemblatore sono probabili e quanto tempo impiegano (nel caso in cui il mutex sia sbloccato)?

Quanto costa un mutex? È un problema avere davvero molti mutex? O posso semplicemente lanciare nel mio codice tante variabili mutex quante sono le intvariabili e non importa davvero?

(Non sono sicuro di quante differenze ci siano tra i diversi hardware. In tal caso, vorrei anche conoscerli. Ma soprattutto, sono interessato all'hardware comune.)

Il punto è che, usando molti mutex, ognuno dei quali copre solo una parte dell'oggetto anziché un singolo mutex per l'intero oggetto, ho potuto proteggere molti blocchi. E mi chiedo fino a che punto dovrei andare al riguardo. Vale a dire che dovrei provare a proteggere qualsiasi blocco possibile il più possibile, non importa quanto più complicato e quanti più mutex questo significhi?


Il post sul blog di WebKits (2016) sul blocco è molto legato a questa domanda e spiega le differenze tra uno spinlock, un blocco adattivo, un futex, ecc.


Questo sarà specifico per l'implementazione e l'architettura. Alcuni mutex non costeranno quasi nulla se c'è il supporto hardware nativo, altri costeranno molto. È impossibile rispondere senza ulteriori informazioni.
Gian

2
@Gian: Beh, ovviamente insinuo questa domanda nella mia domanda. Vorrei sapere dell'hardware comune ma anche notevoli eccezioni se ce ne sono.
Albert,

Davvero non vedo quell'implicazione da nessuna parte. Ti chiedi "istruzioni assembler" - la risposta potrebbe essere ovunque da 1 istruzione a diecimila istruzioni a seconda dell'architettura di cui stai parlando.
Gian

15
@Gian: Quindi, per favore, dai esattamente questa risposta. Si prega di dire che cosa è effettivamente su x86 e amd64, si prega di dare un esempio per un'architettura in cui è 1 istruzione e dare uno dove è 10k. Non è chiaro che voglio saperlo dalla mia domanda?
Albert,

Risposte:


120

Ho la scelta tra avere un mucchio di mutex o uno singolo per un oggetto.

Se hai molti thread e l'accesso all'oggetto avviene spesso, più blocchi aumenterebbero il parallelismo. A scapito della manutenibilità, poiché un maggior numero di blocchi comporta un maggior debug del blocco.

Quanto è efficace bloccare un mutex? Vale a dire quante istruzioni dell'assemblatore sono probabili e quanto tempo impiegano (nel caso in cui il mutex sia sbloccato)?

Le istruzioni precise dell'assemblatore sono il minimo sovraccarico di un mutex : le garanzie di coerenza memoria / cache sono il sovraccarico principale. E meno spesso viene preso un blocco particolare - meglio.

Il mutex è composto da due parti principali (semplificazione eccessiva): (1) una bandiera che indica se il mutex è bloccato o meno e (2) attendere la coda.

Il cambio del flag è solo poche istruzioni e normalmente viene eseguito senza chiamata di sistema. Se mutex è bloccato, accadrà syscall per aggiungere il thread chiamante nella coda di attesa e iniziare l'attesa. Lo sblocco, se la coda di attesa è vuota, è economico ma per il resto è necessario un syscall per riattivare uno dei processi di attesa. (Su alcuni sistemi vengono utilizzati syscall economici / veloci per implementare i mutex, che diventano chiamate di sistema lente (normali) solo in caso di contesa.)

Il blocco del mutex sbloccato è davvero economico. Anche lo sblocco di mutex senza contesa è economico.

Quanto costa un mutex? È un problema avere davvero molti mutex? O posso semplicemente lanciare nel mio codice tante variabili mutex quante sono le variabili int e non importa davvero?

Puoi inserire nel tuo codice tutte le variabili mutex che desideri. Sei limitato solo dalla quantità di memoria che l'applicazione può allocare.

Sommario. I blocchi dello spazio utente (e in particolare i mutex) sono economici e non soggetti a limiti di sistema. Ma troppi di loro sono un incubo per il debug. Tavolo semplice:

  1. Meno blocchi significa più contese (rallentamenti, blocchi CPU) e minore parallelismo
  2. Meno blocchi significa meno problemi nel debug di problemi multi-threading.
  3. Più blocchi significa meno contese e maggiore parallelismo
  4. Più blocchi significa maggiori possibilità di imbattersi in deadlock indefinibili.

È necessario trovare e mantenere uno schema di blocco bilanciato per l'applicazione, generalmente bilanciando il n. 2 e il n. 3.


(*) Il problema con i mutex bloccati meno spesso è che se si dispone di un blocco eccessivo nell'applicazione, il traffico inter-CPU / core causa lo scaricamento della memoria mutex dalla cache dei dati di altre CPU per garantire coerenza della cache. Le operazioni di svuotamento della cache sono come interruzioni leggere e gestite in modo trasparente dalle CPU, ma introducono le cosiddette bancarelle (ricerca di "stallo").

E le bancarelle sono ciò che rende il codice di blocco lento, spesso senza alcuna indicazione apparente del perché l'applicazione è lenta. (Alcuni arch forniscono le statistiche sul traffico tra CPU / core, altri no.)

Per evitare il problema, le persone generalmente ricorrono a un numero elevato di blocchi per ridurre la probabilità di contese e per evitare lo stallo. Questo è il motivo per cui esiste il blocco dello spazio utente economico, non soggetto ai limiti del sistema.


Grazie, questo risponde principalmente alla mia domanda. Non sapevo che il kernel (ad esempio il kernel Linux) gestisse i mutex e tu li controllassi tramite syscalls. Ma poiché lo stesso Linux gestisce gli switch di programmazione e contesto, questo ha senso. Ma ora ho una vaga immaginazione su cosa farà internamente il blocco / sblocco del mutex.
Albert,

2
@Albert: Oh. Ho dimenticato gli interruttori di contesto ... Gli interruttori di contesto sono troppo drenanti per le prestazioni. Se l'acquisizione del blocco fallisce e il thread deve attendere, questa è una sorta di metà del cambio di contesto. CS stesso è veloce, ma poiché la CPU potrebbe essere utilizzata da altri processi, le cache verrebbero riempite con dati alieni. Dopo che il thread ha finalmente acquisito il blocco, è probabile che alla CPU dovrebbe ricaricare praticamente tutto da RAM.
Dummy00001,

@ Dummy00001 Il passaggio a un altro processo significa che è necessario modificare i mapping di memoria della CPU. Non è così economico.
curiousguy,

27

Volevo sapere la stessa cosa, quindi l'ho misurata. Sulla mia scatola (processore a otto core AMD FX (tm) -8150 a 3,612361 GHz), il blocco e lo sblocco di un mutex sbloccato che si trova nella sua linea di cache ed è già memorizzato nella cache, richiede 47 clock (13 ns).

A causa della sincronizzazione tra due core (ho usato CPU # 0 e # 1), ho potuto chiamare una coppia di blocco / sblocco solo una volta ogni 102 ns su due thread, quindi una volta ogni 51 ns, da cui si può concludere che ci vogliono circa 38 ns per recuperare dopo che un thread ha sbloccato prima che il thread successivo possa bloccarlo di nuovo.

Il programma che ho usato per indagare su questo può essere trovato qui: https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx

Nota che ha alcuni valori hardcoded specifici per il mio box (xrange, yrange e rdtsc overhead), quindi probabilmente dovrai sperimentare prima che funzioni per te.

Il grafico che produce in quello stato è:

inserisci qui la descrizione dell'immagine

Ciò mostra il risultato delle esecuzioni di benchmark sul seguente codice:

uint64_t do_Ndec(int thread, int loop_count)
{
  uint64_t start;
  uint64_t end;
  int __d0;

  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
  mutex.lock();
  mutex.unlock();
  asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
  asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
  return end - start;
}

Le due chiamate rdtsc misurano il numero di orologi necessari per bloccare e sbloccare `mutex '(con un overhead di 39 orologi per le chiamate rdtsc sulla mia scatola). Il terzo asm è un loop di ritardo. La dimensione del loop di ritardo è inferiore di 1 conteggio per il thread 1 rispetto a quella per il thread 0, quindi il thread 1 è leggermente più veloce.

La funzione sopra è chiamata in un loop stretto di dimensioni 100.000. Nonostante la funzione sia leggermente più veloce per il thread 1, entrambi i loop si sincronizzano a causa della chiamata al mutex. Ciò è visibile nel grafico dal fatto che il numero di orologi misurati per la coppia di blocco / sblocco è leggermente maggiore per il thread 1, per tenere conto del ritardo più breve nel loop sottostante.

Nel grafico sopra il punto in basso a destra è una misura con un ritardo loop_count di 150, e quindi seguendo i punti in basso, verso sinistra, il loop_count viene ridotto di uno ogni misura. Quando diventa 77 la funzione viene chiamata ogni 102 ns in entrambi i thread. Se successivamente loop_count viene ulteriormente ridotto, non è più possibile sincronizzare i thread e il mutex inizia a essere effettivamente bloccato per la maggior parte del tempo, con conseguente aumento del numero di clock necessari per eseguire il blocco / sblocco. Anche il tempo medio della chiamata di funzione aumenta per questo motivo; quindi i punti della trama ora salgono e vanno di nuovo a destra.

Da ciò possiamo concludere che bloccare e sbloccare un mutex ogni 50 ns non è un problema sulla mia scatola.

Tutto sommato la mia conclusione è che la risposta alla domanda di OP è che l'aggiunta di più mutex è migliore purché si traduca in meno contese.

Prova a bloccare i mutex il più breve possibile. L'unico motivo per metterli -say- al di fuori di un loop sarebbe se quel loop si muove più rapidamente di una volta ogni 100 ns (o meglio, numero di thread che vogliono eseguire quel loop contemporaneamente per 50 ns) o quando 13 ns volte la dimensione del loop è maggiore del ritardo che si ottiene per contesa.

EDIT: Sono diventato molto più informato sull'argomento ora e inizio a dubitare della conclusione che ho presentato qui. Innanzitutto, la CPU 0 e 1 risultano essere hyper-thread; anche se AMD afferma di avere 8 core reali, c'è sicuramente qualcosa di molto sospetto perché i ritardi tra altri due core sono molto più grandi (cioè 0 e 1 formano una coppia, così come 2 e 3, 4 e 5, e 6 e 7 ). In secondo luogo, lo std :: mutex è implementato in modo da far girare i blocchi per un po 'prima di fare effettivamente chiamate di sistema quando non riesce a ottenere immediatamente il blocco su un mutex (che senza dubbio sarà estremamente lento). Quindi quello che ho misurato qui è la posizione più ideale in assoluto e in pratica il blocco e lo sblocco potrebbero richiedere molto più tempo per blocco / sblocco.

In conclusione, un mutex è implementato con l'atomica. Per sincronizzare gli atomici tra i core, è necessario bloccare un bus interno che congela la corrispondente linea di cache per diverse centinaia di cicli di clock. Nel caso in cui non sia possibile ottenere un blocco, è necessario eseguire una chiamata di sistema per mettere in pausa il thread; è ovviamente estremamente lento (le chiamate di sistema sono nell'ordine di 10 mircosecondi). Normalmente questo non è davvero un problema perché quel thread deve dormire comunque-- ma potrebbe essere un problema con alta contesa in cui un thread non può ottenere il blocco per il tempo che gira normalmente e così fa la chiamata di sistema, ma PU CAN prendere la serratura poco dopo. Ad esempio, se più thread bloccano e sbloccano un mutex in un ciclo stretto e ciascuno mantiene il blocco per 1 microsecondo circa, allora potrebbero essere rallentati enormemente dal fatto che vengono costantemente messi a dormire e svegliati di nuovo. Inoltre, una volta che un thread dorme e un altro thread deve riattivarlo, quel thread deve fare una chiamata di sistema ed è ritardato di ~ 10 microsecondi; questo ritardo si verifica quindi quando si sblocca un mutex quando un altro thread è in attesa di quel mutex nel kernel (dopo che lo spin ha richiesto troppo tempo).


10

Questo dipende da ciò che in realtà si chiama "mutex", modalità OS ecc.

Al minimo è un costo di un'operazione di memoria interbloccato. È un'operazione relativamente pesante (rispetto ad altri comandi dell'assemblatore primitivo).

Tuttavia, questo può essere molto più alto. Se quello che chiami "mutex" un oggetto kernel (ovvero - oggetto gestito dal sistema operativo) ed eseguito in modalità utente - ogni operazione su di esso porta a una transazione in modalità kernel, che è molto pesante.

Ad esempio sul processore Intel Core Duo, Windows XP. Funzionamento interbloccato: dura circa 40 cicli della CPU. Chiamata in modalità kernel (cioè chiamata di sistema) - circa 2000 cicli CPU.

In questo caso, puoi prendere in considerazione l'uso di sezioni critiche. È un ibrido tra un kernel mutex e l'accesso alla memoria interbloccata.


7
Le sezioni critiche di Windows sono molto più vicine ai mutex. Hanno una semantica mutex regolare, ma sono processi locali. L'ultima parte li rende molto più veloci, poiché possono essere gestiti interamente all'interno del processo (e quindi codice in modalità utente).
MSalters,

2
Il numero sarebbe più utile se anche la quantità di cicli CPU di operazioni comuni (ad es. Aritmetica / if-else / cache-miss / indirection) fosse fornita anche per il confronto. .... Sarebbe anche bello se ci fosse qualche riferimento al numero. In Internet, è molto difficile trovare tali informazioni.
javaLover

@javaLover Le operazioni non vengono eseguite su cicli; corrono su unità aritmetiche per un numero di cicli. È molto diverso. Il costo di una qualsiasi istruzione nel tempo non è una quantità definita, ma solo il costo delle risorse. Queste risorse sono condivise. L'impatto delle istruzioni di memoria dipende molto dalla memorizzazione nella cache, ecc.
curiousguy,

@curiousguy Accetto. Non ero chiaro. Vorrei rispondere come std::mutexusare mediamente la durata (al secondo) 10 volte di più di int++. Tuttavia, so che è difficile rispondere perché dipende molto da molte cose.
javaLover l'

6

Il costo varierà a seconda dell'implementazione, ma dovresti tenere a mente due cose:

  • il costo sarà molto probabilmente minimo poiché è un'operazione alquanto primitiva e sarà ottimizzata il più possibile grazie al suo modello di utilizzo (usato molto ).
  • non importa quanto sia costoso poiché è necessario utilizzarlo se si desidera un funzionamento multi-thread sicuro. Se ne hai bisogno, allora ne hai bisogno.

Sui sistemi a singolo processore, in genere è possibile disabilitare gli interrupt abbastanza a lungo da modificare atomicamente i dati. I sistemi multiprocessore possono utilizzare una strategia di test e impostazione .

In entrambi i casi, le istruzioni sono relativamente efficienti.

Se devi fornire un singolo mutex per una struttura di dati di grandi dimensioni o avere molti mutex, uno per ogni sezione di esso, è un atto di bilanciamento.

Avendo un singolo mutex, hai un rischio maggiore di contesa tra più thread. Puoi ridurre questo rischio avendo un mutex per sezione ma non vuoi entrare in una situazione in cui un thread deve bloccare 180 mutex per fare il suo lavoro :-)


1
Sì, ma quanto è efficiente? È una singola istruzione della macchina? O circa 10? O circa 100? 1000? Di Più? Tutto ciò è ancora efficiente, tuttavia può fare la differenza in situazioni estreme.
Albert,

1
Bene, questo dipende interamente dall'implementazione. È possibile disattivare gli interrupt, testare / impostare un numero intero e riattivare gli interrupt in un ciclo in circa sei istruzioni della macchina. Test-and-set possono essere eseguiti in altrettanti poiché i processori tendono a fornire ciò come una singola istruzione.
paxdiablo,

Un test-and-set bloccato dal bus è una singola (piuttosto lunga) istruzione su x86. Il resto dei macchinari per utilizzarlo è piuttosto rapido ("il test ha avuto successo?" È una domanda che le CPU sono brave a fare velocemente) ma è la lunghezza dell'istruzione bloccata dal bus che conta davvero in quanto è la parte che blocca le cose. Le soluzioni con interruzioni sono molto più lente, poiché la loro manipolazione è in genere limitata al kernel del sistema operativo per bloccare banali attacchi DoS.
Donal Fellows,

A proposito, non usare drop / reaquire come mezzo per far cedere un thread agli altri; questa è una strategia che fa schifo su un sistema multicore. (È una delle poche cose che CPython sbaglia.)
Donal Fellows

@Donal: cosa intendi con drop / reaquire? Sembra importante; puoi darmi maggiori informazioni a riguardo?
Albert,

5

Sono completamente nuovo su pthreads e mutex, ma posso confermare dalla sperimentazione che il costo del blocco / sblocco di un mutex è quasi zero quando non c'è contesa, ma quando c'è contesa, il costo del blocco è estremamente alto. Ho eseguito un semplice codice con un pool di thread in cui il compito era solo quello di calcolare una somma in una variabile globale protetta da un blocco mutex:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

Con un thread, il programma somma 10.000.000 di valori praticamente istantaneamente (meno di un secondo); con due thread (su un MacBook con 4 core), lo stesso programma richiede 39 secondi.

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.