Vorrei chiedere alle persone con esperienza nel lavorare con i sistemi la scala di Visual Studio: che cosa li rende lenti? Sono gli strati su strati di astrazioni necessari per mantenere la base di codice all'interno delle capacità di comprensione umana? È la pura quantità di codice che deve essere eseguita? È la tendenza moderna verso approcci che fanno risparmiare tempo al programmatore a spese (incredibilmente enormi) nel reparto cicli di clock / utilizzo della memoria?
Penso che tu ne abbia indovinato un certo numero, ma vorrei offrire quello che considero il fattore più importante, avendo lavorato su una base di codice ragionevolmente grande (non sono sicuro che sia grande come Visual Studio - era tra i milioni di righe di codice categoria e circa un migliaio di plugin) per circa 10 anni e si verificano fenomeni osservativi.
È anche un po 'meno controverso poiché non va nelle API o nelle funzionalità del linguaggio o cose del genere. Questi si riferiscono a "costi" che possono generare un dibattito piuttosto che a "spese", e voglio concentrarmi su "spese".
Coordinamento e legami allentati
Quello che ho osservato è che un coordinamento lento e una lunga eredità tendono a portare a molti rifiuti accumulati.
Ad esempio, ho trovato circa cento strutture di accelerazione in questa base di codice, molte delle quali ridondanti.
Avremmo come un albero KD per accelerare un motore fisico, un altro per un nuovo motore fisico che spesso correva in parallelo con quello vecchio, avremmo avuto dozzine di implementazioni di ocre per vari algoritmi di mesh, un altro albero KD per il rendering , raccolta, ecc. ecc. ecc. Queste sono tutte strutture di alberi grandi e voluminose utilizzate per accelerare le ricerche. Ogni singolo può portare da centinaia di megabyte a gigabyte di memoria per un input di dimensioni molto medie. Non sono sempre stati istanziati e utilizzati tutto il tempo, ma in qualsiasi momento, 4 o 5 potrebbero essere in memoria contemporaneamente.
Ora tutti questi stavano memorizzando gli stessi identici dati per accelerare le ricerche per loro. Puoi immaginarlo come il vecchio database analogico che memorizza tutti i suoi campi in 20 diverse mappe / dizionari / alberi B + ridondanti contemporaneamente, organizzati in modo identico con le stesse chiavi e li cerca continuamente. Ora stiamo prendendo 20 volte la memoria e l'elaborazione.
Inoltre, a causa della ridondanza, c'è poco tempo per ottimizzare uno di essi con il prezzo di manutenzione che ne deriva e, anche se lo facessimo, avrebbe solo il 5% dell'effetto che idealmente avrebbe.
Quali sono le cause di questi fenomeni? La coordinazione libera è stata la causa numero uno che ho visto. Molti membri del team lavorano spesso nei loro ecosistemi isolati, sviluppando o utilizzando strutture di dati di terze parti, ma non usando le stesse strutture che altri membri del team stavano usando anche se erano palesi duplicati delle stesse identiche preoccupazioni.
Cosa causa il persistere di questi fenomeni? L'eredità e la compatibilità sono state la causa numero uno che ho visto. Poiché abbiamo già pagato i costi per implementare queste strutture di dati e grandi quantità di codice dipendevano da queste soluzioni, spesso era troppo rischioso tentare di consolidarle in meno strutture di dati. Anche se molte di queste strutture dati erano altamente ridondanti concettualmente, non erano sempre vicine allo stesso identico nei loro design di interfaccia. Quindi sostituirli sarebbe stato un grande e rischioso cambiamento piuttosto che lasciarli consumare memoria e tempo di elaborazione.
Efficienza di memoria
In genere l'uso della memoria e la velocità tendono ad essere correlati almeno a livello di massa. Spesso è possibile individuare il software lento in base al modo in cui si sta eseguendo il backup della memoria. Non è sempre vero che più memoria porta a un rallentamento, poiché ciò che conta è la memoria "calda" (quale memoria si accede continuamente - se un programma utilizza un carico di memoria della barca ma solo 1 megabyte viene utilizzato tutto il tempo, quindi non è un grosso problema in termini di velocità).
Quindi puoi individuare i potenziali maiali in base all'uso della memoria per la maggior parte del tempo. Se un'applicazione richiede decine di centinaia di megabyte di memoria all'avvio, probabilmente non sarà molto efficiente. Decine di megabyte potrebbero sembrare piccole quando abbiamo gigabyte di DRAM in questi giorni, ma le cache della CPU più grandi e lente sono ancora nell'intervallo di megabyte miseri, e le più veloci sono ancora nell'intervallo dei kilobyte. Di conseguenza, un programma che utilizza 20 megabyte solo per avviarsi e non fare nulla in realtà sta ancora usando abbastanza "molta" memoria dal punto di vista della cache della CPU hardware, soprattutto se si accederà ripetutamente a tutti i 20 megabyte di tale memoria e frequentemente mentre il programma è in esecuzione.
Soluzione
Per me la soluzione è cercare team più coordinati e più piccoli per costruire prodotti, quelli che possono in qualche modo tenere traccia delle loro "spese" ed evitare di "acquistare" gli stessi articoli ancora e ancora e ancora.
Costo
Mi immergerò nella parte più controversa del "costo" solo un po 'da teenager con un fenomeno di "spesa" che ho osservato. Se un linguaggio finisce con un inevitabile prezzo per un oggetto (come uno che fornisce una riflessione sul runtime e non può forzare l'allocazione contigua per una serie di oggetti), quel prezzo è costoso solo nel contesto di un elemento molto granulare, come un singolo Pixel
o Boolean
.
Eppure vedo un sacco di codice sorgente per i programmi che gestiscono un carico pesante (es: gestire da centinaia di migliaia a milioni di Pixel
o Boolean
casi) pagando quel costo a un livello così granulare.
La programmazione orientata agli oggetti può in qualche modo esasperarla. Eppure non è il costo di "oggetti" di per sé o addirittura OOP in difetto, è semplicemente che tali costi vengono pagati a un livello così granulare di un elemento adolescente che verrà istanziato da milioni.
Quindi questo è l'altro fenomeno "costo" e "spesa" che sto osservando. Il costo è di pochi centesimi, ma si sommano se acquistiamo un milione di lattine di soda singolarmente invece di negoziare con un produttore per un acquisto all'ingrosso.
La soluzione qui per me è l'acquisto "all'ingrosso". Gli oggetti vanno benissimo anche nei linguaggi che hanno un prezzo di centesimi per ognuno a condizione che questo costo non venga pagato individualmente un milione di volte per l'equivalente analogico di una lattina di soda.
Ottimizzazione precoce
Non mi è mai piaciuta molto la formulazione usata da Knuth qui, perché "l'ottimizzazione prematura" raramente rende i programmi di produzione reali più veloci. Alcuni interpretano ciò come "ottimizzazione precoce" quando Knuth significava più "ottimizzazione senza la conoscenza / esperienza adeguata per conoscere il suo vero impatto sul software". Semmai, l'effetto pratico della vera ottimizzazione prematura spesso rallenta il software , poiché il degrado della manutenibilità significa che c'è poco tempo per ottimizzare i percorsi critici che contano davvero .
Questo è il fenomeno finale che ho osservato, in cui gli sviluppatori che cercavano di risparmiare qualche soldo sull'acquisto di una singola lattina di soda, mai più per essere comprati, o peggio ancora, una casa, stavano perdendo tutto il loro tempo a pizzicare penny (o peggio, penny immaginari da non riuscendo a capire il loro compilatore o l'architettura dell'hardware) quando miliardi di dollari furono spesi inutilmente altrove.
Il tempo è molto limitato, quindi cercare di ottimizzare gli assoluti senza avere le informazioni contestuali appropriate spesso ci priva dell'opportunità di ottimizzare i luoghi che contano davvero, e quindi, in termini di effetto pratico, direi che "l'ottimizzazione prematura rende il software molto più lento. "
Il problema è che ci sono tipi di sviluppatori che prenderanno ciò che ho scritto sopra sugli oggetti e cercheranno di stabilire uno standard di codifica che vieti la programmazione orientata agli oggetti o qualcosa di folle di quel tipo. Un'ottimizzazione efficace è un'efficace definizione delle priorità ed è assolutamente inutile se stiamo affogando in un mare di problemi di manutenzione.