Ottimizzazione del codice: il modo ottimale per ottimizzare

Pubblicato: 2022-03-11

L'ottimizzazione delle prestazioni è una delle maggiori minacce al codice.

Potresti pensare, non un'altra di quelle persone . Capisco. L'ottimizzazione di qualsiasi tipo dovrebbe chiaramente essere una buona cosa, a giudicare dalla sua etimologia, quindi naturalmente, vuoi essere bravo.

Non solo per distinguerti dalla massa come sviluppatore migliore. Non solo per evitare di essere "Dan" su The Daily WTF , ma perché ritieni che l'ottimizzazione del codice sia la cosa giusta da fare. Sei orgoglioso del tuo lavoro.

L'hardware del computer diventa sempre più veloce e il software è più facile da realizzare, ma qualunque cosa semplice tu voglia solo essere in grado di fare, Dammit richiede sempre più tempo del precedente. Scuoti la testa di fronte a questo fenomeno (per inciso, noto come legge di Wirth) e decidi di invertire quella tendenza.

È nobile da parte tua, ma fermati.

Semplicemente fermati!

Sei in grave pericolo di vanificare i tuoi obiettivi, non importa quanto sei esperto nella programmazione.

Come mai? Torniamo indietro.

Prima di tutto, cos'è l'ottimizzazione del codice?

Spesso, quando lo definiamo, assumiamo di volere che il codice funzioni meglio. Diciamo che l'ottimizzazione del codice consiste nella scrittura o nella riscrittura del codice in modo che un programma utilizzi meno memoria o spazio su disco possibile, riduca al minimo il tempo della CPU o la larghezza di banda della rete o utilizzi al meglio i core aggiuntivi.

In pratica, a volte utilizziamo per impostazione predefinita un'altra definizione: scrivere meno codice.

Ma il codice preventivamente tosto che stai scrivendo con quell'obiettivo è ancora più probabile che diventi una spina nel fianco di qualcuno. Di chi? La prossima persona sfortunata che deve comprendere il tuo codice, che potresti anche essere te stesso. E qualcuno intelligente e capace, come te, può evitare l'auto-sabotaggio: mantieni i tuoi fini nobili ma rivaluta i tuoi mezzi, nonostante sembrino indiscutibilmente intuitivi.

Code Golfing: +197%, Performance: -398%, Semplicità: -9999%

Quindi l'ottimizzazione del codice è un termine un po' vago. Questo prima ancora di considerare alcuni degli altri modi in cui è possibile ottimizzare il codice, che analizzeremo di seguito.

Iniziamo ascoltando i consigli dei saggi mentre esploriamo insieme le famose regole di ottimizzazione del codice di Jackson:

  1. Non farlo.
  2. (Solo per esperti!) Non farlo ancora .

1. Non farlo: canalizzare il perfezionismo

Inizierò con un esempio piuttosto imbarazzante e estremo di un tempo in cui, molto tempo fa, mi stavo solo bagnando i piedi nel meraviglioso mondo di SQL, "mangia la tua torta e mangiala". Il problema è che poi ho calpestato la torta e non volevo più mangiarla perché era bagnata e cominciava a puzzare di piedi.

Mi stavo solo bagnando i piedi nel meraviglioso mondo di SQL. Il problema è che poi ho calpestato la torta...

Attesa. Fammi uscire da questa macchina rottamata da una metafora che ho appena creato e spiegata.

Stavo facendo ricerca e sviluppo per un'app intranet, che speravo sarebbe diventata un giorno un sistema di gestione completamente integrato per la piccola impresa in cui lavoravo. Traccerebbe tutto per loro e, a differenza del loro sistema allora attuale, non perderebbe mai i loro dati, perché sarebbe supportato da un RDBMS, non dal file piatto traballante coltivato in casa che altri sviluppatori avevano usato. Volevo progettare tutto il più intelligente possibile fin dall'inizio perché avevo una tabula rasa. Le idee per questo sistema stavano esplodendo come fuochi d'artificio nella mia mente e ho iniziato a progettare i contatti tabella e le loro numerose variazioni contestuali per un CRM, moduli di contabilità, inventario, acquisti, CMS e gestione dei progetti, che presto sarei stato un dogfood.

Che tutto si è fermato, dal punto di vista dello sviluppo e delle prestazioni, a causa di... hai indovinato, l'ottimizzazione.

Ho visto che gli oggetti (rappresentati come righe di tabella) potrebbero avere molte relazioni diverse tra loro nel mondo reale e che potremmo trarre vantaggio dal monitoraggio di queste relazioni: avremmo conservato più informazioni e alla fine potremmo automatizzare l'analisi aziendale ovunque. Considerando questo come un problema di ingegneria, ho fatto qualcosa che sembrava un'ottimizzazione della flessibilità del sistema.

A questo punto, è importante prestare attenzione al tuo viso, perché non sarò ritenuto responsabile se il tuo palmo ti fa male. Pronto? Ho creato due tabelle: relationship e una a cui aveva un riferimento di chiave esterna, relationship_type . relationship potrebbe fare riferimento a due righe qualsiasi in qualsiasi punto dell'intero database e descrivere la natura della relazione tra di esse.

Tabelle del database: dipendente, azienda, relazione, tipo_relazione

Oddio. Avevo appena ottimizzato quella flessibilità così tanto .

Troppo, in effetti. Ora avevo un nuovo problema: un dato relationship_type non avrebbe naturalmente senso tra ogni data combinazione di righe. Sebbene possa avere senso che una person abbia un rapporto di employed by con company , ciò non potrebbe mai essere semanticamente equivalente al rapporto tra, diciamo, due document s.

Ok nessun problema. Aggiungeremo semplicemente due colonne a relationship_type , specificando a quali tabelle potrebbe essere applicata questa relazione. (Punti bonus qui se indovini che ho pensato di normalizzarlo spostando quelle due colonne in una nuova tabella che fa riferimento a relationship_type.id , in modo che le relazioni che potrebbero applicarsi semanticamente a più di una coppia di tabelle non avrebbero i nomi delle tabelle duplicati. Dopotutto, se dovessi cambiare il nome di una tabella e dimenticassi di aggiornarlo in tutte le righe applicabili, potrebbe creare un bug! In retrospettiva, almeno i bug avrebbero fornito cibo per i ragni che abitano il mio cranio.)

Tabelle del database: tipo_relazione e applicabile_a, e i dati contorti delle due colonne di tipo_relazione rappresentate da frecce

Per fortuna, ho perso i sensi in una tempesta di indizi prima di viaggiare troppo lungo questo sentiero. Quando mi sono svegliato, mi sono reso conto di essere riuscito, più o meno, a re-implementare le tabelle interne relative alla chiave esterna dell'RDBMS sopra se stesso. Normalmente mi godo i momenti che finiscono con me che faccio la spavalda proclamazione che "sono così meta", ma questo, sfortunatamente, non era uno di questi. Dimentica la mancata scalabilità : l'orrendo rigonfiamento di questo design ha reso il back-end della mia app ancora semplice, il cui DB era a malapena popolato con dati di test, quasi inutilizzabile.

Usa le chiavi esterne, Luke!

Torniamo indietro per un secondo e diamo un'occhiata a due delle tante metriche in gioco qui. Uno è la flessibilità, che era stato il mio obiettivo dichiarato. In questo caso, la mia ottimizzazione, essendo di natura architettonica, non era nemmeno prematura:

Passaggi di ottimizzazione del codice: l'architettura è la prima parte di un programma da ottimizzare

(Ci occuperemo di più nel mio articolo pubblicato di recente, Come evitare la maledizione dell'ottimizzazione prematura.) Tuttavia, la mia soluzione ha fallito in modo spettacolare essendo troppo flessibile. L'altra metrica, la scalabilità, era quella che non stavo nemmeno considerando, ma sono riuscita a distruggere almeno in modo altrettanto spettacolare con danni collaterali.

Esatto, "Oh".

Doppio facepalm, per quando un facepalm non lo taglia

Questa è stata una potente lezione per me su come l'ottimizzazione può andare completamente storta. Il mio perfezionismo è completamente imploso: la mia intelligenza mi ha portato a produrre una delle soluzioni più oggettivamente poco intelligenti che abbia mai realizzato.

Ottimizza le tue abitudini, non il tuo codice

Mentre ti accorgi che tendi a fare il refactoring prima ancora di avere un prototipo funzionante e una suite di test per dimostrarne la correttezza, considera dove altro puoi incanalare questo impulso. Sudoku e Mensa sono fantastici, ma forse sarebbe meglio qualcosa che andrà effettivamente a beneficio del tuo progetto:

  1. Sicurezza
  2. Stabilità di esecuzione
  3. Chiarezza e stile
  4. Efficienza di codifica
  5. Efficacia del test
  6. Profilazione
  7. Il tuo toolkit/DE
  8. ASCIUTTO (non ripetere te stesso)

Ma attenzione: l'ottimizzazione di uno qualsiasi di questi verrà a scapito di altri. Per lo meno, viene a costo del tempo.

Ecco dove è facile vedere quanta arte c'è nel codice di creazione. Per ognuno di questi, posso raccontarti storie su come si pensava che troppo o troppo poco fosse la scelta sbagliata. Anche chi sta pensando qui è una parte importante del contesto.

Ad esempio, per quanto riguarda DRY: in un lavoro che avevo, ho ereditato una base di codice che conteneva almeno l'80% di dichiarazioni ridondanti, perché apparentemente il suo autore non era a conoscenza di come e quando scrivere una funzione. L'altro 20% del codice era auto-simile in modo confuso.

Mi è stato assegnato il compito di aggiungere alcune funzionalità ad esso. Una di queste funzionalità dovrebbe essere ripetuta in tutto il codice da implementare e qualsiasi codice futuro dovrebbe essere accuratamente copiato per utilizzare la nuova funzionalità.

Ovviamente, doveva essere rifattorizzato solo per la mia sanità mentale (valore elevato) e per eventuali futuri sviluppatori. Ma, poiché ero nuovo della base di codice, ho prima scritto dei test in modo da poter essere sicuro che il mio refactoring non introducesse regressioni. In effetti, hanno fatto proprio questo: ho rilevato due bug lungo il percorso che non avrei notato tra tutto l'output gobbledygook prodotto dallo script.

Alla fine pensavo di aver fatto abbastanza bene. Dopo il refactoring, ho impressionato il mio capo per aver implementato quella che era stata considerata una funzionalità difficile con poche semplici righe di codice; inoltre, il codice era nel complesso di un ordine di grandezza più performante. Ma non passò molto tempo dopo che lo stesso capo mi disse che ero stato troppo lento e che il progetto doveva essere già terminato. Traduzione: l'efficienza della codifica era una priorità più alta.

Attenzione: l'ottimizzazione di qualsiasi [aspetto] particolare verrà a scapito degli altri. Per lo meno, viene a costo del tempo.

Penso ancora di aver fatto il corso giusto lì, anche se l'ottimizzazione del codice non è stata apprezzata direttamente dal mio capo in quel momento. Senza il refactoring e i test, penso che ci sarebbe voluto più tempo per essere effettivamente corretto, ovvero concentrarsi sulla velocità di codifica l'avrebbe effettivamente contrastato. (Ehi, questo è il nostro tema!)

Contrasta questo con alcuni lavori che ho fatto su un mio piccolo progetto collaterale. Nel progetto, stavo provando un nuovo motore di modelli e volevo prendere delle buone abitudini fin dall'inizio, anche se provare il nuovo motore di modelli non era l'obiettivo finale del progetto.

Non appena ho notato che alcuni blocchi che avevo aggiunto erano molto simili tra loro, e inoltre ogni blocco richiedeva di fare riferimento alla stessa variabile per tre volte, il campanello DRY mi è suonato in testa e mi sono messo alla ricerca di quello giusto modo di fare quello che stavo cercando di fare con questo motore di modelli.

Si è scoperto, dopo un paio d'ore di debugging infruttuoso, che questo non era attualmente possibile con il motore di template nel modo che immaginavo. Non solo non c'era una soluzione DRY perfetta ; non c'era alcuna soluzione DRY!

Cercando di ottimizzare questo mio valore, ho completamente fatto deragliare la mia efficienza di codifica e la mia felicità, perché questa deviazione è costata al mio progetto i progressi che avrei potuto avere quel giorno.

Anche allora, mi sbagliavo del tutto? A volte vale la pena investire un po', in particolare con un nuovo contesto tecnologico, per conoscere le migliori pratiche prima invece che dopo. Meno codice da riscrivere e cattive abitudini da annullare, giusto?

No, penso che non fosse saggio anche cercare un modo per ridurre la ripetizione nel mio codice, in netto contrasto con il mio atteggiamento nell'aneddoto precedente. Il motivo è che il contesto è tutto: stavo esplorando un nuovo pezzo di tecnologia su un piccolo progetto di gioco, senza accontentarmi di un lungo periodo. Qualche riga in più e ripetizione non avrebbe fatto male a nessuno, ma la perdita di concentrazione ha danneggiato me e il mio progetto.

Aspetta, quindi cercare le migliori pratiche può essere una cattiva abitudine? A volte. Se il mio obiettivo principale fosse l'apprendimento del nuovo motore, o l'apprendimento in generale, allora sarebbe stato tempo ben speso: armeggiare, trovare i limiti, scoprire caratteristiche non correlate e trucchi attraverso la ricerca. Ma avevo dimenticato che questo non era il mio obiettivo principale, e mi è costato.

È un'arte, come ho detto. E lo sviluppo di quell'arte beneficia del promemoria, non farlo . Almeno ti porta a considerare quali valori sono in gioco mentre lavori e quali sono più importanti per te nel tuo contesto.

E quella seconda regola? Quando possiamo effettivamente ottimizzare?

2. Non farlo ancora : qualcuno l'ha già fatto

OK, da te o da qualcun altro, scopri che la tua architettura è già stata impostata, i flussi di dati sono stati pensati e documentati ed è ora di programmare.

Facciamo un ulteriore passo avanti: non codificarlo ancora .

Questo di per sé può odorare di ottimizzazione prematura, ma è un'eccezione importante. Come mai? Per evitare la temuta NIHS, o sindrome del "non inventato qui", supponendo che le tue priorità includano le prestazioni del codice e la riduzione al minimo dei tempi di sviluppo. In caso contrario, se i tuoi obiettivi sono completamente orientati all'apprendimento, puoi saltare questa sezione successiva.

Sebbene sia possibile che le persone reinventino la ruota quadrata per pura arroganza, credo che persone oneste e umili, come me e te, possano commettere questo errore solo non conoscendo tutte le opzioni a nostra disposizione. Conoscere ogni opzione di ogni API e strumento nel tuo stack e tenerli al passo mentre crescono e si evolvono è sicuramente molto lavoro.

Ma dedicare questo tempo è ciò che ti rende un esperto e ti impedisce di essere la miliardesima persona su CodeSOD a essere maledetta e presa in giro per la scia di devastazione lasciata dalla loro affascinante interpretazione dei calcolatori di data e ora o manipolatori di stringhe.

(Un buon contrappunto a questo schema generale è la vecchia API del Calendar Java, ma da allora è stata corretta.)

Controlla la tua libreria standard, controlla l'ecosistema del tuo framework, controlla FOSS che risolve già il tuo problema

È probabile che i concetti con cui hai a che fare abbiano nomi piuttosto standard e noti, quindi una rapida ricerca su Internet ti farà risparmiare un sacco di tempo.

Ad esempio, mi stavo recentemente preparando a fare un'analisi delle strategie dell'IA per un gioco da tavolo. Mi sono svegliato una mattina rendendomi conto che l'analisi che stavo pianificando avrebbe potuto essere eseguita in modo più efficiente di ordini di grandezza se avessi semplicemente usato un certo concetto combinatorio che ricordavo. Non essendo interessato a capire da solo l'algoritmo per questo concetto in questo momento, ero già in vantaggio conoscendo il nome giusto da cercare. Tuttavia, ho scoperto che dopo circa 50 minuti di ricerca e di prova del codice preliminare, non ero riuscito a trasformare lo pseudo-codice a metà che avevo trovato in una corretta implementazione. (Riesci a credere che ci sia un post sul blog in cui l'autore presume un output errato dell'algoritmo, implementa l'algoritmo in modo errato per corrispondere alle ipotesi, i commentatori lo sottolineano e poi anni dopo, non è ancora risolto?) A quel punto, il mio tè mattutino si è attivato e ho cercato [name of concept] [my programming language] . 30 secondi dopo, avevo il codice corretto da GitHub e stavo passando a ciò che volevo effettivamente fare. Il solo fatto di essere specifico e includere la lingua, invece di presumere che avrei dovuto implementarlo da solo, significava tutto.

È ora di progettare la struttura dei dati e implementare il tuo algoritmo

...di nuovo, non giocare a golf di codice. Dai priorità alla correttezza e alla chiarezza nei progetti del mondo reale.

Investimento di tempo: 10 ore, Tempo di esecuzione: +25%, Utilizzo della memoria: +3%, Confusione: 100%

OK, quindi hai guardato e non c'è già nulla che risolva il tuo problema integrato nella tua toolchain o con licenza libera sul web. Tu fai il tuo.

Nessun problema. Il consiglio è semplice, in questo ordine:

  1. Progettalo in modo che sia semplice da spiegare a un programmatore alle prime armi.
  2. Scrivi un test che soddisfi le aspettative prodotte da quel progetto.
  3. Scrivi il tuo codice in modo che un programmatore inesperto possa facilmente ricavarne il design.

Semplice, ma forse difficile da seguire. È qui che entrano in gioco abitudini di codifica e odori di codice, arte, artigianato ed eleganza. C'è ovviamente un aspetto ingegneristico in quello che stai facendo a questo punto, ma ancora una volta, non giocare a golf di codice. Dai priorità alla correttezza e alla chiarezza nei progetti del mondo reale.

Se ti piacciono i video, eccone uno che segue più o meno i passaggi precedenti. Per i video-avversi, riassumerò: è un test di codifica dell'algoritmo in un colloquio di lavoro su Google. L'intervistato prima progetta l'algoritmo in un modo che sia facile da comunicare. Prima di scrivere qualsiasi codice, ci sono esempi dell'output atteso da un progetto funzionante. Quindi il codice segue naturalmente.

Per quanto riguarda i test stessi, so che in alcuni ambienti lo sviluppo basato sui test può essere controverso. Penso che parte del motivo sia che può essere esagerato, perseguito religiosamente fino al punto di sacrificare i tempi di sviluppo. (Di nuovo, sparandoci nei piedi cercando di ottimizzare anche una variabile di troppo dall'inizio.) Anche Kent Beck non porta il TDD a un tale estremo, e ha inventato la programmazione estrema e ha scritto il libro sul TDD. Quindi inizia con qualcosa di semplice per assicurarti che il tuo output sia corretto. Dopotutto, lo faresti comunque manualmente dopo la codifica, giusto? (Mi scuso se sei un programmatore così rockstar che non esegui nemmeno il tuo codice dopo averlo scritto per la prima volta. In tal caso, forse potresti considerare di lasciare un test ai futuri manutentori del tuo codice solo così sai che non lo faranno interrompi la tua fantastica implementazione.) Quindi, invece di fare una differenza visiva manuale, con un test in atto stai già lasciando che il computer lo faccia per te.

Durante il processo piuttosto meccanico di implementazione degli algoritmi e delle strutture dati, evita di effettuare ottimizzazioni riga per riga e non pensare nemmeno di utilizzare un linguaggio esterno personalizzato di livello inferiore (Assembly se stai codificando in C, C se 'codifica in Perl, ecc.) a questo punto. Il motivo è semplice: se il tuo algoritmo viene completamente sostituito e non scoprirai fino a più tardi nel processo se è necessario, allora i tuoi sforzi di ottimizzazione di basso livello non avranno alcun effetto alla fine.

Un esempio di ECMAScript

Nell'eccellente sito di revisione del codice della comunità exercism.io, ho recentemente trovato un esercizio che suggeriva esplicitamente di provare a ottimizzare per la deduplicazione o per chiarezza. Ho ottimizzato per la deduplicazione, solo per mostrare quanto possono diventare ridicole le cose se si porta DRY, una mentalità di codifica altrimenti vantaggiosa, come ho detto sopra, troppo oltre. Ecco come appariva il mio codice:

 const zeroPhrase = "No more"; const wallPhrase = " on the wall"; const standardizeNumber = number => { if (number === 0) { return zeroPhrase; } return '' + number; } const bottlePhrase = number => { const possibleS = (number === 1) ? '' : 's'; return standardizeNumber(number) + " bottle" + possibleS + " of beer"; } export default class Beer { static verse(number) { const nextNumber = (number === 0) ? 99 : (number - 1); const thisBottlePhrase = bottlePhrase(number); const nextBottlePhrase = bottlePhrase(nextNumber); let phrase = thisBottlePhrase + wallPhrase + ", " + thisBottlePhrase.toLowerCase() + ".\n"; if (number === 0) { phrase += "Go to the store and buy some more"; } else { const bottleReference = (number === 1) ? "it" : "one"; phrase += "Take " + bottleReference + " down and pass it around"; } return phrase + ", " + nextBottlePhrase.toLowerCase() + wallPhrase + ".\n"; } static sing(start = 99, end = 0) { return Array.from(Array(start - end + 1).keys()).map(offset => { return this.verse(start - offset); }).join('\n'); } }

Quasi nessuna duplicazione di stringhe! Scrivendolo in questo modo, ho implementato manualmente una forma di compressione del testo per la canzone della birra (ma solo per la canzone della birra). Qual è stato il vantaggio, esattamente? Bene, diciamo che vuoi cantare di bere birra dalle lattine invece che dalle bottiglie. Potrei farlo cambiando una singola istanza di bottle in can .

Carino!

…Giusto?

No, perché poi si rompono tutti i test. OK, è facile da risolvere: faremo solo una ricerca e sostituiremo la bottle nelle specifiche del test dell'unità. E questo è esattamente tanto facile da fare quanto farlo sul codice stesso in primo luogo e comporta gli stessi rischi di rompere le cose involontariamente.

Nel frattempo, le mie variabili prenderanno stranamente il nome in seguito, con cose come bottlePhrase che non hanno nulla a che fare con le bottiglie . L'unico modo per evitarlo è aver previsto esattamente il tipo di modifica che sarebbe stata apportata e utilizzato un termine più generico come vessel o container al posto di bottle nei nomi delle mie variabili.

La saggezza dell'essere a prova di futuro in questo modo è piuttosto discutibile. Quali sono le probabilità che vorrai cambiare qualcosa? E se lo fai, ciò che cambi funzionerà in modo così conveniente? Nell'esempio bottlePhrase , cosa succede se si desidera localizzare in una lingua che ha più di due forme plurali? Esatto, tempo di refactoring e il codice potrebbe apparire anche peggio in seguito.

Ma quando le tue esigenze cambiano e non stai solo cercando di anticiparle, allora forse è il momento di riformulare. O forse puoi ancora rimandare: quanti tipi di navi o localizzazioni aggiungerai, realisticamente? Ad ogni modo, quando hai bisogno di bilanciare la tua deduplicazione con chiarezza, vale la pena guardare questa dimostrazione di Katrina Owen.

Tornando al mio brutto esempio: inutile dire che i vantaggi della deduplica non vengono nemmeno realizzati qui molto. Intanto quanto è costato?

Oltre a richiedere più tempo per scrivere in primo luogo, ora è un po' meno banale da leggere, eseguire il debug e mantenere. Immagina il livello di leggibilità con una moderata quantità di duplicazione consentita. Ad esempio, facendo esplicitare ciascuna delle quattro variazioni di versi.

Ma non abbiamo ancora ottimizzato!

Ora che il tuo algoritmo è stato implementato e hai dimostrato che il suo output è corretto, congratulazioni! Hai una linea di base!

Infine, è il momento di... ottimizzare, giusto? No, ancora Non farlo ancora . È ora di prendere la tua linea di base e fare un bel benchmark . Imposta una soglia per le tue aspettative intorno a questo e inseriscila nella tua suite di test. Quindi se qualcosa all'improvviso rende questo codice più lento, anche se funziona ancora, lo saprai prima che esca dalla porta.

Aspetta ancora l'ottimizzazione, finché non avrai implementato un intero pezzo dell'esperienza utente pertinente. Fino a quel momento, potresti prendere di mira una parte del codice completamente diversa da quella necessaria.

Vai a finire la tua app (o componente), se non l'hai già fatto, impostando tutte le linee di base del benchmark algoritmico mentre procedi.

Una volta fatto, questo è un ottimo momento per creare e confrontare test end-to-end che coprano gli scenari di utilizzo del mondo reale più comuni del tuo sistema.

Forse scoprirai che va tutto bene.

O forse hai stabilito che, nel suo contesto di vita reale, qualcosa è troppo lento o richiede troppa memoria.

OK, ora puoi ottimizzare

C'è solo un modo per essere obiettivi al riguardo. È ora di analizzare i grafici di fiamma e altri strumenti di profilazione. Gli ingegneri esperti possono o meno indovinare meglio più spesso dei principianti, ma non è questo il punto: l'unico modo per saperlo con certezza è creare un profilo. Questa è sempre la prima cosa da fare nel processo di ottimizzazione del codice per le prestazioni.

È possibile eseguire il profilo durante un determinato test end-to-end per ottenere ciò che avrà davvero l'impatto maggiore. (E più tardi, dopo la distribuzione, il monitoraggio dei modelli di utilizzo è un ottimo modo per rimanere al passo con gli aspetti del tuo sistema più rilevanti da misurare in futuro.)

Nota che non stai cercando di utilizzare il profiler fino in fondo: stai cercando più un profilo a livello di funzione che un profilo a livello di istruzioni, in generale, perché il tuo obiettivo a questo punto è solo scoprire quale algoritmo è il collo di bottiglia .

Ora che hai utilizzato la profilazione per identificare il collo di bottiglia del tuo sistema, ora puoi effettivamente tentare di ottimizzare, fiducioso che valga la pena fare la tua ottimizzazione. Puoi anche dimostrare quanto sia stato efficace (o inefficace) il tuo tentativo, grazie a quei benchmark di base che hai fatto lungo il percorso.

Tecniche generali

Innanzitutto, ricorda di rimanere di alto livello il più a lungo possibile:

A livello dell'intero algoritmo, una tecnica è la riduzione della forza. Nel caso di ridurre i cicli alle formule, tuttavia, fai attenzione a lasciare commenti. Non tutti conoscono o ricordano ogni formula combinatoria. Inoltre, fai attenzione all'uso della matematica: a volte ciò che pensi possa essere una riduzione della forza, alla fine, non lo è. Ad esempio, supponiamo che x * (y + z) abbia un chiaro significato algoritmico. Se il tuo cervello è stato addestrato a un certo punto, per qualsiasi motivo, a separare automaticamente termini simili, potresti essere tentato di riscriverlo come x * y + x * z . Per prima cosa, questo pone una barriera tra il lettore e il chiaro significato algoritmico che c'era stato. (Peggio ancora, ora è in realtà meno efficiente a causa dell'operazione di moltiplicazione aggiuntiva richiesta. È come se lo srotolamento del ciclo gli avesse appena incrostato i pantaloni.) In ogni caso, una breve nota sulle tue intenzioni farebbe molto e potrebbe persino aiutarti a vedere le tue proprio errore prima di commetterlo.

Sia che tu stia utilizzando una formula o semplicemente sostituendo un algoritmo basato su loop con un altro algoritmo basato su loop, sei pronto per misurare la differenza.

Ma forse puoi ottenere prestazioni migliori semplicemente modificando la struttura dei dati. Informati sulla differenza di prestazioni tra le varie operazioni che devi fare sulla struttura che stai utilizzando e su eventuali alternative. Forse un hash sembra un po 'più disordinato per funzionare nel tuo contesto, ma vale la pena il tempo di ricerca superiore rispetto a un array? Questi sono i tipi di compromessi che spetta a te decidere.

Potresti notare che questo si riduce al sapere quali algoritmi vengono eseguiti per tuo conto quando chiami una funzione di convenienza. Quindi è davvero la stessa cosa della riduzione della forza, alla fine. E sapere cosa stanno facendo le librerie del tuo fornitore dietro le quinte è fondamentale non solo per le prestazioni ma anche per evitare bug involontari.

Micro-ottimizzazioni

OK, la funzionalità del tuo sistema è terminata, ma dal punto di vista dell'esperienza utente, le prestazioni potrebbero essere ulteriormente ottimizzate. Supponendo che tu abbia fatto tutto il possibile più in alto, è tempo di considerare le ottimizzazioni che abbiamo evitato per tutto il tempo fino ad ora. Considera, perché questo livello di ottimizzazione è ancora un compromesso con chiarezza e manutenibilità. Ma hai deciso che è il momento, quindi vai avanti con la profilazione a livello di istruzione, ora che sei nel contesto dell'intero sistema, dove conta davvero.

Proprio come con le librerie che usi, sono state dedicate innumerevoli ore di progettazione a tuo vantaggio a livello di compilatore o interprete. (Dopo tutto, l'ottimizzazione del compilatore e la generazione di codice sono argomenti enormi tutti loro). Questo è vero anche a livello di processore. Cercare di ottimizzare il codice senza essere consapevoli di ciò che sta accadendo ai livelli più bassi è come pensare che avere la trazione integrale implichi che anche il tuo veicolo possa fermarsi più facilmente.

È difficile dare buoni consigli generici oltre a questo perché dipende davvero dal tuo stack tecnologico e da ciò a cui punta il tuo profiler. Ma, poiché stai misurando, sei già in una posizione eccellente per chiedere aiuto, se le soluzioni non ti si presentano organicamente e intuitivamente dal contesto del problema. (Anche il sonno e il tempo speso a pensare a qualcos'altro possono aiutare.)

A questo punto, a seconda del contesto e dei requisiti di ridimensionamento, Jeff Atwood suggerirebbe probabilmente di aggiungere semplicemente l'hardware, che può essere più economico del tempo dello sviluppatore.

Forse non segui quella strada. In tal caso, può essere utile esplorare varie categorie di tecniche di ottimizzazione del codice:

  • Memorizzazione nella cache
  • Bit hack e quelli specifici per ambienti a 64 bit
  • Ottimizzazione del ciclo
  • Ottimizzazione della gerarchia della memoria

Più specificamente:

  • Suggerimenti per l'ottimizzazione del codice in C e C++
  • Suggerimenti per l'ottimizzazione del codice in Java
  • Ottimizzazione dell'utilizzo della CPU in .NET
  • Memorizzazione nella cache della web farm ASP.NET
  • Ottimizzazione del database SQL o ottimizzazione di Microsoft SQL Server in particolare
  • Ridimensionare il gioco di Scala! struttura
  • Ottimizzazione avanzata delle prestazioni di WordPress
  • Ottimizzazione del codice con prototipi JavaScript e catene di ambiti
  • Ottimizzazione delle prestazioni di React
  • Efficienza dell'animazione iOS
  • Suggerimenti per le prestazioni di Android

In ogni caso, ho altre cose da non fare per te:

Non riutilizzare una variabile per più scopi distinti. In termini di manutenibilità, è come guidare un'auto senza olio. Solo nelle situazioni embedded più estreme questo ha mai avuto senso, e anche in quei casi, direi che non ha più senso. Questo è il compito del compilatore di organizzare. Fallo da solo, quindi sposta una riga di codice e hai introdotto un bug. L'illusione di salvare la memoria vale per te?

Non utilizzare macro e funzioni inline senza sapere perché. Sì, l'overhead della chiamata di funzione è un costo. Ma evitarlo spesso rende più difficile il debug del codice e talvolta lo rende effettivamente più lento. Usare questa tecnica ovunque solo perché è una buona idea di tanto in tanto è un esempio di martello d'oro.

Non srotolare manualmente i loop. Ancora una volta, questa forma di ottimizzazione del ciclo è quasi sempre qualcosa di meglio ottimizzato da un processo automatizzato come la compilazione, non sacrificando la leggibilità del codice.

L'ironia negli ultimi due esempi di ottimizzazione del codice è che possono effettivamente essere anti-prestazioni. Ovviamente, dal momento che stai facendo benchmark, puoi provarlo o smentirlo per il tuo codice particolare. Ma anche se vedi un miglioramento delle prestazioni, torna al lato artistico e vedi se il guadagno vale la perdita di leggibilità e manutenibilità.

È tuo: ottimizzazione ottimizzata in modo ottimale

Il tentativo di ottimizzazione delle prestazioni può essere vantaggioso. Il più delle volte, tuttavia, è fatto molto prematuramente, porta con sé una litania di cattivi effetti collaterali e, ironicamente, porta a prestazioni peggiori. Spero che tu abbia ottenuto un maggiore apprezzamento per l'arte e la scienza dell'ottimizzazione e, soprattutto, il suo contesto appropriato.

Sono felice se questo ci aiuta a eliminare l'idea di scrivere un codice perfetto dall'inizio e a scrivere invece il codice corretto. Dobbiamo ricordarci di ottimizzare dall'alto verso il basso, dimostrare dove si trovano i colli di bottiglia e misurare prima e dopo averli sistemati. Questa è la strategia ottimale e ottimale per ottimizzare l'ottimizzazione. Buona fortuna.