2013-04-19 12 views
24
if (var != X) 
    var = X; 

È ragionevole o no? Il compilatore ottimizzerà sempre la dichiarazione if? Ci sono casi d'uso che potrebbero beneficiare della dichiarazione if?È un'ottimizzazione ragionevole controllare se una variabile contiene un valore specifico prima di scrivere quel valore?

Cosa succede se var è una variabile volatile?

Sono interessato alle risposte sia in C++ che in Java poiché le variabili volatili hanno semantica diversa in entrambe le lingue. Anche la compilazione JIT di Java può fare la differenza.

L'istruzione if introduce ramificazioni e letture aggiuntive che non si verificherebbero se avessimo sempre sovrascritto var con X, quindi è negativo. D'altra parte, se si utilizza questa ottimizzazione, se si utilizza questa ottimizzazione, eseguiamo solo una lettura e non eseguiamo una scrittura, che potrebbe avere alcuni effetti sulla cache. Chiaramente, ci sono alcuni compromessi qui. Mi piacerebbe sapere come sembra in pratica. Qualcuno ha fatto qualche test su questo?

EDIT:

Sono per lo più interessati a come sembra in un ambiente multi-processore. In una situazione banale non sembra avere molto senso controllare prima la variabile. Ma quando deve essere mantenuta la coerenza della cache tra processori/core, il controllo extra potrebbe essere effettivamente vantaggioso. Mi chiedo quanto può avere un grande impatto? Inoltre, il processore non dovrebbe eseguire tale ottimizzazione autonomamente? Se lo var == X lo assegna ancora una volta il valore X non dovrebbe "sporcare" la cache. Ma possiamo fare affidamento su questo?

+1

questo probabilmente dipende quasi interamente dal caso d'uso specifico, no? – mfrankli

+0

Stavo pensando a questa domanda esatta non 5 minuti fa, solo con un valore booleano – Niro

+0

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

risposta

8

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.

8

In generale la risposta è no. Dal momento che se si dispone di un semplice tipo di dati, il compilatore sarebbe in grado di eseguire le ottimizzazioni necessarie. E in caso di tipi con operatore pesante = è responsabilità dell'operatore = scegliere il modo ottimale per assegnare un nuovo valore.

+6

Un caso in cui può avere senso effettuare il controllo è quando la variabile di destinazione in questione è pesantemente referenziata in un ambiente multiprocessore e si desidera evitare di "sporcare" inutilmente la linea della cache. Ma ci sono pochissimi casi in cui ha senso nella programmazione "normale". –

+0

@HotLicks Con domande così ampie non c'è mai una risposta adatta a tutti. E una volta che lo scenario di utilizzo diventa comune, si rifletterà in operatore = in un modo o nell'altro. E grazie per il buon esempio di caso speciale. – alexrider

+0

Non penso che operator = possa aiutare qui - la domanda sembra orientata verso i primitivi (da qui l'attenzione su 'volatile'), e indipendentemente da Java non ha alcuna capacità di sovraccaricare l'operazione = (che sarebbe comunque priva di significato dal momento che stiamo parlando solo dell'assegnazione di riferimento qui). – BeeOnRope

9

È un'ottimizzazione ragionevole controllare se una variabile contiene un valore specifico prima di scrivere quel valore?

Esistono casi di utilizzo che potrebbero beneficiare della dichiarazione if?

È quando l'assegnazione è significativamente più costosa di un confronto di disuguaglianza restituendo false.

Un esempio potrebbe essere un grande * std::set, che può richiedere la duplicazione di molte allocazioni di heap.

** per qualche definizione di "grande" *

Sarà il compilatore ottimizzare-out sempre if?

Questo è un "no" abbastanza sicuro, come lo sono molte domande che contengono sia "ottimizzare" che "sempre".

Lo standard C++ fa menzione rara delle ottimizzazioni, ma non ne richiede mai uno.

Cosa succede se var è una variabile volatile?

Poi può eseguire l'if, anche se volatile doesn't achieve what most people assume.

1

Sarebbe sensato se tu avessi letto-scrittura semantica coinvolti, ogni volta che la lettura di blocco è di solito meno dirompente rispetto alla scrittura.

0

In java la risposta è sempre no. Tutti i compiti che puoi fare in Java sono primitivi. In C++, la risposta è ancora quasi sempre no - se la copia è molto più costosa di un controllo di uguaglianza, la classe in questione dovrebbe fare lo stesso controllo di uguaglianza.

+0

Non vero per variabili volatili o variabili condivise tra thread (volatili o meno), in Java. – BeeOnRope

4

In C++, l'assegnazione di una variabile SEMPLICE (ovvero un numero intero normale o variabile) è sicuramente e sempre più veloce di controllare se ha già quel valore e quindi impostarlo se non avesse il valore. Sarei molto sorpreso se questo non fosse vero anche in Java, ma non so quanto siano complicate o semplici le cose in Java - Ho scritto poche centinaia di righe, e non ho studiato in realtà il codice byte e il bytecode JITed lavori.

Chiaramente, se la variabile è molto facile da controllare, ma complicata da impostare, che potrebbe essere il caso per le classi e altre cose simili, allora potrebbe esserci un valore. Il caso tipico in cui si trova questo sarebbe in un codice in cui il "valore" è una sorta di indice o hash, ma se non è una corrispondenza, è richiesto molto lavoro. Un esempio potrebbe essere in una task-interruttore:

if (current_process != new_process_to_run) 
    current_process == new_process_to_run; 

Perché qui, un "processo" è un oggetto complesso di alterare, ma il != può essere fatto su l'ID del processo.

Se l'oggetto è semplice o complesso, il compilatore quasi certamente non capirà cosa stai cercando di fare qui, quindi probabilmente non lo ottimizzerà - ma i compilatori sono più intelligenti di quanto pensi che SOMETIMES, e più stupidi a altre volte, quindi non scommetterei in entrambi i casi.

volatile dovrebbe forzare sempre il compilatore a leggere e scrivere valori sulla variabile, sia che "pensi" sia necessario o meno, quindi leggerà sicuramente la variabile e WRITE la variabile. Ovviamente, se la variabile è volatile probabilmente significa che può cambiare o rappresentare un po 'di hardware, quindi dovresti essere EXTRA attento a come lo tratti anche tu ... Una lettura extra di una scheda PCI-X potrebbe comportare diversi cicli di bus (il ciclo del bus è di un ordine di grandezza più lento della velocità del processore!), il che probabilmente influirà molto più sulle prestazioni. Ma poi scrivere su un registro hardware può (ad esempio) far sì che l'hardware faccia qualcosa di inaspettato, e controllare che abbiamo quel valore prima di renderlo più veloce, perché "qualche operazione ricomincia", o qualcosa del genere.

+0

'sicuramente e sempre' .. e come con tutte le affermazioni definite e assolute anche questa è falsa (si pensi al comportamento della cache tra molti thread). Ciò detto nella normale situazione a thread singolo Java JIT produrrà in genere un codice simile a 'gcc -O2' se le regole del linguaggio lo consentiranno. – Voo

+0

Se la variabile è 'volatile' in Java, controllare quindi impostare sarà quasi certamente più veloce se la variabile è spesso uguale (vedere la mia risposta) - in genere con un margine significativo. Lo stesso vale per C++ per il costrutto equivalente (che non è 'volatile', ma' std :: atomic' stuff). – BeeOnRope

1

In Objective-C si ha la situazione in cui l'assegnazione di un indirizzo oggetto a una variabile puntatore può richiedere che l'oggetto sia "mantenuto" (conteggio dei riferimenti incrementato). In tal caso ha senso vedere se il valore assegnato è lo stesso del valore corrente nella variabile puntatore, per evitare di dover eseguire le operazioni di incremento/decremento relativamente costose.

Altre lingue che utilizzano il conteggio dei riferimenti hanno probabilmente scenari simili.

Tuttavia, quando si assegna, ad esempio, uno o un boolean a una variabile semplice (al di fuori dello scenario della cache multiprocessore menzionato altrove) il test viene raramente meritato. La velocità di un negozio nella maggior parte dei processori è almeno veloce quanto il carico/test/ramo.

5

Ci sono situazioni in cui anche un'assegnazione banale di dire una variabile puntuale può essere più costosa di una lettura e una diramazione (soprattutto se prevedibile).

Perché? Multithreading. Se diversi thread leggono solo lo stesso valore, tutti possono condividere quel valore nelle loro cache. Ma non appena ci scrivi, devi invalidare la cacheline e ottenere il nuovo valore la prossima volta che vuoi leggerlo o devi ottenere il valore aggiornato per mantenere la cache coerente. Entrambe le situazioni portano a un maggiore traffico tra i core e aggiungono latenza alle letture.

Se il ramo è piuttosto imprevedibile anche se è probabilmente ancora più lento.

Problemi correlati