Sì, ci sono sicuramente casi in cui ciò è sensato e, come suggerito, le variabili volatili sono uno di quei casi - anche per l'accesso con singolo thread!
Le scritture volatili sono costose, sia da un punto di vista hardware che di compilazione/JIT. A livello hardware, queste scritture potrebbero essere 10x-100x più costose di una normale scrittura, poiché i buffer di scrittura devono essere svuotati (su x86, i dettagli varieranno in base alla piattaforma). A livello di compilatore/JIT, le scritture volatili inibiscono molte ottimizzazioni comuni.
La speculazione, tuttavia, può solo farti arrivare così lontano - la prova è sempre nel benchmarking. Ecco un microbenchmark che prova le tue due strategie. L'idea di base è copiare i valori da una matrice all'altra (praticamente System.arraycopy), con due varianti - una che copia incondizionatamente e una che controlla per vedere se i valori sono diversi prima.
Ecco le routine di copia per il semplice caso, non volatile (sorgente completo here):
// no check
for (int i=0; i < ARRAY_LENGTH; i++) {
target[i] = source[i];
}
// check, then set if unequal
for (int i=0; i < ARRAY_LENGTH; i++) {
int x = source[i];
if (target[i] != x) {
target[i] = x;
}
}
I risultati usando il codice di cui sopra per copiare una lunghezza array di 1000, utilizzando Caliper come mio microbenchmark imbracatura , sono:
benchmark arrayType ns linear runtime
CopyNoCheck SAME 470 =
CopyNoCheck DIFFERENT 460 =
CopyCheck SAME 1378 ===
CopyCheck DIFFERENT 1856 ====
Ciò include anche circa 150ns di sovraccarico per corsa per reimpostare l'array di destinazione ogni volta. Saltare il controllo è molto più veloce - circa 0,47 ns per elemento (o circa 0,32 ns per elemento dopo aver rimosso l'overhead di installazione, quindi praticamente 1 ciclo sulla mia scatola).
Il controllo è di circa 3 volte più lento quando gli array sono uguali e 4 volte più lento sono diversi. Sono sorpreso di quanto sia pessimo il controllo, dato che è perfettamente previsto. Sospetto che il colpevole sia in gran parte il JIT - con un corpo del ciclo molto più complesso, potrebbe essere srotolato meno volte e altre ottimizzazioni potrebbero non essere applicabili.
Passiamo alla cassa volatile. Qui, ho usato AtomicIntegerArray
come array di elementi volatili, poiché Java non ha alcun tipo di array nativo con elementi volatili. Internamente, questa classe sta scrivendo direttamente nell'array usando sun.misc.Unsafe
, che consente le scritture volatili. L'assemblaggio generato è sostanzialmente simile al normale accesso all'array, diverso dall'aspetto volatile (e probabilmente dall'eliminazione del controllo di intervallo, che potrebbe non essere efficace nel caso AIA).
Ecco il codice:
// no check
for (int i=0; i < ARRAY_LENGTH; i++) {
target.set(i, source[i]);
}
// check, then set if unequal
for (int i=0; i < ARRAY_LENGTH; i++) {
int x = source[i];
if (target.get(i) != x) {
target.set(i, x);
}
}
Ed ecco i risultati:
arrayType benchmark us linear runtime
SAME CopyCheckAI 2.85 =======
SAME CopyNoCheckAI 10.21 ===========================
DIFFERENT CopyCheckAI 11.33 ==============================
DIFFERENT CopyNoCheckAI 11.19 =============================
la situazione è capovolta. Il primo controllo è ~ 3.5 volte più veloce del solito metodo. Tutto è molto più lento nel complesso: nel caso in esame, stiamo pagando ~ 3 ns per loop, e nei casi peggiori ~ 10 ns (le volte sopra sono in noi e coprono la copia dell'intero array di 1000 elementi). Le scritture volatili sono davvero più costose. C'è circa 1 ns di sovraccarico incluso nel caso DIFFERENT per ripristinare l'array su ogni iterazione (motivo per cui anche il semplice è leggermente più lento per DIVERSO). Sospetto che gran parte del sovraccarico nel caso del "controllo" sia in realtà un controllo dei limiti.
Questo è tutto single threaded. Se tu avessi una contesa cross-core su un volatile, i risultati sarebbero molto, molto peggio per il metodo semplice, e quasi altrettanto buono di quanto sopra per il caso di controllo (la linea della cache si starebbe semplicemente nello stato condiviso - no necessario traffico di coerenza).
Ho anche testato solo gli estremi di "ogni elemento uguale" rispetto a "ogni elemento diverso". Ciò significa che il ramo nell'algoritmo "verifica" è sempre perfettamente previsto. Se avessi un mix di uguali e diversi, non avresti solo una combinazione ponderata dei tempi per SAME e CASI DIVERSI - fai di peggio, a causa di una cattiva previsione (sia a livello di hardware, e forse anche a livello di JIT , che non può più essere ottimizzato per il ramo sempre occupato).
Quindi, se è sensato, anche per volatile, dipende dal contesto specifico - il mix di valori uguali e non uguali, il codice circostante e così via. Di solito non lo faccio volatile da solo in uno scenario a thread singolo, a meno che non sospetti che un gran numero di set siano ridondanti. In strutture pesantemente multithreading, tuttavia, leggere e quindi eseguire una scrittura volatile (o un'altra operazione costosa, come un CAS) è una best practice e vedrai il codice di qualità come le strutture java.util.concurrent
.
questo probabilmente dipende quasi interamente dal caso d'uso specifico, no? – mfrankli
Stavo pensando a questa domanda esatta non 5 minuti fa, solo con un valore booleano – Niro
In particolare, il caso d'uso che ho in mente è un codice molto multi-thread. Evitare il più possibile le scritture rimuove il meccanismo di coerenza della cache. – ciamej