2011-01-08 20 views
94

Dopo aver letto The JSR-133 Cookbook for Compiler Writers sull'implementazione di volatile, in particolare la sezione "Interazioni con le istruzioni atomiche", presumo che la lettura di una variabile volatile senza aggiornamento richieda una LoadLoad o una barriera LoadStore. Più in basso nella pagina vedo che LoadLoad e LoadStore sono effettivamente non operativi sulle CPU X86. Questo significa che le operazioni di lettura volatili possono essere eseguite senza una invalidazione esplicita della cache su x86, ed è veloce quanto una variabile normale letta (ignorando i vincoli di riordino di volatile)?È volatile costoso?

Credo di non capire questo correttamente. Qualcuno potrebbe volermi illuminare?

EDIT: Mi chiedo se ci sono differenze negli ambienti multiprocessore. Sui sistemi a CPU singola la CPU potrebbe esaminare le proprie cache di thread, come afferma John V., ma su sistemi multi CPU ci deve essere qualche opzione di configurazione per le CPU che non è abbastanza e la memoria principale deve essere colpita, rendendo più volatile su sistemi multi cpu, giusto?

PS: Il mio modo di saperne di più su questo mi sono imbattuto in merito ai seguenti grandi articoli, e dal momento che questa domanda può essere interessante per gli altri, io condividere i miei link qui:

+1

È possibile leggere la mia modifica sulla configurazione con più CPU a cui si fa riferimento. Può succedere che su sistemi multi CPU per un riferimento di breve durata, non più di una singola lettura/scrittura sulla memoria principale si verificherebbe. –

+2

la lettura volatile non è costosa. il costo principale è il modo in cui previene le ottimizzazioni. in pratica, il costo in media non è molto elevato, a meno che non si usi volatili in un circuito chiuso. – irreputable

+2

Questo articolo su infoq (http://www.infoq.com/articles/memory_barriers_jvm_concurrency) potrebbe anche interessarti, mostra gli effetti di volatile e sincronizzati sul codice generato per diverse architetture. Questo è anche un caso in cui jvm può eseguire meglio di un compilatore in anticipo, dal momento che sa se è in esecuzione su un sistema uniprocessore e può omettere alcune barriere di memoria. –

risposta

107

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

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 per stampare il codice assembly il metodo di esecuzione simile 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::[email protected] (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::[email protected] (line 33) 
0xb396cea9: cmp %ecx,%ebp 
0xb396ceab: jne 0xb396ceaf 
0xb396cead: cmp %ebx,%edi 
0xb396ceaf: je  0xb396cece   ;*getstatic l 
            ; - Test2::[email protected] (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::[email protected] (line 40) 
0xb396cec3: add $0x8,%esp 
0xb396cec6: pop %ebp 
0xb396cec7: test %eax,0xb7732000 ; {poll_return} 
;... lines removed 

Se si guardano le 2 riferimenti a getstatic, la prima riguarda un carico dalla memoria, il secondo salta il carico mentre il valore viene riutilizzato dal/i registro/i in cui è già stato caricato (a lungo è a 64 bit e sul mio computer portatile a 32 bit utilizza 2 registri).

Se rendiamo la variabile l variabile, 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::[email protected] (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::[email protected] (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::[email protected] (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 getstatic al l variabile comporta un carico dalla memoria, cioè il valore non può essere mantenuta in un registro su più volatili letture. Per garantire che ci sia una lettura atomica, il valore viene letto dalla memoria principale in un registro MMX movsd 0x6fb7b2f0(%ebp),%xmm0 rendendo l'operazione di lettura una singola istruzione (dall'esempio precedente abbiamo visto che il valore a 64 bit richiederebbe normalmente due letture a 32 bit su un sistema a 32 bit).

Quindi il costo complessivo di una lettura volatile equivale all'incirca a un carico di memoria e può essere economico come un accesso alla cache L1. Tuttavia, se un altro core sta scrivendo sulla variabile volatile, la cache-line sarà invalidata richiedendo una memoria principale o forse un accesso alla cache L3. Il costo effettivo dipenderà in gran parte dall'architettura della CPU. Anche tra Intel e AMD i protocolli di coerenza della cache sono diversi.

+0

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

+0

+1 In JDK5 volatile non può essere riordinato rispetto a * any * read/write (che risolve il double-check locking, per esempio). Ciò implica che influenzerà anche la manipolazione dei campi non volatili? Sarebbe interessante combinare l'accesso a campi volatili e non volatili. – ewernli

+0

@evemli, devi stare attento, ho fatto questa affermazione una volta sola, ma è stato trovato non corretto. C'è un caso limite. Il modello di memoria Java consente la semantica dei motel roach, quando i negozi possono essere riordinati in anticipo rispetto agli archivi volatili. Se l'hai preso dall'articolo di Brian Goetz sul sito IBM, allora vale la pena ricordare che questo articolo semplifica le specifiche JMM. –

4

accesso ad una variabile volatile è per molti versi simile a spostare l'accesso a una variabile normale 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 l'accesso a una variabile volatile può essere eseguito senza penalità: ci deve essere un modo per garantire che una scrittura sul processore A sia sincronizzata con una lettura su processore B.

+4

La lettura delle variabili volatili ha la stessa penalità di 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 quale variabile (ad es. Cache del processore) viene svuotata o invalidata. Mentre esegue lo svuotamento sincronizzato o invalida tutto, l'accesso alla variabile volatile dovrebbe sempre essere ignorato dalla cache. – Daniel

+10

-1, l'accesso a una variabile volatile è piuttosto diverso rispetto all'utilizzo di un blocco sincronizzato. L'inserimento di un blocco sincronizzato richiede una scrittura atomica comparativa basata su CompareSndet per rimuovere il blocco e una scrittura volatile per rilasciarlo. Se il blocco è soddisfatto, il controllo deve passare dallo spazio utente allo spazio del kernel per arbitrare il blocco (questo è il bit costoso). L'accesso a un volatile rimarrà sempre nello spazio dell'utente. –

+0

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

11

nelle parole del Modello memoria Java (come definito per Java 5+ in JSR 133), qualsiasi operazione - lettura o scrittura - su una variabile volatile crea un accade-prima rapporto rispetto a qualsiasi altra operazione sulla stessa variabile. Ciò significa che compilatore e JIT sono obbligati ad 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 è necessario creare una variabile volatile a meno che non si sappia che sarà accessibile da più thread al di fuori dei blocchi synchronized. Anche allora si dovrebbe considerare se volatile è la scelta migliore rispetto a synchronized, AtomicReference ed i suoi amici, le esplicite Lock classi, ecc

18

In generale, sulla maggior parte dei processori moderni un carico volatile è paragonabile ad un carico normale. Un archivio volatile è circa 1/3 del tempo di un'uscita montior-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.

Questo significa che volatili lettura operazioni possono essere eseguite senza un'invalidazione esplicito della cache su x86, ed è un una variabile veloce normale leggere (trascurando il riordino vincoli di volatili)?

Sì, a volte durante la convalida di un campo la CPU non può colpire anche la memoria principale, invece spiare altre cache filo e ottenere il valore da lì (spiegazione molto generale).

Tuttavia, secondo il suggerimento di Neil, se si dispone di un campo a cui si accede da più thread, è necessario avvolgerlo come AtomicReference. Essendo un AtomicReference esegue approssimativamente lo stesso throughput per le letture/scritture, ma è anche più ovvio che il campo sarà accessibile e modificato da più thread.

Modifica per rispondere modifica del PO:

coerenza della cache è un po 'di un protocollo complicato, ma in breve: CPU sarà condividere una linea di cache comune, che è collegato alla memoria principale. Se una CPU carica memoria e nessun'altra CPU dispone che la CPU assumerà che è il valore più aggiornato. Se un'altra CPU tenta di caricare la stessa posizione di memoria, la CPU già caricata ne sarà a conoscenza e condividerà effettivamente il riferimento memorizzato nella cache della CPU richiedente; ora la CPU della richiesta ha una copia di quella memoria nella cache della CPU. (Non ha mai dovuto cercare nella memoria principale il riferimento)

C'è molto più protocollo in questione, ma questo dà un'idea di cosa sta succedendo. Anche per rispondere alla tua altra domanda, con l'assenza di più processori, le letture/scritture volatili possono infatti essere più veloci rispetto a più processori. Esistono alcune applicazioni che potrebbero essere eseguite più rapidamente contemporaneamente con una singola CPU e poi multiple.

+5

Un AtomicReference è solo un wrapper di un campo volatile con funzioni native aggiunte che forniscono funzionalità aggiuntive come getAndSet, compareAndSet ecc. Quindi, dal punto di vista delle prestazioni, utilizzarlo è utile solo se è necessaria la funzionalità aggiunta. Ma mi chiedo perché ti riferisci al sistema operativo qui? La funzionalità è implementata direttamente negli opcode della CPU. E questo implica che su sistemi multiprocessore, in cui una CPU non ha conoscenza del contenuto della cache di altre CPU che i dati volatili sono più lenti perché le CPU devono sempre colpire la memoria principale? – Daniel

+0

Hai ragione, ho parlato dell'OS dovrebbe scrivere CPU, correggendolo ora. E sì, so che AtomicReference è semplicemente un wrapper per campi volatili ma aggiunge anche una sorta di documentazione che il campo stesso sarà accessibile da più thread. –

+0

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