Il codice a bassa latenza a volte deve essere "brutto"?


21

(Questo è principalmente rivolto a coloro che hanno una conoscenza specifica dei sistemi a bassa latenza, per evitare che le persone rispondano semplicemente con opinioni prive di fondamento).

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.?

È logico - a chi importa se sembra brutto (purché sia ​​mantenibile) - se hai bisogno di velocità, hai bisogno di velocità?

Sarei interessato a ricevere notizie da persone che hanno lavorato in tali settori.


1
@ user997112: il motivo stretto è autoesplicativo. Dice: "Ci aspettiamo che le risposte siano supportate da fatti, riferimenti o competenze specifiche, ma questa domanda probabilmente solleciterà il dibattito, gli argomenti, il sondaggio o una discussione estesa. Non significa necessariamente che siano corretti, ma era la chiusura motivo scelto da tutti e tre i votanti stretti
Robert Harvey,

Aneddoticamente, direi che la ragione per cui questa domanda sta attirando voti stretti è che potrebbe essere percepita come una trappola sottilmente velata (anche se non credo che lo sia).
Robert Harvey,

8
Tiro fuori il collo: ho espresso il terzo voto per chiudere come "non costruttivo" perché penso che l'interrogatore risponda praticamente alla sua stessa domanda. Il codice "bello" che non viene eseguito abbastanza velocemente per eseguire il lavoro non è riuscito a soddisfare i requisiti di latenza. Il codice "brutto" che funziona abbastanza velocemente può essere reso più gestibile attraverso una buona documentazione. Come misurare la bellezza o la bruttezza è un argomento per un'altra domanda.
Blrfl

1
Il codice sorgente per LMAX's Disruptor non è troppo brutto. Ci sono alcune parti "al diavolo con il modello di sicurezza Java" (classe Unsafe) e alcune modifiche specifiche all'hardware (variabili riempite con cache-line) ma è IMO molto leggibile.
James,

5
@ Carson63000, user1598390 e chiunque altro sia interessato: se la domanda finisce chiusa, sentiti libero di chiedere la chiusura sul nostro sito Meta , è inutile discutere una chiusura nei commenti, in particolare una chiusura che non è avvenuta . Inoltre, tieni presente che ogni domanda chiusa può essere riaperta, non è la fine del mondo. Tranne ovviamente se i Maya avevano ragione, nel qual caso è stato bello conoscervi tutti!
yannis,

Risposte:


31

Pensi che ci sia un compromesso tra la scrittura di un codice "simpatico" orientato agli oggetti e la scrittura di un codice [lat] a latenza molto bassa?

Sì.

Ecco perché esiste la frase "ottimizzazione prematura". Esiste per forzare gli sviluppatori a misurare le loro prestazioni e ottimizzare solo quel codice che farà la differenza nelle prestazioni, progettando sensibilmente la loro architettura applicativa dall'inizio in modo che non cada sotto carico.

In questo modo, nella massima misura possibile, riuscirai a mantenere il tuo codice carino, ben progettato, orientato agli oggetti e a ottimizzare solo con brutto codice quelle piccole porzioni che contano.


15
"Fallo funzionare, poi fallo in fretta". Questa risposta copre praticamente tutto ciò che pensavo di dire mentre leggevo la domanda.
Carson63000,

13
Aggiungerò "Misura, non indovinare"
Martijn Verburg del

1
Penso che valga la pena di prenderne parte all'evitamento del lavoro di base, a condizione che non vada a scapito della leggibilità. Mantenere le cose concise, leggibili e fare solo le cose ovvie che devono fare porta a molte vittorie indirette a lungo termine come altri sviluppatori che sanno cosa diavolo fare del tuo codice in modo da non duplicare lo sforzo o fare cattive ipotesi su come funziona.
Erik Reppen,

1
Sull'ottimizzazione prematura - vale ancora anche se il codice ottimizzato sarà "bello" come il codice non ottimizzato. Il punto è non perdere tempo puntando sulla velocità / qualunque cosa tu non abbia bisogno di raggiungere. In effetti, l'ottimizzazione non riguarda sempre la velocità, e probabilmente c'è una cosa come l'ottimizzazione non necessaria per la "bellezza". Il tuo codice non deve essere una grande opera d'arte per essere leggibile e mantenibile.
Steve314,

Io secondo @ Steve314. Sono il responsabile delle prestazioni di un prodotto e spesso trovo un codice estremamente complicato, la cui origine può risalire a una sorta di ottimizzazione delle prestazioni. Semplificare quel codice rivela spesso un significativo miglioramento delle prestazioni. Uno di questi esempi si è trasformato in un miglioramento delle prestazioni 5 volte quando l'ho semplificato (riduzione netta di migliaia di righe di codice). Chiaramente, nessuno si è preso il tempo per misurare effettivamente e semplicemente ha fatto l'ottimizzazione prematura di quello che pensavano sarebbe stato un codice lento .
Brandon,

5

Sì, l'esempio che do non è C ++ vs. Java ma è Assembly vs. COBOL come è quello che conosco.

Entrambe le lingue sono molto veloci, ma anche COBOL quando compilato ha molte più istruzioni che vengono inserite nel set di istruzioni che non devono necessariamente essere presenti rispetto alla scrittura di tali istruzioni da soli in Assembly.

La stessa idea può essere applicata direttamente alla tua domanda di scrivere "codice dall'aspetto brutto" rispetto all'uso dell'ereditarietà / polimorfismo in C ++. Credo che sia necessario scrivere un codice dall'aspetto sgradevole, se l'utente finale ha bisogno di intervalli di tempo di transazione inferiori al secondo, allora è nostro compito come programmatori dare loro che non importa come succede.

Detto questo, l'uso liberale dei commenti aumenta notevolmente la funzionalità e la manutenibilità del programmatore, non importa quanto brutto sia il codice.


3

Sì, esiste un compromesso. Con questo intendo che il codice più veloce e più brutto non è necessario meglio: i vantaggi quantitativi del "codice veloce" devono essere ponderati rispetto alla complessità di manutenzione delle modifiche al codice necessarie per raggiungere tale velocità.

Il compromesso deriva dal costo aziendale. Il codice più complesso richiede programmatori più qualificati (e programmatori con un set di competenze più mirato, come quelli con architettura della CPU e conoscenze di progettazione), impiega più tempo a leggere e comprendere il codice e a correggere i bug. Il costo aziendale per lo sviluppo e il mantenimento di tale codice potrebbe essere compreso tra 10 e 100 volte rispetto al codice normalmente scritto.

Questo costo di manutenzione è giustificabile in alcuni settori , in cui i clienti sono disposti a pagare un premio molto elevato per un software molto veloce.

Alcune ottimizzazioni della velocità rendono un ritorno sull'investimento (ROI) migliore di altri. Vale a dire, alcune tecniche di ottimizzazione possono essere applicate con un impatto minore sulla manutenibilità del codice (preservando la struttura di livello superiore e la leggibilità di livello inferiore) rispetto al codice normalmente scritto.

Pertanto, un imprenditore dovrebbe:

  • Guarda i costi e i benefici,
  • Effettuare misurazioni e calcoli
    • Chiedi al programmatore di misurare la velocità del programma
    • Chiedi al programmatore di stimare il tempo di sviluppo necessario per l'ottimizzazione
    • Fai la tua stima dell'aumento delle entrate grazie a un software più veloce
    • Chiedi ai progettisti di software o ai responsabili della gestione della qualità di valutare qualitativamente gli svantaggi derivanti dalla ridotta intuitività e leggibilità del codice sorgente
  • E dai la priorità ai frutti bassi dell'ottimizzazione del software.

Questi compromessi sono altamente specifici per le circostanze.

Questi non possono essere decisi in modo ottimale senza la partecipazione di manager e proprietari di prodotti.

Questi sono altamente specifici per le piattaforme. Ad esempio, le CPU desktop e mobili hanno diverse considerazioni. Anche le applicazioni server e client hanno diverse considerazioni.


Sì, è generalmente vero che il codice più veloce sembra diverso dal codice normalmente scritto. Qualsiasi codice diverso richiederà più tempo per la lettura. Se ciò implica bruttezza è negli occhi di chi guarda.

Le tecniche con cui ho una certa esposizione sono: (senza cercare di rivendicare alcun livello di competenza) ottimizzazione a vettore corto (SIMD), parallelismo delle attività a grana fine, pre-allocazione della memoria e riutilizzo degli oggetti.

SIMD ha in genere gravi ripercussioni sulla leggibilità di basso livello, anche se in genere non richiede modifiche strutturali di livello superiore (a condizione che l'API sia progettata tenendo presente la prevenzione dei colli di bottiglia).

Alcuni algoritmi possono essere facilmente trasformati in SIMD (l'imbarazzantemente vettorializzabile). Alcuni algoritmi richiedono più riarrangiamenti di calcolo per poter utilizzare SIMD. In casi estremi come il parallelismo SIMD sul fronte d'onda, è necessario scrivere algoritmi completamente nuovi (e implementazioni brevettabili) per trarne vantaggio.

La parallelizzazione delle attività a grana fine richiede la riorganizzazione degli algoritmi nei grafici del flusso di dati e l'applicazione ripetuta della decomposizione funzionale (computazionale) all'algoritmo fino a quando non è possibile ottenere ulteriori vantaggi di margine. Le fasi decomposte sono in genere concatenate con uno stile di continuazione, un concetto preso in prestito dalla programmazione funzionale.

Per decomposizione funzionale (computazionale), gli algoritmi che avrebbero potuto essere normalmente scritti in una sequenza lineare e concettualmente chiara (righe di codice eseguibili nello stesso ordine in cui sono scritti) devono essere suddivisi in frammenti e distribuiti in più funzioni o classi. (Vedi l'objectification dell'algoritmo, di seguito). Questa modifica ostacolerà notevolmente i colleghi programmatori che non hanno familiarità con il processo di progettazione della decomposizione che ha dato origine a tale codice.

Per rendere tale codice mantenibile, gli autori di tale codice devono scrivere documentazioni elaborate dell'algoritmo - ben oltre il tipo di commenti sul codice o diagrammi UML fatti per il codice normalmente scritto. Questo è simile al modo in cui i ricercatori scrivono i loro articoli accademici.


No, il codice rapido non deve necessariamente essere in contraddizione con l'orientamento agli oggetti.

In altre parole, è possibile implementare un software molto veloce che è ancora orientato agli oggetti. Tuttavia, verso l'estremità inferiore di tale implementazione (a livello di dadi e bulloni in cui si verifica la maggior parte del calcolo), il design degli oggetti può discostarsi in modo significativo dai design ottenuti dal design orientato agli oggetti (OOD). Il design di livello inferiore è orientato verso l'algoritmo-oggettivazione.

Alcuni vantaggi della programmazione orientata agli oggetti (OOP), come l'incapsulamento, il polimorfismo e la composizione, possono ancora essere ricavati dall'objectification di algoritmo di basso livello.Questa è la principale giustificazione per l'utilizzo di OOP a questo livello.

La maggior parte dei vantaggi della progettazione orientata agli oggetti (OOD) viene persa. Ancora più importante, non c'è intuitività nel design di basso livello. Un collega programmatore non può imparare a lavorare con il codice di livello inferiore senza prima comprendere appieno come l'algoritmo è stato trasformato e decomposto in primo luogo, e questa comprensione non è ottenibile dal codice risultante.


2

Sì, a volte il codice deve essere "brutto" per farlo funzionare nel tempo richiesto, ma tutto il codice non deve essere brutto. Le prestazioni dovrebbero essere testate e profilate prima di trovare i pezzi di codice che devono essere "brutti" e quelle sezioni dovrebbero essere annotate con un commento in modo che i futuri sviluppatori sappiano cosa è brutto di proposito e cosa è solo pigrizia. Se qualcuno sta scrivendo un sacco di codice mal progettato che rivendica motivi di prestazioni, faglielo dimostrare.

La velocità è importante quanto qualsiasi altro requisito di un programma, fornire correzioni errate a un missile guidato equivale a fornire le giuste correzioni dopo l'impatto. La manutenibilità è sempre una preoccupazione secondaria del codice di lavoro.


2

Alcuni degli studi che ho visto indicano che il codice pulito e facile da leggere è spesso più veloce del codice più complesso e difficile da leggere. In parte, ciò è dovuto al modo in cui sono progettati gli ottimizzatori. Tendono ad essere molto migliori nell'ottimizzare una variabile in un registro, piuttosto che fare lo stesso con un risultato intermedio di un calcolo. Le sequenze lunghe di assegnazioni che utilizzano un singolo operatore che portano al risultato finale possono essere ottimizzate meglio di un'equazione lunga e complicata. I nuovi ottimizzatori potrebbero aver ridotto la differenza tra codice pulito e codice complicato, ma dubito che lo abbiano eliminato.

Altre ottimizzazioni come lo svolgimento di loop possono essere aggiunte in modo pulito quando richiesto.

Qualsiasi ottimizzazione aggiunta per migliorare le prestazioni dovrebbe essere accompagnata da un commento appropriato. Ciò dovrebbe includere una dichiarazione che è stato aggiunto come ottimizzazione, preferibilmente con misure di prestazione prima e dopo.

Ho trovato che la regola 80/20 si applica al codice che ho ottimizzato. Come regola generale, non ottimizzo nulla che non richieda almeno l'80% delle volte. Quindi mirare (e di solito ottenere) un aumento delle prestazioni di 10 volte. Ciò migliora le prestazioni di circa 4 volte. La maggior parte delle ottimizzazioni che ho implementato non hanno reso il codice significativamente meno "bello". Il tuo chilometraggio può variare.


2

Se per brutto, vuoi dire difficile da leggere / capire a livello di dove gli altri sviluppatori lo useranno nuovamente o avranno bisogno di capirlo, allora direi che un codice elegante e di facile lettura alla fine ti renderà quasi sempre un aumento delle prestazioni a lungo termine in un'app che devi mantenere.

Altrimenti, a volte c'è abbastanza di una vittoria in termini di prestazioni per far valere la pena mettere brutti in una bella scatola con un'interfaccia killer su di essa, ma nella mia esperienza, questo è un dilemma piuttosto raro.

Pensa all'evitamento del lavoro di base mentre procedi. Salva i trucchi arcani per quando si presenta effettivamente un problema di prestazioni. E se devi scrivere qualcosa che qualcuno potrebbe capire solo attraverso la familiarità con l'ottimizzazione specifica, fai quello che puoi almeno per rendere il brutto facile da capire da un riutilizzo del tuo punto di vista del codice. Il codice che esegue miseramente raramente lo fa mai perché gli sviluppatori stavano pensando troppo a ciò che il prossimo avrebbe ereditato, ma se i cambiamenti frequenti sono l'unica costante di un'app (la maggior parte delle app Web nella mia esperienza), codice rigido / non flessibile che è difficile da modificare è praticamente chiedere l'elemosina nel panico per iniziare a spuntare in tutta la tua base di codice. Pulito e snello è meglio per le prestazioni a lungo termine.


Vorrei suggerire due cambiamenti: (1) Ci sono luoghi in cui è necessaria la velocità. In quei luoghi, penso che valga la pena comprendere meglio l' interfaccia , piuttosto che rendere comprensibile l' implementazione , poiché quest'ultima potrebbe essere molto più difficile. (2) "Il codice che esegue miseramente raramente lo fa mai ...", che vorrei riformulare come "Una forte enfasi sull'eleganza e la semplicità del codice è raramente la causa di prestazioni miserabili. Il primo è ancora più importante se frequenti cambiamenti sono anticipati, ... "
rwong

L'implementazione è stata una cattiva scelta di parole in una conversazione OOPish. Lo intendevo in termini di facilità di riutilizzo e modifica. # 2, ho appena aggiunto una frase per stabilire che 2 è essenzialmente il punto che stavo sollevando.
Erik Reppen,

1

Complessi e brutti non sono la stessa cosa. Il codice che ha molti casi speciali, che è ottimizzato per ottenere l'ultimo calo delle prestazioni e che sembra inizialmente un groviglio di connessioni e dipendenze può infatti essere progettato con molta attenzione e molto bello una volta capito. In effetti, se le prestazioni (misurate in termini di latenza o altro) sono abbastanza importanti da giustificare un codice molto complesso, allora il codice deve essere ben progettato. In caso contrario, non puoi essere sicuro che tutta quella complessità sia davvero migliore di una soluzione più semplice.

Il codice brutto, per me, è un codice che è sciatto, scarsamente considerato e / o inutilmente complicato. Non credo che vorresti avere nessuna di quelle funzionalità nel codice che deve eseguire.


1

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 ArrayListdi Integeroggetti 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).


0

Non essere diverso, ma ecco cosa faccio:

  1. Scrivilo pulito e mantenibile.

  2. Fai una diagnosi delle prestazioni e risolvi i problemi che ti dice, non quelli che indovini. Garantiti, saranno diversi da quello che ti aspetti.

Puoi fare queste correzioni in modo che sia ancora chiaro e gestibile, ma dovrai aggiungere commenti in modo che le persone che guardano il codice sappiano perché l'hai fatto in quel modo. In caso contrario, lo annulleranno.

Quindi c'è un compromesso? Non credo proprio.


0

Puoi scrivere un brutto codice che è molto veloce e puoi anche scrivere un bellissimo codice che è veloce come il tuo brutto codice. Il collo di bottiglia non sarà nella bellezza / organizzazione / struttura del tuo codice ma nelle tecniche che hai scelto. Ad esempio, stai usando socket non bloccanti? Stai utilizzando un design a thread singolo? Stai usando una coda senza blocchi per la comunicazione tra thread? Stai producendo immondizia per il GC? Stai eseguendo un'operazione di I / O di blocco nel thread critico? Come puoi vedere, questo non ha nulla a che fare con la bellezza.


0

Che cosa conta l'utente finale?

  • Prestazione
  • Caratteristiche / Funzionalità
  • Design

Caso 1: codice errato ottimizzato

  • Dura manutenzione
  • Difficilmente leggibile se come progetto open source

Caso 2: buon codice non ottimizzato

  • Facile manutenzione
  • Esperienza utente negativa

Soluzione?

Facile, ottimizzare pezzi di codice critici per le prestazioni

per esempio:

Un programma composto da 5 metodi , 3 dei quali sono per la gestione dei dati, 1 per la lettura del disco, l'altro per la scrittura del disco

Questi 3 metodi di gestione dei dati utilizzano i due metodi I / O e dipendono da essi

Vorremmo ottimizzare i metodi I / O.

Motivo: i metodi I / O hanno meno probabilità di essere modificati, né influiscono sulla progettazione dell'app e, tutto sommato, tutto in quel programma dipende da loro, e quindi sembrano critici per le prestazioni, utilizzeremmo qualsiasi codice per ottimizzarli .

Ciò significa che otteniamo un buon codice e un design gestibile del programma, mantenendolo veloce ottimizzando alcune parti del codice

Sto pensando..

Penso che un cattivo codice renda difficile per gli umani l'ottimizzazione e che piccoli errori potrebbero peggiorare ulteriormente, quindi un buon codice per un principiante / principiante sarebbe meglio se solo scrivesse bene quel brutto codice.

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.