[...] (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 Pixel
oggetto (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' Pixel
oggetto, usassi un array di byte e modellassi un Image
oggetto. 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 int
invece di Integer
) e iniziare a modellare interfacce di massa come un Image
interfaccia, non Pixel
interfaccia. 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 int
garantisce una rappresentazione contigua. Una serie di Integer
no. 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 Integer
potrebbe 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 circostantiIntegers
erano 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 Integer
essere 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 final
per 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 Integer
vs. int
può 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
, float
ecc., 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 int
vs. 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 ints
e un milione casuale Integers
e di farlo ripetutamente (il Integers
rimpasto 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 Account
se 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.