Pensi che ci sia un compromesso tra la scrittura di un codice "simpatico" orientato agli oggetti e la scrittura di un codice a bassa latenza molto veloce? Ad esempio, evitando le funzioni virtuali in C ++ / l'overhead del polimorfismo, ecc. Riscrivere il codice che sembra cattivo, ma è molto veloce, ecc.?
Lavoro in un campo un po 'più incentrato sulla velocità effettiva che sulla latenza, ma è molto critico per le prestazioni e direi "una specie" .
Eppure un problema è che così tante persone sbagliano completamente le loro nozioni di performance. I novizi spesso sbagliano quasi tutto e il loro intero modello concettuale di "costo computazionale" ha bisogno di essere rielaborato, con solo la complessità algoritmica che riguarda l'unica cosa che possono fare. Gli intermedi sbagliano molte cose. Gli esperti sbagliano alcune cose.
Misurare con strumenti accurati in grado di fornire metriche quali mancati riscontri nella cache e errori di previsione delle filiali è ciò che tiene sotto controllo tutte le persone di qualsiasi livello di esperienza nel settore.
La misurazione è anche ciò che indica cosa non ottimizzare . Gli esperti spesso impiegano meno tempo a ottimizzare rispetto ai principianti, poiché stanno ottimizzando i veri hotspot misurati e non stanno cercando di ottimizzare le pugnalate selvagge nel buio sulla base di intuizioni su ciò che potrebbe essere lento (che, in forma estrema, potrebbe tentare di micro-ottimizzare solo su ogni altra riga nella base di codice).
Progettare per le prestazioni
A parte questo, la chiave per progettare per le prestazioni proviene dalla parte del design , come nella progettazione dell'interfaccia. Uno dei problemi dell'inesperienza è che tende a esserci uno spostamento precoce delle metriche di implementazione assolute, come il costo di una chiamata di funzione indiretta in un contesto generalizzato, come se il costo (che è meglio compreso in senso immediato dal punto di vista di un ottimizzatore of view anziché un punto di vista ramificato) è un motivo per evitarlo nell'intera base di codice.
I costi sono relativi . Sebbene vi sia un costo per una chiamata di funzione indiretta, ad esempio, tutti i costi sono relativi. Se stai pagando quel costo una volta per chiamare una funzione che scorre attraverso milioni di elementi, preoccuparsi di questo costo è come passare ore a contrattare con penny per l'acquisto di un prodotto da un miliardo di dollari, solo per concludere di non acquistare quel prodotto perché era un centesimo troppo costoso.
Design dell'interfaccia più grossolana
L' aspetto del design dell'interfaccia delle prestazioni spesso cerca prima di spingere questi costi a un livello più grossolano. Invece di pagare i costi di astrazione di runtime per una singola particella, ad esempio, potremmo spingere quel costo al livello del sistema di particelle / emettitore, trasformando effettivamente una particella in un dettaglio di implementazione e / o semplicemente dati grezzi di questa raccolta di particelle.
Quindi la progettazione orientata agli oggetti non deve essere incompatibile con la progettazione per le prestazioni (sia latenza che throughput), ma ci possono essere tentazioni in un linguaggio che si concentra su di esso per modellare oggetti granulari sempre più adolescenti e lì l'ultimo ottimizzatore non può Aiuto. Non può fare cose come unire una classe che rappresenta un singolo punto in un modo che produce una rappresentazione SoA efficiente per i modelli di accesso alla memoria del software. Una raccolta di punti con il design dell'interfaccia modellato a livello di grossolanità offre tale opportunità e consente di scorrere verso soluzioni sempre più ottimali in base alle esigenze. Tale design è progettato per la memoria di massa *.
* Nota l'attenzione sulla memoria qui e non sui dati , poiché lavorare a lungo in aree critiche per le prestazioni tenderà a cambiare la tua visione dei tipi di dati e delle strutture dei dati e vedere come si collegano alla memoria. Un albero di ricerca binario non diventa più solo sulla complessità logaritmica in casi come blocchi di memoria possibilmente disparati e ostili alla cache per i nodi degli alberi se non aiutati da un allocatore fisso. La vista non elimina la complessità algoritmica, ma non la vede più indipendentemente dai layout di memoria. Si inizia anche a vedere le iterazioni di lavoro come più sulle iterazioni di accesso alla memoria. *
Molti progetti critici per le prestazioni possono effettivamente essere molto compatibili con l'idea di progetti di interfaccia di alto livello che sono facili da capire e da usare per gli umani. La differenza è che "di alto livello" in questo contesto riguarderebbe l'aggregazione di massa della memoria, un'interfaccia modellata per raccolte potenzialmente grandi di dati e con un'implementazione nascosta che potrebbe essere di livello piuttosto basso. Un'analogia visiva potrebbe essere un'auto davvero comoda e facile da guidare e da maneggiare e molto sicura mentre si va alla velocità del suono, ma se fai scoppiare il cofano, ci sono piccoli demoni che respirano il fuoco all'interno.
Con un design più grossolano, tende anche a rappresentare un modo più semplice per fornire schemi di blocco più efficienti e sfruttare il parallelismo nel codice (il multithreading è un argomento esaustivo che salterò qui).
Pool di memoria
Un aspetto critico della programmazione a bassa latenza sarà probabilmente un controllo molto esplicito sulla memoria per migliorare la località di riferimento, nonché solo la velocità generale di allocazione e deallocazione della memoria. Una memoria di pool di allocatori personalizzati fa effettivamente eco allo stesso tipo di mentalità progettuale che abbiamo descritto. È progettato per la maggior parte ; è progettato a un livello approssimativo. Prealloca la memoria in blocchi di grandi dimensioni e raggruppa la memoria già allocata in piccoli blocchi.
L'idea è esattamente la stessa di spingere cose costose (allocare un pezzo di memoria contro un allocatore per scopi generici, ad esempio) a un livello più grossolano e più grossolano. Un pool di memoria è progettato per gestire la memoria in blocco .
Tipo Sistemi Segrega memoria
Una delle difficoltà con la progettazione granulare orientata agli oggetti in qualsiasi lingua è che spesso vuole introdurre molti tipi e strutture di dati definiti dall'utente. Quei tipi possono quindi voler essere allocati in piccoli pezzi adolescenti se sono allocati dinamicamente.
Un esempio comune in C ++ sarebbe per i casi in cui è richiesto il polimorfismo, in cui la naturale tentazione è quella di allocare ogni istanza di una sottoclasse a un allocatore di memoria generico.
Questo finisce per spezzare layout di memoria possibilmente contigui in piccoli bit e pezzi sparsi in tutto il campo di indirizzamento, il che si traduce in più errori di pagina e mancate cache.
I campi che richiedono una risposta deterministica a latenza più bassa, senza balbuzie, sono probabilmente l'unico posto in cui gli hotspot non si riducono sempre a un singolo collo di bottiglia, dove piccole inefficienze possono effettivamente effettivamente "accumularsi" (qualcosa che molte persone immaginano accadendo in modo errato con un profiler per tenerli sotto controllo, ma nei campi guidati dalla latenza, in realtà ci possono essere alcuni rari casi in cui si accumulano piccole inefficienze). E molte delle ragioni più comuni di un tale accumulo possono essere queste: l'eccessiva allocazione di piccoli pezzi di memoria in tutto il luogo.
In linguaggi come Java, può essere utile utilizzare più array di semplici tipi di dati vecchi quando possibile per aree collo di bottiglia (aree elaborate in loop stretti) come una matrice di int
(ma ancora dietro un'interfaccia di alto livello ingombrante) invece di, diciamo , uno ArrayList
di Integer
oggetti definiti dall'utente . Questo evita la segregazione della memoria che normalmente accompagnerebbe quest'ultima. In C ++, non dobbiamo degradare la struttura abbastanza se i nostri modelli di allocazione della memoria sono efficienti, poiché i tipi definiti dall'utente possono essere allocati contigui lì e anche nel contesto di un contenitore generico.
Memoria di fusione insieme
Una soluzione qui è quella di raggiungere un allocatore personalizzato per tipi di dati omogenei e possibilmente anche tra tipi di dati omogenei. Quando piccoli tipi di dati e strutture di dati vengono appiattiti in bit e byte in memoria, assumono una natura omogenea (sebbene con alcuni requisiti di allineamento variabili). Quando non li guardiamo da una mentalità incentrata sulla memoria, il sistema di tipi di linguaggi di programmazione "vuole" dividere / separare regioni di memoria potenzialmente contigue in piccoli pezzi sparsi da adolescenti.
Lo stack utilizza questo focus incentrato sulla memoria per evitarlo e potenzialmente memorizzare al suo interno qualsiasi possibile combinazione mista di istanze di tipo definite dall'utente. Utilizzare lo stack di più è una grande idea quando possibile poiché la parte superiore è quasi sempre posizionata in una riga della cache, ma possiamo anche progettare allocatori di memoria che imitano alcune di queste caratteristiche senza un modello LIFO, fondendo la memoria su diversi tipi di dati in contigui blocchi anche per schemi di allocazione e deallocazione di memoria più complessi.
L'hardware moderno è progettato per essere al suo apice durante l'elaborazione di blocchi di memoria contigui (accedendo ripetutamente alla stessa linea di cache, alla stessa pagina, ad es.). La parola chiave è contiguità, in quanto ciò è utile solo se ci sono dati di interesse circostanti. Quindi gran parte della chiave (ma anche della difficoltà) delle prestazioni è quella di fondere nuovamente blocchi di memoria segregati in blocchi contigui a cui si accede nella loro interezza (tutti i dati circostanti sono rilevanti) prima dello sfratto. Il ricco sistema di tipi di tipi specialmente definiti dall'utente nei linguaggi di programmazione può essere l'ostacolo maggiore qui, ma possiamo sempre aggirare e risolvere il problema attraverso un allocatore personalizzato e / o progetti più voluminosi, se del caso.
Brutto
"Brutto" è difficile da dire. È una metrica soggettiva e qualcuno che lavora in un campo molto critico in termini di prestazioni inizierà a cambiare la sua idea di "bellezza" in una molto più orientata ai dati e focalizzata su interfacce che elaborano le cose alla rinfusa.
Pericoloso
"Pericoloso" potrebbe essere più semplice. In generale, le prestazioni tendono a voler raggiungere il codice di livello inferiore. L'implementazione di un allocatore di memoria, ad esempio, è impossibile senza raggiungere i tipi di dati e lavorare al livello pericoloso di bit e byte non elaborati. Di conseguenza, può aiutare a focalizzare l'attenzione su un'attenta procedura di test in questi sottosistemi critici per le prestazioni, ridimensionando l'accuratezza dei test con il livello di ottimizzazioni applicate.
bellezza
Tuttavia, tutto ciò sarebbe a livello di dettaglio di attuazione. In una veterana mentalità su larga scala e critica delle prestazioni, la "bellezza" tende a spostarsi verso i design dell'interfaccia piuttosto che i dettagli di implementazione. Diventa una priorità esponenzialmente più alta cercare interfacce "belle", utilizzabili, sicure ed efficienti piuttosto che implementazioni dovute a rotture di accoppiamento e a cascata che possono verificarsi a seguito di un cambio di progettazione dell'interfaccia. Le implementazioni possono essere scambiate in qualsiasi momento. In genere ripetiamo le prestazioni secondo necessità e come sottolineato dalle misurazioni. La chiave con il design dell'interfaccia è modellare a un livello abbastanza grossolano per lasciare spazio a tali iterazioni senza rompere l'intero sistema.
In effetti, suggerirei che l'attenzione di un veterano allo sviluppo critico per le prestazioni tenderà spesso a porre l'attenzione prevalente sulla sicurezza, i test, la manutenibilità, solo il discepolo di SE in generale, poiché una base di codice su larga scala che ha una serie di prestazioni sottosistemi critici (sistemi di particelle, algoritmi di elaborazione delle immagini, elaborazione video, feedback audio, raytracer, motori mesh, ecc.) dovranno prestare particolare attenzione all'ingegneria del software per evitare di annegare in un incubo di manutenzione. Non è una semplice coincidenza che spesso i prodotti più sorprendentemente efficienti là fuori possano anche avere il minor numero di bug.
TL; DR
Ad ogni modo, questa è la mia opinione sull'argomento, che spazia dalle priorità in settori veramente critici per le prestazioni, cosa può ridurre la latenza e causare accumulazioni di piccole inefficienze e ciò che costituisce effettivamente la "bellezza" (quando si guardano le cose in modo più produttivo).