Java è molto più difficile da "modificare" per le prestazioni rispetto a C / C ++? [chiuso]


11

La "magia" della JVM ostacola l'influenza che un programmatore ha sulle microottimizzazioni in Java? Di recente ho letto in C ++ a volte l'ordinamento dei membri dei dati può fornire ottimizzazioni (garantito, nell'ambiente dei microsecondi) e presumo che le mani di un programmatore siano legate quando si tratta di spremere le prestazioni da Java?

Apprezzo che un algoritmo decente offra maggiori guadagni di velocità, ma una volta che hai l'algoritmo corretto è più difficile da modificare Java a causa del controllo JVM?

In caso contrario, le persone potrebbero fornire esempi di quali trucchi è possibile utilizzare in Java (oltre ai semplici flag di compilazione).


14
Il principio alla base di tutta l'ottimizzazione di Java è questo: JVM probabilmente lo ha già fatto meglio di quanto tu possa fare. L'ottimizzazione consiste principalmente nel seguire pratiche di programmazione sensate ed evitare le solite cose come concatenare le stringhe in un ciclo.
Robert Harvey,

3
Il principio della micro-ottimizzazione in tutte le lingue è che il compilatore lo ha già fatto meglio di quanto tu possa fare. L'altro principio di micro-ottimizzazione in tutte le lingue è che gettare più hardware su di esso è più economico del tempo del programmatore di micro-ottimizzarlo. Il programmatore deve tendere a ridimensionare i problemi (algoritmi non ottimali), ma la micro-ottimizzazione è una perdita di tempo. A volte la micro-ottimizzazione ha senso su sistemi embedded in cui non è possibile gettare più hardware su di esso, ma Android che utilizza Java e un'implementazione piuttosto scadente, mostra che molti di essi hanno già abbastanza hardware.
Jan Hudec,

1
per "trucchi prestazioni Java", vale la pena studiare sono: Effective Java , Angelika Langer Links - Java prestazioni e prestazioni relativi articoli di Brian Goetz in Java teoria e pratica e Threading Lightly serie elencati qui
moscerino

2
Prestare estrema attenzione a suggerimenti e trucchi - la JVM, i sistemi operativi e l'hardware vanno avanti - è meglio imparare la metodologia di ottimizzazione delle prestazioni e applicare miglioramenti per il proprio ambiente particolare :-)
Martijn Verburg

In alcuni casi, una VM può eseguire ottimizzazioni in fase di esecuzione che non sono pratiche da eseguire in fase di compilazione. L'uso della memoria gestita può migliorare le prestazioni, anche se spesso avrà un footprint di memoria più elevato. La memoria non utilizzata viene liberata quando è conveniente, piuttosto che APPENA POSSIBILE.
Brian,

Risposte:


5

Certo, a livello di micro-ottimizzazione la JVM farà alcune cose sulle quali avrai un controllo limitato rispetto a C e C ++ in particolare.

D'altra parte, la varietà di comportamenti del compilatore con C e C ++, in particolare, avrà un impatto negativo molto maggiore sulla tua capacità di fare micro-ottimizzazioni in qualsiasi tipo di modo vagamente portatile (anche attraverso le revisioni del compilatore).

Dipende dal tipo di progetto che stai modificando, dagli ambienti a cui ti rivolgi e così via. E alla fine, non ha molta importanza dato che stai ottenendo alcuni ordini di grandezza risultati migliori dalle ottimizzazioni algoritmiche / della struttura dei dati / del programma.


Può importare molto quando scopri che la tua app non si adatta ai core
James,

@james - ti interessa elaborare?
Telastyn,


1
@James, il ridimensionamento tra i core ha ben poco a che fare con il linguaggio di implementazione (tranne Python!) E, più ancora, con l'architettura dell'applicazione.
James Anderson,

29

Le microottimizzazioni non valgono quasi mai il tempo e quasi tutte le semplici sono eseguite automaticamente da compilatori e runtime.

Vi è, tuttavia, un'importante area di ottimizzazione in cui C ++ e Java sono fondamentalmente diversi, ovvero l'accesso alla memoria di massa. C ++ ha una gestione manuale della memoria, il che significa che è possibile ottimizzare il layout dei dati dell'applicazione e gli schemi di accesso per sfruttare appieno le cache. Questo è piuttosto difficile, in qualche modo specifico per l'hardware su cui stai eseguendo (quindi i miglioramenti delle prestazioni potrebbero scomparire su hardware diverso), ma se fatto bene, può portare a prestazioni assolutamente mozzafiato. Ovviamente lo paghi con il potenziale per tutti i tipi di bug orribili.

Con un linguaggio garbage collection come Java, questo tipo di ottimizzazioni non può essere fatto nel codice. Alcuni possono essere eseguiti dal runtime (automaticamente o tramite la configurazione, vedere di seguito), e alcuni non sono possibili (il prezzo da pagare per essere protetto dai bug di gestione della memoria).

In caso contrario, le persone potrebbero fornire esempi di quali trucchi è possibile utilizzare in Java (oltre ai semplici flag di compilazione).

I flag del compilatore sono irrilevanti in Java perché il compilatore Java quasi non ottimizza; lo fa il runtime.

E in effetti i runtime Java hanno una moltitudine di parametri che possono essere modificati, specialmente per quanto riguarda il garbage collector. Non c'è niente di "semplice" in quelle opzioni: i valori predefiniti sono buoni per la maggior parte delle applicazioni e per ottenere prestazioni migliori è necessario comprendere esattamente cosa fanno le opzioni e come si comporta l'applicazione.


1
+1: sostanzialmente quello che stavo scrivendo nella mia risposta, forse una migliore formulazione.
Klaim,

1
+1: Punti molto buoni, spiegati in modo molto conciso: "Questo è abbastanza difficile ... ma se fatto bene, può portare a prestazioni assolutamente mozzafiato. Ovviamente lo paghi con il potenziale per tutti i tipi di orribili bug ".
Giorgio,

1
@MartinBa: è più che si paga per ottimizzare la gestione della memoria. Se non si tenta di ottimizzare la gestione della memoria, la gestione della memoria C ++ non è così difficile (evitarlo interamente tramite STL o renderlo relativamente semplice utilizzando RAII). Naturalmente, l'implementazione di RAII in C ++ richiede più righe di codice che non fare nulla in Java (ovvero perché Java lo gestisce per te).
Brian,

3
@Martin Ba: Fondamentalmente sì. Puntatori ciondolanti, buffer overflow, puntatori non inizializzati, errori nell'aritmetica dei puntatori, tutto ciò che semplicemente non esiste senza la gestione manuale della memoria. E l'ottimizzazione di accesso alla memoria richiede più o meno di fare un sacco di gestione manuale della memoria.
Michael Borgwardt,

1
Ci sono un paio di cose che puoi fare a Java. Uno è il pooling di oggetti, che massimizza le possibilità di localizzazione della memoria degli oggetti (diversamente dal C ++ dove può garantire la localizzazione della memoria).
RokL

5

[...] (concesso, nell'ambiente dei microsecondi) [...]

I micro-secondi si sommano se stiamo eseguendo il loop su milioni di miliardi di cose. Una sessione personale di vtune / micro-ottimizzazione da C ++ (nessun miglioramento algoritmico):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Tutto tranne "multithreading", "SIMD" (scritto a mano per battere il compilatore) e l'ottimizzazione della patch a 4 valenze erano ottimizzazioni della memoria a livello micro. Anche il codice originale a partire dai tempi iniziali di 32 secondi era già stato ottimizzato abbastanza (complessità algoritmica teoricamente ottimale) e questa è una sessione recente. La versione originale molto prima di questa recente sessione ha richiesto oltre 5 minuti per l'elaborazione.

L'ottimizzazione dell'efficienza della memoria può aiutare spesso da diverse volte a ordini di grandezza in un contesto a thread singolo e altro in contesti multithread (i vantaggi di un rappresentante di memoria efficiente spesso si moltiplicano con più thread nel mix).

L'importanza della microottimizzazione

Sono un po 'agitato da questa idea che le micro-ottimizzazioni sono una perdita di tempo. Sono d'accordo che sia un buon consiglio generale, ma non tutti lo fanno in modo errato sulla base di intuizioni e superstizioni piuttosto che di misurazioni. Fatto correttamente, non produce necessariamente un micro impatto. Se prendiamo il proprio Embree (kernel raytracing) di Intel e testiamo solo il semplice BVH scalare che hanno scritto (non il pacchetto ray che è esponenzialmente più difficile da battere), e quindi proviamo a battere le prestazioni di quella struttura di dati, può essere un esperienza umiliante anche per un veterano abituato alla profilazione e alla messa a punto del codice per decenni. Ed è tutto a causa delle micro-ottimizzazioni applicate. La loro soluzione può elaborare oltre cento milioni di raggi al secondo quando ho visto professionisti industriali che lavorano nel raytracing che possono "

Non c'è modo di prendere un'implementazione semplice di un BVH con solo un focus algoritmico e ottenere oltre un centinaio di milioni di intersezioni di raggi primari al secondo da qualsiasi compilatore ottimizzante (persino il proprio ICC di Intel). Spesso non si ottiene nemmeno un milione di raggi al secondo. Ci vogliono soluzioni di qualità professionale per ottenere anche qualche milione di raggi al secondo. Ci vuole micro-ottimizzazione a livello Intel per ottenere oltre cento milioni di raggi al secondo.

algoritmi

Penso che la micro-ottimizzazione non sia importante fintanto che le prestazioni non sono importanti a livello di minuti a secondi, ad esempio o ore a minuti. Se prendiamo un orribile algoritmo come il bubble sort e lo usiamo su un input di massa come esempio, e poi lo confrontiamo con anche un'implementazione di base di merge sort, il primo potrebbe richiedere mesi per l'elaborazione, il secondo forse 12 minuti, di conseguenza di complessità quadratica vs linearitmica.

La differenza tra mesi e minuti probabilmente farà sì che la maggior parte delle persone, anche quelle che non lavorano in settori critici per le prestazioni, considerino inaccettabili i tempi di esecuzione se richiede che gli utenti aspettino mesi per ottenere un risultato.

Nel frattempo, se confrontiamo l'ordinamento unisci non micro-ottimizzato e semplice con quicksort (che non è affatto algoritmicamente superiore all'unione dell'ordinamento e offre solo miglioramenti a livello micro per la località di riferimento), il quicksort micro-ottimizzato potrebbe finire in 15 secondi anziché 12 minuti. Far attendere 12 minuti agli utenti potrebbe essere perfettamente accettabile (tipo di pausa caffè).

Penso che questa differenza sia probabilmente trascurabile per la maggior parte delle persone tra, diciamo, 12 minuti e 15 secondi, ed è per questo che la micro-ottimizzazione è spesso considerata inutile poiché è spesso solo come la differenza tra minuti e secondi, e non minuti e mesi. L'altra ragione per cui penso che sia considerato inutile è che viene spesso applicato ad aree che non contano: qualche piccola area che non è nemmeno circolare e critica che produce una discutibile differenza dell'1% (che potrebbe benissimo essere solo rumore). Ma per le persone che si preoccupano di questi tipi di differenze di tempo e sono disposte a misurare e fare bene, penso che valga la pena prestare attenzione almeno ai concetti di base della gerarchia della memoria (in particolare i livelli superiori relativi agli errori di pagina e ai mancati cache) .

Java lascia molto spazio a buone microottimizzazioni

Uff, scusa - con quel tipo di sfogo a parte:

La "magia" della JVM ostacola l'influenza che un programmatore ha sulle microottimizzazioni in Java?

Un po ', ma non tanto quanto la gente potrebbe pensare se lo fai bene. Ad esempio, se stai eseguendo l'elaborazione delle immagini, nel codice nativo con SIMD scritto a mano, multithreading e ottimizzazioni della memoria (modelli di accesso e possibilmente rappresentazione anche in base all'algoritmo di elaborazione delle immagini), è facile sgranocchiare centinaia di milioni di pixel al secondo per 32- bit RGBA pixel (canali di colore a 8 bit) e talvolta anche miliardi al secondo.

È impossibile avvicinarsi da nessuna parte a Java se si dice, fatto un Pixeloggetto (questo da solo gonfiarebbe la dimensione di un pixel da 4 byte a 16 su 64 bit).

Ma potresti essere molto più vicino se evitassi l' Pixeloggetto, usassi un array di byte e modellassi un Imageoggetto. Java è ancora abbastanza competente lì se inizi a usare array di semplici vecchi dati. Ho già provato questo genere di cose in Java e ne sono rimasto piuttosto impressionato a condizione che non si creino un sacco di piccoli oggetti per adolescenti ovunque 4 volte più grandi del normale (es: usare intinvece di Integer) e iniziare a modellare interfacce di massa come un Imageinterfaccia, non Pixelinterfaccia. Mi permetto persino di dire che Java può competere con le prestazioni del C ++ se si esegue il loop su semplici dati vecchi e non su oggetti (enormi array di float, ad esempio, non Float).

Forse ancora più importante delle dimensioni della memoria è che un array di intgarantisce una rappresentazione contigua. Una serie di Integerno. La contiguità è spesso essenziale per la località di riferimento poiché significa che più elementi (es: 16 ints) possono rientrare tutti in una singola riga della cache e potenzialmente essere accessibili insieme prima dello sfratto con schemi di accesso alla memoria efficienti. Nel frattempo un singolo Integerpotrebbe essere bloccato da qualche parte nella memoria con la memoria circostante essendo irrilevante, solo per avere quella regione di memoria caricata in una linea di cache solo per utilizzare un singolo numero intero prima dello sfratto invece di 16 numeri interi. Anche se siamo diventati meravigliosamente fortunati e circostantiIntegerserano tutti vicini l'uno all'altro in memoria, possiamo solo inserire 4 in una linea di cache a cui è possibile accedere prima dello sfratto come risultato di Integeressere 4 volte più grandi, e questo è lo scenario migliore.

E ci sono molte micro-ottimizzazioni da avere lì poiché siamo unificati sotto la stessa architettura / gerarchia di memoria. I modelli di accesso alla memoria non importa quale sia il linguaggio che usi, concetti come la piastrellatura / blocco dei loop potrebbero essere generalmente applicati molto più spesso in C o C ++, ma beneficiano altrettanto di Java.

Di recente ho letto in C ++ a volte l'ordinamento dei membri dei dati può fornire ottimizzazioni [...]

L'ordine dei membri dei dati generalmente non ha importanza in Java, ma è soprattutto una buona cosa. In C e C ++, preservare l'ordine dei membri dei dati è spesso importante per motivi ABI, quindi i compilatori non si sbagliano. Gli sviluppatori umani che lavorano lì devono stare attenti a fare cose come organizzare i loro membri di dati in ordine decrescente (dal più grande al più piccolo) per evitare di sprecare memoria sull'imbottitura. Con Java, a quanto pare la JIT può riordinare i membri al volo per garantire un allineamento corretto minimizzando il riempimento, quindi, a condizione che sia così, automatizza qualcosa che i programmatori medi C e C ++ possono spesso fare male e finire per sprecare memoria in quel modo ( che non è solo uno spreco di memoria, ma spesso una perdita di velocità aumentando il passo tra le strutture AoS inutilmente e causando più mancate cache). E' una cosa molto robotica per riorganizzare i campi per ridurre al minimo l'imbottitura, quindi idealmente gli umani non se ne occupano. L'unica volta in cui la disposizione dei campi può essere importante in un modo che richiede a un essere umano di conoscere la disposizione ottimale è se l'oggetto è più grande di 64 byte e stiamo organizzando i campi in base al modello di accesso (non riempimento ottimale) - nel qual caso potrebbe essere uno sforzo più umano (richiede la comprensione di percorsi critici, alcuni dei quali sono informazioni che un compilatore non può prevedere senza sapere cosa faranno gli utenti con il software).

In caso contrario, le persone potrebbero fornire esempi di quali trucchi è possibile utilizzare in Java (oltre ai semplici flag di compilazione).

La più grande differenza per me in termini di una mentalità ottimizzante tra Java e C ++ è che C ++ potrebbe consentire di usare oggetti un po 'più (adolescenti) più di Java in uno scenario critico per le prestazioni. Ad esempio, C ++ può racchiudere un numero intero in una classe senza spese generali (benchmarkato ovunque). Java deve avere quel metadata in stile puntatore + padding di allineamento per oggetto, motivo per cui Booleanè più grande di boolean(ma in cambio offre vantaggi uniformi di riflessione e la possibilità di ignorare qualsiasi funzione non contrassegnata come finalper ogni singolo UDT).

È un po 'più facile in C ++ controllare la contiguità dei layout di memoria attraverso campi non omogenei (es: interleaving float e ints in un array attraverso una struttura / classe), poiché spesso si perde la località spaziale (o almeno si perde il controllo) in Java durante l'allocazione di oggetti tramite GC.

... ma spesso le soluzioni più performanti spesso le suddividono comunque e usano un modello di accesso SoA su array contigui di semplici vecchi dati. Quindi, per le aree che richiedono prestazioni di picco, le strategie per ottimizzare il layout di memoria tra Java e C ++ sono spesso le stesse e spesso ti faranno demolire quelle interfacce orientate agli oggetti per adolescenti a favore di interfacce stile raccolta che possono fare cose come hot / suddivisione dei campi freddi, ripetizioni SoA, ecc. Le ripetizioni AoSoA non omogenee sembrano in qualche modo impossibili in Java (a meno che tu non abbia appena usato una matrice grezza di byte o qualcosa del genere), ma quelli sono per rari casi in cui entrambii modelli di accesso sequenziale e casuale devono essere veloci pur avendo contemporaneamente una miscela di tipi di campo per i campi caldi. Per me la maggior parte della differenza nella strategia di ottimizzazione (a livello generale di livello) tra questi due è discutibile se si sta raggiungendo il massimo delle prestazioni.

Le differenze variano un po 'di più se stai semplicemente raggiungendo prestazioni "buone" - non potendo fare così tanto con piccoli oggetti come Integervs. intpuò essere un po' più di una PITA, specialmente con il modo in cui interagisce con i generici . È un po 'più difficile costruire una struttura di dati generica come obiettivo di ottimizzazione centrale in Java che funziona per int, floatecc., Evitando quelle UDT più grandi e costose, ma spesso le aree più critiche per le prestazioni richiedono il roll-off delle proprie strutture di dati messo a punto per uno scopo molto specifico, quindi è solo fastidioso per il codice che si impegna per buone prestazioni ma non per le massime prestazioni.

Oggetto ambientale

Si noti che l'overhead di oggetti Java (metadati e perdita di località spaziale e perdita temporanea di località temporali dopo un ciclo GC iniziale) è spesso grande per cose che sono veramente piccole (come intvs. Integer) che vengono archiviate da milioni in una struttura di dati che è ampiamente contiguo e accessibile con anelli molto stretti. Sembra esserci molta sensibilità su questo argomento, quindi dovrei chiarire che non vuoi preoccuparti degli oggetti in testa per oggetti grandi come le immagini, ma oggetti davvero minuscoli come un singolo pixel.

Se qualcuno dovesse dubitare di questa parte, suggerirei di fare un punto di riferimento tra la somma di un milione casuale intse un milione casuale Integerse di farlo ripetutamente (il Integersrimpasto rimarrà in memoria dopo un ciclo GC iniziale).

Trucco finale: design dell'interfaccia che lasciano spazio all'ottimizzazione

Quindi l'ultimo trucco Java per come lo vedo io se hai a che fare con un posto che gestisce un carico pesante su piccoli oggetti (es: a Pixel, un vettore 4, una matrice 4x4, un Particle, forse anche un Accountse ha solo un piccolo campi) è di evitare di usare oggetti per queste cose adolescenziali e usare array (possibilmente concatenati insieme) di semplici vecchi dati. Gli oggetti quindi diventano interfacce di raccolta come Image, ParticleSystem, Accounts, un insieme di matrici o vettori, ecc quelli individuali sono reperibili indice, ad es Questo è anche uno dei trucchi design innovativo in C e C ++, dato che anche senza il carico di oggetto base e memoria disgiunta, la modellazione dell'interfaccia a livello di una singola particella impedisce le soluzioni più efficienti.


1
Considerando che le cattive prestazioni alla rinfusa potrebbero effettivamente avere una discreta possibilità di schiacciare le massime prestazioni nelle aree critiche, non credo che si possa ignorare completamente il vantaggio di avere facilmente buone prestazioni. E il trucco di trasformare una matrice di strutture in una struttura di matrici si rompe in qualche modo quando si accederà contemporaneamente a tutti (o quasi tutti) i valori che comprendono una delle strutture originali. BTW: Vedo che stai scoprendo molti post vecchi e aggiungi la tua buona risposta, a volte anche la buona risposta ;-)
Deduplicatore

1
@Deduplicator Spero di non dare fastidio alle persone sbattendo troppo! Questo ha avuto un po 'di rabbia - forse dovrei migliorarlo un po'. SoA vs. AoS è spesso difficile per me (accesso sequenziale vs. accesso casuale). Raramente conosco in anticipo quale dovrei usare poiché nel mio caso c'è spesso una combinazione di accesso sequenziale e casuale. La preziosa lezione che ho spesso imparato è quella di progettare interfacce che lasciano abbastanza spazio per giocare con la rappresentazione dei dati - interfacce un po 'più voluminose che possiedono algoritmi di trasformazione di grandi dimensioni quando possibile (a volte non possibile con bit di adolescenti accessibili casualmente qua e là).

1
Bene, l'ho notato solo perché le cose sono molto lente. E ho preso il mio tempo con ognuno.
Deduplicatore

Mi chiedo davvero perché sia user204677andato via. Una risposta così grande.
Oligofren,

3

C'è una zona intermedia tra la micro-ottimizzazione, da un lato, e una buona scelta dell'algoritmo, dall'altro.

È l'area delle accelerazioni a fattore costante e può produrre ordini di grandezza.
Il modo in cui lo fa è eliminando intere frazioni del tempo di esecuzione, come il primo 30%, quindi il 20% di ciò che rimane, quindi il 50% di quello, e così via per diverse iterazioni, fino a quando non rimane quasi nulla.

Non lo vedi in piccoli programmi in stile demo. Dove lo vedi è in grandi programmi seri con molte strutture di dati di classe, in cui lo stack di chiamate è in genere a molti livelli di profondità. Un buon modo per trovare le opportunità di accelerazione è esaminare campioni a tempo casuale dello stato del programma.

Generalmente le accelerazioni consistono in cose come:

  • minimizzare le chiamate newraggruppando e riutilizzando vecchi oggetti,

  • riconoscendo le cose che vengono fatte che sono in qualche modo lì per il bene della generalità, piuttosto che essere effettivamente necessarie,

  • rivedere la struttura dei dati utilizzando diverse classi di raccolta che hanno lo stesso comportamento big-O ma sfruttano i modelli di accesso effettivamente utilizzati,

  • salvare i dati che sono stati acquisiti dalle chiamate di funzione anziché richiamare la funzione (è una tendenza naturale e divertente dei programmatori assumere che funzioni con nomi più brevi vengano eseguite più rapidamente).

  • tollerare un certo grado di incoerenza tra le strutture di dati ridondanti, anziché cercare di mantenerle completamente coerenti con gli eventi di notifica,

  • ecc ecc.

Ma ovviamente nessuna di queste cose dovrebbe essere fatta senza prima aver dimostrato di essere problematici prendendo campioni.


2

Java (per quanto ne so) non ti dà alcun controllo sulle posizioni delle variabili in memoria, quindi hai un momento più difficile per evitare cose come la falsa condivisione e l'allineamento delle variabili (puoi riempire una classe con diversi membri inutilizzati). Un'altra cosa di cui non credo che tu possa trarre vantaggio sono istruzioni come mmpause, ma queste cose sono specifiche della CPU e quindi se pensi di averne bisogno, Java potrebbe non essere il linguaggio da usare.

Esiste la classe Unsafe che ti dà la flessibilità di C / C ++ ma anche con il pericolo di C / C ++.

Potrebbe essere utile esaminare il codice assembly che la JVM genera per il proprio codice

Per leggere un'app Java che esamina questo tipo di dettagli, consultare il codice Disruptor rilasciato da LMAX


2

A questa domanda è molto difficile rispondere, perché dipende dalle implementazioni del linguaggio.

In generale, al giorno d'oggi c'è ben poco spazio per tali "micro ottimizzazioni". Il motivo principale è che i compilatori sfruttano tali ottimizzazioni durante la compilazione. Ad esempio, non vi è alcuna differenza di prestazioni tra operatori pre-incremento e post-incremento in situazioni in cui la loro semantica è identica. Un altro esempio potrebbe essere ad esempio un ciclo come questo in for(int i=0; i<vec.size(); i++)cui si potrebbe sostenere che invece di chiamare ilsize()funzione membro durante ogni iterazione sarebbe meglio ottenere la dimensione del vettore prima del ciclo e quindi confrontarlo con quella singola variabile ed evitare così la funzione una chiamata per iterazione. Tuttavia, ci sono casi in cui un compilatore rileverà questo caso stupido e memorizzerà nella cache il risultato. Tuttavia, questo è possibile solo quando la funzione non ha effetti collaterali e il compilatore può essere sicuro che la dimensione del vettore rimanga costante durante il ciclo, quindi si applica semplicemente a casi abbastanza banali.


Per quanto riguarda il secondo caso, non credo che il compilatore possa ottimizzarlo nel prossimo futuro. Rilevare che è sicuro ottimizzare vec.size () dipende dal dimostrare che la dimensione se il vettore / perso non cambia all'interno del ciclo, che credo sia indecidibile a causa del problema di arresto.
Lie Ryan,

@LieRyan Ho visto molti casi (semplici) in cui il compilatore ha generato un file binario esattamente identico se il risultato è stato "memorizzato nella cache" manualmente e se è stato chiamato size (). Ho scritto del codice e si scopre che il comportamento dipende fortemente dal modo in cui il programma funziona. Ci sono casi in cui il compilatore può garantire che non è possibile cambiare la dimensione del vettore durante il ciclo, e quindi ci sono casi in cui non può garantirlo, in modo molto simile al problema di interruzione come hai detto. Per ora non sono in grado di verificare la mia richiesta (lo smontaggio del C ++ è un
problema

2
@Lie Ryan: molte cose che sono indecidibili nel caso generale sono perfettamente decidibili per casi specifici ma comuni, ed è davvero tutto ciò di cui hai bisogno qui.
Michael Borgwardt,

@LieRyan Se chiami constmetodi su questo vettore, sono abbastanza sicuro che molti compilatori di ottimizzazione lo capiranno .
K.Steff,

in C #, e penso di aver letto anche in Java, se non si memorizza la dimensione della cache, il compilatore sa che può rimuovere i controlli per vedere se si va fuori dai limiti dell'array e se si esegue la dimensione della cache, è necessario eseguire i controlli , che in genere costa più di quanto stai risparmiando memorizzando nella cache. Cercare di superare in astuzia gli ottimizzatori è raramente un buon piano.
Kate Gregory,

1

le persone potrebbero fornire esempi di quali trucchi è possibile utilizzare in Java (oltre ai semplici flag di compilazione).

Oltre ai miglioramenti degli algoritmi, assicurati di considerare la gerarchia di memoria e il modo in cui il processore la utilizza. Ci sono grandi vantaggi nel ridurre le latenze di accesso alla memoria, una volta capito come la lingua in questione alloca memoria ai suoi tipi di dati e oggetti.

Esempio Java per accedere a un array di 1000x1000 ints

Considera il seguente codice di esempio: accede alla stessa area di memoria (un array di ints 1000x1000), ma in un ordine diverso. Sul mio mac mini (Core i7, 2,7 GHz) l'output è il seguente, a dimostrazione del fatto che l'attraversamento dell'array per righe ha più che raddoppiato le prestazioni (in media oltre 100 round ciascuno).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Questo perché l'array è archiviato in modo tale che colonne consecutive (ovvero valori int) siano posizionate adiacenti in memoria, mentre le file consecutive non lo sono. Per utilizzare effettivamente i dati, il processore deve essere trasferito nella sua cache. Il trasferimento della memoria avviene tramite un blocco di byte, chiamato linea cache - il caricamento di una linea cache direttamente dalla memoria introduce latenze e quindi riduce le prestazioni di un programma.

Per il Core i7 (ponte sabbioso) una linea cache contiene 64 byte, quindi ogni accesso alla memoria recupera 64 byte. Poiché il primo test accede alla memoria in una sequenza prevedibile, il processore pre-recupererà i dati prima che vengano effettivamente consumati dal programma. Complessivamente, ciò si traduce in una minore latenza negli accessi alla memoria e quindi migliora le prestazioni.

Codice del campione:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }

1

La JVM può interferire e spesso interferisce e il compilatore JIT può cambiare in modo significativo tra le versioni. Alcune microottimizzazioni sono impossibili in Java a causa delle limitazioni del linguaggio, come l'hyper-threading o la raccolta SIMD dei più recenti processori Intel.

Si consiglia di leggere un blog altamente informativo sull'argomento di uno degli autori di Disruptor :

Uno deve sempre chiedersi perché preoccuparsi di usare Java se si desidera microottimizzazioni, ci sono molti metodi alternativi per l'accelerazione di una funzione come l'uso di JNA o JNI per passare a una libreria nativa.

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.