È volatile costoso?


111

Dopo aver letto The JSR-133 Cookbook for Compiler Writers sull'implementazione di volatile, in particolare la sezione "Interazioni con istruzioni atomiche", presumo che la lettura di una variabile volatile senza aggiornarla abbia bisogno di un LoadLoad o di una barriera LoadStore. Più in basso nella pagina vedo che LoadLoad e LoadStore sono effettivamente no-op sulle CPU X86. Ciò significa che le operazioni di lettura volatile possono essere eseguite senza un'esplicita invalidazione della cache su x86, ed è veloce come una normale lettura di variabili (ignorando i vincoli di riordino di volatile)?

Credo di non capirlo correttamente. Qualcuno potrebbe preoccuparsi di illuminarmi?

EDIT: mi chiedo se ci siano differenze negli ambienti multiprocessore. Su sistemi a CPU singola la CPU potrebbe guardare le proprie cache dei thread, come afferma John V., ma su sistemi multi CPU ci deve essere qualche opzione di configurazione per le CPU che non è sufficiente e la memoria principale deve essere colpita, rendendo il volatile più lento su sistemi multi cpu, giusto?

PS: Mentre cercavo di saperne di più su questo, sono incappato nei seguenti fantastici articoli e poiché questa domanda potrebbe essere interessante per gli altri, condividerò i miei collegamenti qui:


1
Puoi leggere la mia modifica sulla configurazione con più CPU a cui ti riferisci. Può accadere che su sistemi multi CPU per un riferimento di breve durata, non si verifichi più di una singola lettura / scrittura nella memoria principale.
John Vint

2
la lettura volatile in sé non è costosa. il costo principale è come impedisce le ottimizzazioni. in pratica neanche quel costo in media non è molto alto, a meno che il volatile non venga utilizzato in un ciclo ristretto.
irreputabile

2
Anche questo articolo su infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) potrebbe interessarti, mostra gli effetti di volatile e sincronizzato sul codice generato per diverse architetture. Questo è anche un caso in cui la jvm può funzionare meglio di un compilatore in anticipo, poiché sa se è in esecuzione su un sistema monoprocessore e può omettere alcune barriere di memoria.
Jörn Horstmann

Risposte:


123

Su Intel una lettura volatile non contestata è abbastanza economica. Se consideriamo il seguente semplice caso:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Utilizzando la capacità di Java 7 di stampare il codice assembly, il metodo run assomiglia a:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Se guardi i 2 riferimenti a getstatic, il primo implica un caricamento dalla memoria, il secondo salta il caricamento poiché il valore viene riutilizzato dai registri in cui è già caricato (lungo è 64 bit e sul mio laptop a 32 bit utilizza 2 registri).

Se rendiamo volatile la variabile l, l'assemblaggio risultante è diverso.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

In questo caso entrambi i riferimenti getstatici alla variabile l comportano un caricamento dalla memoria, cioè il valore non può essere mantenuto in un registro su più letture volatili. Per garantire che ci sia una lettura atomica, il valore viene letto dalla memoria principale in un registro MMX movsd 0x6fb7b2f0(%ebp),%xmm0rendendo l'operazione di lettura una singola istruzione (dall'esempio precedente abbiamo visto che il valore a 64 bit normalmente richiederebbe due letture a 32 bit su un sistema a 32 bit).

Quindi il costo complessivo di una lettura volatile sarà più o meno equivalente a un carico di memoria e può essere economico quanto un accesso alla cache L1. Tuttavia, se un altro core sta scrivendo sulla variabile volatile, la riga della cache verrà invalidata richiedendo una memoria principale o forse un accesso alla cache L3. Il costo effettivo dipenderà fortemente dall'architettura della CPU. Anche tra Intel e AMD i protocolli di coerenza della cache sono diversi.


nota a margine, java 6 ha la stessa capacità di mostrare l'assemblaggio (è l'hotspot che lo fa)
bestsss

+1 In JDK5 il volatile non può essere riordinato rispetto a qualsiasi lettura / scrittura (che corregge il blocco del doppio controllo, ad esempio). Ciò implica che influenzerà anche il modo in cui i campi non volatili vengono manipolati? Sarebbe interessante combinare l'accesso a campi volatili e non volatili.
ewernli

@evemli, devi stare attento, ho fatto questa affermazione io stesso una volta, ma è risultata errata. C'è un caso limite. Il modello di memoria Java consente la semantica da motel roach, quando i negozi possono essere riordinati prima dei negozi volatili. Se hai preso questo dall'articolo di Brian Goetz sul sito IBM, allora vale la pena ricordare che questo articolo semplifica eccessivamente la specifica JMM.
Michael Barker

20

In generale, sulla maggior parte dei processori moderni un carico volatile è paragonabile a un carico normale. Un negozio volatile è circa 1/3 del tempo di un monitor-enter / monitor-exit. Questo è visto su sistemi che sono coerenti con la cache.

Per rispondere alla domanda dell'OP, le scritture volatili sono costose mentre le letture di solito non lo sono.

Ciò significa che le operazioni di lettura volatile possono essere eseguite senza un'esplicita invalidazione della cache su x86, ed è veloce come una normale lettura di variabili (ignorando i vincoli di riordino di volatile)?

Sì, a volte durante la convalida di un campo la CPU potrebbe non raggiungere nemmeno la memoria principale, invece spiare altre cache di thread e ottenere il valore da lì (spiegazione molto generale).

Tuttavia, secondo il suggerimento di Neil, se hai un campo a cui accedono più thread, devi avvolgerlo come AtomicReference. Essendo un AtomicReference, esegue all'incirca lo stesso throughput per letture / scritture, ma è anche più ovvio che il campo sarà accessibile e modificato da più thread.

Modifica per rispondere alla modifica dell'OP:

La coerenza della cache è un protocollo un po 'complicato, ma in breve: le CPU condivideranno una linea di cache comune collegata alla memoria principale. Se una CPU carica la memoria e nessun'altra CPU l'ha avuta, la CPU presumerà che sia il valore più aggiornato. Se un'altra CPU tenta di caricare la stessa posizione di memoria, la CPU già caricata ne sarà consapevole e condividerà effettivamente il riferimento memorizzato nella cache alla CPU richiedente - ora la CPU richiesta ha una copia di quella memoria nella sua cache della CPU. (Non ha mai dovuto cercare nella memoria principale il riferimento)

C'è un po 'più di protocollo coinvolto, ma questo dà un'idea di cosa sta succedendo. Anche per rispondere all'altra tua domanda, con l'assenza di più processori, le letture / scritture volatili possono infatti essere più veloci rispetto a più processori. Ci sono alcune applicazioni che potrebbero essere eseguite più velocemente contemporaneamente con una singola CPU e quindi con più.


5
Un AtomicReference è solo un wrapper per un campo volatile con funzioni native aggiunte che forniscono funzionalità aggiuntive come getAndSet, compareAndSet ecc., Quindi dal punto di vista delle prestazioni utilizzarlo è utile se hai bisogno della funzionalità aggiunta. Ma mi chiedo perché ti riferisci al sistema operativo qui? La funzionalità è implementata direttamente nei codici operativi della CPU. E questo implica che su sistemi con più processori, dove una CPU non è a conoscenza del contenuto della cache di altre CPU, i volatili sono più lenti perché le CPU devono sempre raggiungere la memoria principale?
Daniel

Hai ragione, mi manca ho parlato del sistema operativo che avrei dovuto scrivere CPU, risolvendolo ora. E sì, so che AtomicReference è semplicemente un wrapper per campi volatili, ma aggiunge anche come una sorta di documentazione che il campo stesso sarà accessibile da più thread.
John Vint

@ John, perché dovresti aggiungere un altro riferimento indiretto tramite AtomicReference? Se hai bisogno di CAS, ok, ma AtomicUpdater potrebbe essere un'opzione migliore. Per quanto ricordo non ci sono elementi intrinseci su AtomicReference.
bestsss

@bestsss Per tutti gli scopi generali, hai ragione che non c'è differenza tra AtomicReference.set / get e carico volatile e negozi. Detto questo, ho avuto la stessa sensazione (e lo faccio in una certa misura) su quando usare quale. Questa risposta può dettagliarla un po ' stackoverflow.com/questions/3964317/… . L'utilizzo di entrambi è più di una preferenza, il mio unico argomento a favore dell'uso di AtomicReference su un semplice volatile è per una documentazione chiara - che di per sé non è l'argomento più grande che io capisca
John Vint

In una nota a margine alcuni sostengono che l'utilizzo di un campo volatile / AtomicReference (senza la necessità di un CAS) porta a un codice difettoso old.nabble.com/…
John Vint

12

Nelle parole del Java Memory Model (come definito per Java 5+ in JSR 133), qualsiasi operazione - lettura o scrittura - su una volatilevariabile crea una relazione accade prima rispetto a qualsiasi altra operazione sulla stessa variabile. Ciò significa che il compilatore e JIT sono costretti a evitare determinate ottimizzazioni come il riordino delle istruzioni all'interno del thread o l'esecuzione di operazioni solo all'interno della cache locale.

Poiché alcune ottimizzazioni non sono disponibili, il codice risultante è necessariamente più lento di quanto sarebbe stato, anche se probabilmente non di molto.

Tuttavia non dovresti creare una variabile a volatilemeno che tu non sappia che vi si accederà da più thread al di fuori dei synchronizedblocchi. Anche in questo caso dovresti considerare se volatile è la scelta migliore rispetto a synchronized, AtomicReferencee ai suoi amici, alle Lockclassi esplicite , ecc.


4

L'accesso a una variabile volatile è per molti versi simile al wrapping dell'accesso a una variabile ordinaria in un blocco sincronizzato. Ad esempio, l'accesso a una variabile volatile impedisce alla CPU di riordinare le istruzioni prima e dopo l'accesso, e questo generalmente rallenta l'esecuzione (anche se non posso dire di quanto).

Più in generale, su un sistema multiprocessore non vedo come sia possibile accedere a una variabile volatile senza penalità: deve esserci un modo per garantire che una scrittura sul processore A sia sincronizzata con una lettura sul processore B.


4
La lettura di variabili volatili ha la stessa penalità rispetto a un ingresso monitor, per quanto riguarda le possibilità di riordino delle istruzioni, mentre la scrittura di una variabile volatile equivale a un'uscita monitor. Una differenza potrebbe essere quali variabili (ad esempio cache del processore) vengono scaricate o invalidate. Sebbene la sincronizzazione scarichi o invalidi tutto, l'accesso alla variabile volatile dovrebbe sempre ignorare la cache.
Daniel

12
-1, l'accesso a una variabile volatile è leggermente diverso dall'utilizzo di un blocco sincronizzato. L'immissione di un blocco sincronizzato richiede una scrittura basata su compareAndSet atomica per rimuovere il blocco e una scrittura volatile per rilasciarlo. Se il blocco è soddisfatto, il controllo deve passare dallo spazio utente allo spazio kernel per arbitrare il blocco (questo è il bit costoso). L'accesso a un volatile rimarrà sempre nello spazio utente.
Michael Barker

@MichaelBarker: Sei sicuro che tutti i monitor debbano essere protetti dal kernel e non dall'app?
Daniel

@ Daniel: se rappresenti un monitor utilizzando un blocco sincronizzato o un blocco, allora sì, ma solo se il monitor è soddisfatto. L'unico modo per farlo senza l'arbitrato del kernel è usare la stessa logica, ma girare occupato invece di parcheggiare il thread.
Michael Barker

@MichaelBarker: Okey, per serrature soddisfatte lo capisco.
Daniel
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.