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.
È 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. –
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
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. –