Va bene sostituire il codice ottimizzato con codice leggibile?


78

A volte ti imbatti in una situazione in cui devi estendere / migliorare un po 'di codice esistente. Vedi che il vecchio codice è molto snello, ma è anche difficile da estendere e richiede tempo per la lettura.

È una buona idea sostituirlo con un codice moderno?

Qualche tempo fa mi è piaciuto l'approccio snello, ma ora mi sembra che sia meglio sacrificare molte ottimizzazioni a favore di astrazioni più elevate, interfacce migliori e codice più leggibile ed estensibile.

Sembra che anche i compilatori stiano migliorando, quindi cose come quelle che struct abc = {}si trasformano in silenzio in memsets, shared_ptrs stanno praticamente producendo lo stesso codice di twiddling di puntatori non elaborati, i template funzionano molto bene perché producono un codice super lean e così via.

Tuttavia, a volte vedi array basati su stack e vecchie funzioni C con una logica oscura, e di solito non sono sul percorso critico.

È una buona idea cambiare tale codice se si deve toccare una piccola parte di esso in entrambi i modi?


20
La leggibilità e le ottimizzazioni non si contrappongono il più delle volte.
deadalnix,

23
La leggibilità può migliorare con alcuni commenti?
YetAnotherUser

17
È preoccupante che l'identificazione OOP sia considerata "codice moderno"
James,

7
come la filosofia slackware: se non è rotto non risolverlo, almeno hai un ottimo motivo per farlo
osdamv,

5
Per codice ottimizzato, intendi il vero codice ottimizzato o il cosiddetto codice ottimizzato?
dan04,

Risposte:


115

Dove?

  • In una home page di un sito Web su scala Google, non è accettabile. Mantieni le cose il più velocemente possibile.

  • In una parte di un'applicazione che viene utilizzata da una persona una volta all'anno, è perfettamente accettabile sacrificare le prestazioni per ottenere la leggibilità del codice.

In generale, quali sono i requisiti non funzionali per la parte del codice su cui stai lavorando? Se un'azione deve essere eseguita sotto i 900 ms. in un dato contesto (macchina, carico, ecc.) l'80% delle volte, e in realtà, esegue meno di 200 ms. Il 100% delle volte, sicuramente, rende il codice più leggibile anche se potrebbe influire leggermente sulle prestazioni. Se d'altra parte la stessa azione non è mai stata eseguita in meno di dieci secondi, beh, dovresti piuttosto provare a vedere cosa c'è che non va nella performance (o nel requisito in primo luogo).

Inoltre, in che modo il miglioramento della leggibilità diminuirà le prestazioni? Spesso gli sviluppatori stanno adattando il comportamento vicino all'ottimizzazione precoce: hanno paura di aumentare la leggibilità, credendo che distruggerà drasticamente le prestazioni, mentre il codice più leggibile impiegherà qualche microsecondo in più per fare la stessa azione.


47
+1! Se non hai numeri, ottieni alcuni numeri. Se non hai tempo per ottenere i numeri, non hai tempo per cambiarli.
Tacroy,

49
Spesso, gli sviluppatori "ottimizzano" in base al mito e al malinteso, per esempio, supponendo che "C" sia più veloce di "C ++" ed evitino le caratteristiche del C ++ da una sensazione generale che le cose siano più veloci senza numeri per eseguirne il backup. Mi ricorda uno sviluppatore C che ho seguito e che pensava gotofosse più veloce che per i loop. Ironia della sorte, l'ottimizzatore ha fatto meglio con i loop, quindi ha reso il codice più lento e più difficile da leggere.
Gort the Robot,

6
Invece di aggiungere un'altra risposta, ho fatto +1 su questa risposta. Se la comprensione dei frammenti di codice è così importante, commentali bene. Ho lavorato in un ambiente C / C ++ / Assembly con un codice legacy vecchio di dieci anni con dozzine di collaboratori. Se il codice funziona, lascialo in pace e torna al lavoro.
Chris K,

Ecco perché tendo a scrivere solo codice leggibile. Le prestazioni possono essere raggiunte tagliando i pochi punti caldi.
Luca,

36

Di solito no .

La modifica del codice può causare problemi a catena imprevisti in altre parti del sistema (che a volte possono passare inosservati fino a molto più tardi in un progetto se non si dispone di unità solide e test del fumo in atto). Di solito vado per mentalità "se non è rotto, non aggiustarlo".

L'eccezione a questa regola è se stai implementando una nuova funzionalità che tocca questo codice. Se, a quel punto, non ha senso e il refactoring deve davvero aver luogo, allora procedi fino a quando il tempo di refactoring (e test e buffer sufficienti per affrontare i problemi di knock-on) è tutto considerato nelle stime.

Naturalmente, profilo, profilo, profilo , soprattutto se si tratta di un'area del percorso critico.


2
Sì, ma supponi che l'ottimizzazione fosse necessaria. Non sappiamo sempre se lo fosse, e probabilmente vogliamo prima determinarlo.
Hayylem,

2
@haylem: No, suppongo che il codice funziona così com'è. Suppongo anche che il refactoring del codice causi invariabilmente problemi a catena in altre parti del sistema (a meno che tu non abbia a che fare con una banale porzione di codice che non ha dipendenze esterne).
Demian Brecht,

C'è della verità in questa risposta, e ironicamente questo è perché i problemi a catena sono raramente documentati, compresi, comunicati o addirittura prestati attenzione dagli sviluppatori. Se gli sviluppatori hanno una comprensione più approfondita dei problemi che sono accaduti in passato, sapranno cosa misurare e saranno più sicuri nel fare modifiche al codice.
rwong

29

In breve: dipende

  • Avrai davvero bisogno o utilizzerai la tua versione refactored / avanzata?

    • C'è un guadagno concreto, immediato o a lungo termine?
    • Questo guadagno è solo per manutenibilità o davvero architettonico?
  • Deve davvero essere ottimizzato?

    • Perché?
    • A quale target target devi puntare?

Nei dettagli

Avrai bisogno delle cose pulite e lucide?

Ci sono cose da essere cauti qui, e devi identificare il limite tra ciò che è reale, il guadagno misurabile e qual è solo la tua preferenza personale e la potenziale cattiva abitudine di toccare il codice che non dovrebbe essere.

Più specificamente, sappi questo:

C'è qualcosa come Over-Engineering

È un anti-pattern e presenta problemi integrati:

  • esso può essere più estensibile , ma non può essere più facile da estendere,
  • essa non può essere più semplice da capire ,
  • ultimo, ma sicuramente non ultimo qui: potresti rallentare l'intero codice.

Alcuni potrebbero anche menzionare il principio KISS come riferimento, ma qui è controintuitivo: il modo ottimizzato è il modo semplice o il modo pulito e architettonico? La risposta non è necessariamente assoluta, come spiegato nel resto di seguito.

Non ne avrai bisogno

Il principio YAGNI non è completamente ortogonale all'altra questione, ma aiuta a porsi la domanda: ne avrai bisogno?

L'architettura più complessa rappresenta davvero un vantaggio per te, oltre a dare l'impressione di essere più mantenibile?

Se non è rotto, non risolverlo

Scrivi questo su un grande poster e appendilo accanto al tuo schermo o nell'area della cucina al lavoro o nella sala riunioni degli sviluppatori. Ovviamente ci sono molti altri mantra che vale la pena ripetere, ma questo particolare è importante ogni volta che provi a fare "lavori di manutenzione" e senti l'impulso di "migliorarlo".

È naturale per noi voler "migliorare" il codice o anche semplicemente toccarlo, anche inconsciamente, mentre lo leggiamo per cercare di capirlo. È una buona cosa, poiché significa che siamo supponenti e cerchiamo di ottenere una comprensione più profonda degli interni, ma è anche legato al nostro livello di abilità, alla nostra conoscenza (come si decide cosa è meglio o no? Bene, vedere le sezioni seguenti ...) e tutte le ipotesi che facciamo su ciò che pensiamo di conoscere il software ...:

  • lo fa davvero,
  • in realtà deve fare,
  • alla fine dovrà fare,
  • e quanto bene lo fa.

Deve davvero essere ottimizzato?

Detto questo, perché è stato "ottimizzato" in primo luogo? Dicono che l'ottimizzazione prematura è la radice di tutto il male, e se vedi un codice non documentato e apparentemente ottimizzato, di solito potresti presumere che probabilmente non ha seguito le Regole di Ottimizzazione non ha avuto un grande bisogno dello sforzo di ottimizzazione e che era solo il la consueta arroganza dello sviluppatore sta entrando in gioco. Ancora una volta, forse è solo il tuo a parlare ora.

In tal caso, entro quali limiti diventa accettabile? Se ce n'è bisogno, questo limite esiste e ti dà spazio per migliorare le cose, o una linea dura per decidere di lasciarlo andare.

Inoltre, fai attenzione alle caratteristiche invisibili. È probabile che la tua versione "estensibile" di questo codice ti occuperà anche più memoria in fase di esecuzione e presenterà anche un footprint di memoria statica più grande per l'eseguibile. Le funzionalità OO brillanti comportano costi non intuitivi come questi e potrebbero essere importanti per il tuo programma e l'ambiente su cui dovrebbe funzionare.

Misura, misura, misura

Come la gente di Google ora, si tratta di dati! Se è possibile eseguirne il backup con i dati, è necessario.

C'è questa storia non così vecchia che per ogni $ 1 speso in sviluppo sarà seguito da almeno $ 1 in test e almeno $ 1 in supporto (ma in realtà, è molto di più).

Il cambiamento ha un impatto su molte cose:

  • potresti aver bisogno di produrre una nuova build;
  • dovresti scrivere nuovi test unitari (sicuramente se non ce ne fossero, e la tua architettura più estensibile probabilmente lascia spazio a più, dato che hai più superficie per i bug);
  • dovresti scrivere nuovi test delle prestazioni (per assicurarti che questo rimanga stabile in futuro e per vedere dove si trovano i colli di bottiglia), e questi sono difficili da fare ;
  • dovrai documentarlo (e più estensibile significa più spazio per i dettagli);
  • tu (o qualcun altro) dovrai ripeterlo ampiamente nel QA;
  • il codice non è (quasi) mai privo di bug e dovrai supportarlo.

Quindi non è solo il consumo di risorse hardware (velocità di esecuzione o footprint di memoria) che devi misurare qui, è anche il consumo di risorse del team . Entrambi devono essere previsti per definire un obiettivo target, da misurare, spiegare e adattare in base allo sviluppo.

E per il tuo manager, questo significa adattarlo al piano di sviluppo attuale, quindi comunicalo e non entrare in una furiosa codifica cow-boy / sottomarino / black-ops.


In generale...

Si ma...

Non fraintendetemi, in generale, sarei a favore di fare il motivo per cui mi suggerite, e spesso lo sostengo. Ma devi essere consapevole del costo a lungo termine.

In un mondo perfetto, è la soluzione giusta:

  • l'hardware del computer migliora nel tempo,
  • i compilatori e le piattaforme di runtime migliorano nel tempo,
  • ottieni un codice quasi perfetto, pulito, gestibile e leggibile.

In pratica:

  • potresti peggiorare le cose

    Hai bisogno di più occhi per guardarlo, e più lo complessi, più occhi hai bisogno.

  • non puoi predire il futuro

    Non puoi sapere con assoluta certezza se ne avrai mai bisogno e nemmeno se le "estensioni" di cui avrai bisogno sarebbero state più facili e veloci da implementare nel vecchio modulo, e se loro stessi avrebbero bisogno di essere super-ottimizzati .

  • rappresenta, dal punto di vista della direzione, un costo enorme senza guadagno diretto.

Renderlo parte del processo

Indichi qui che si tratta di una modifica piuttosto piccola e che hai in mente alcuni problemi specifici. Direi che di solito va bene in questo caso, ma la maggior parte di noi ha anche storie personali di piccoli cambiamenti, modifiche quasi chirurgiche, che alla fine si sono trasformate in incubo di manutenzione e scadenze quasi mancate o esplose perché Joe Programmer non ne ha visto uno dei motivi alla base del codice e toccato qualcosa che non avrebbe dovuto essere.

Se si dispone di un processo per gestire tali decisioni, si toglie loro il vantaggio personale:

  • Se testerai le cose correttamente, saprai più rapidamente se le cose sono rotte,
  • Se li misuri, saprai se sono migliorati,
  • Se lo rivedi, saprai se butta via le persone.

La copertura dei test, la profilazione e la raccolta dei dati sono difficili

Ma, naturalmente, il codice di test e le metriche potrebbero soffrire degli stessi problemi che stai cercando di evitare per il tuo codice reale: testi le cose giuste e sono le cose giuste per il futuro e misuri le misure giuste? cose?

Tuttavia, in generale, più si esegue il test (fino a un certo limite) e si misura, più dati si raccolgono e più si è sicuri. Cattivo tempo di analogia: pensalo come guidare (o la vita in generale): puoi essere il miglior pilota del mondo, se l'auto si rompe su di te o qualcuno ha deciso di suicidarsi guidando nella tua auto oggi stesso, il tuo le abilità potrebbero non essere sufficienti. Ci sono entrambe le cose ambientali che possono colpirti e anche gli errori umani contano.

Le revisioni del codice sono i test di corridoio del team di sviluppo

E penso che l'ultima parte sia la chiave qui: fare recensioni di codice. Non conoscerai il valore dei tuoi miglioramenti se li rendi soli. Le revisioni del codice sono i nostri "test di corridoio": segui la versione di Raymond della Legge di Linus sia per rilevare bug e rilevare sovrastampa e altri anti-schemi, sia per assicurarti che il codice sia in linea con le capacità del tuo team. Non ha senso avere il codice "migliore" se nessun altro, se non in grado di comprenderlo e mantenerlo, vale sia per le ottimizzazioni criptiche che per i progetti architettonici profondi a 6 strati.

Come parole di chiusura, ricorda:

Tutti sanno che il debug è due volte più difficile della scrittura di un programma in primo luogo. Quindi se sei intelligente quanto puoi esserlo quando lo scrivi, come eseguirai mai il debug? - Brian Kernighan


"Se non è rotto, non aggiustarlo" va contro il refactoring. Non importa se qualcosa funziona, se non è realizzabile, deve essere modificato.
Miyamoto Akira,

@MiyamotoAkira: è una cosa a due velocità. Se non è rotto ma accettabile e meno probabile che venga visualizzato il supporto, potrebbe essere accettabile lasciarlo da solo anziché introdurre potenziali nuovi bug o dedicare tempo allo sviluppo. Si tratta solo di valutare i benefici, sia a breve che a lungo termine, del refactoring. Non c'è una risposta chiara, richiede una valutazione.
Hayylem,

concordato. Suppongo che non mi piaccia la frase (e la filosofia dietro di essa) perché vedo il refactoring come opzione predefinita, e solo se sembra che ci vorrà troppo tempo o troppo difficile, allora / dovrebbe essere deciso di non farlo procedi. Intendiamoci, sono stato bruciato da persone che non cambiano le cose, che nonostante funzionino, erano chiaramente la soluzione sbagliata non appena dovevi mantenerle o estenderle.
Miyamoto Akira,

@MiyamotoAkira: frasi brevi e dichiarazioni di opinioni non possono esprimere molto. Sono pensati per essere in faccia e per essere sviluppati sul lato, immagino. Sono molto interessato a rivedere e toccare il codice il più spesso possibile, anche se spesso senza una grande rete di sicurezza o senza molte ragioni. Se è sporco, lo pulisci. Allo stesso modo, anche io sono stato bruciato alcune volte. E verrà comunque bruciato. Finché non sono quelli di 3 ° grado, non mi dispiace così tanto, finora sono sempre state ustioni a breve termine per guadagni a lungo termine.
Hayylem,

8

In generale, dovresti prima concentrarti sulla leggibilità e sulle prestazioni molto più tardi. Il più delle volte, queste ottimizzazioni delle prestazioni sono comunque trascurabili, ma i costi di manutenzione possono essere enormi.

Certamente tutte le "piccole" cose dovrebbero essere cambiate a favore della chiarezza poiché, come hai sottolineato, la maggior parte di esse verrà comunque ottimizzata dal compilatore.

Per quanto riguarda le ottimizzazioni più grandi, potrebbe esserci la possibilità che le ottimizzazioni siano effettivamente fondamentali per raggiungere prestazioni ragionevoli (anche se questo non accade sorprendentemente spesso). Vorrei apportare le modifiche e quindi profilare il codice prima e dopo le modifiche. Se il nuovo codice presenta significativi problemi di prestazioni, è sempre possibile eseguire il rollback alla versione ottimizzata e, in caso contrario, attenersi semplicemente alla versione del codice più pulita.

Modificare solo una parte del codice alla volta e vedere come influisce sulle prestazioni dopo ogni ciclo di refactoring.


8

Dipende dal motivo per cui il codice è stato ottimizzato e dagli effetti della sua modifica e dall'impatto del codice sulle prestazioni complessive. Dovrebbe anche dipendere dal fatto che tu abbia un buon modo per caricare le modifiche ai test.

Non è necessario apportare questa modifica senza la profilazione prima e dopo e, ovviamente, sotto un carico simile a quello che si vedrebbe sulla produzione. Ciò significa non utilizzare un piccolo sottoinsieme di dati su una macchina sviluppatore o testare quando un solo utente utilizza il sistema.

Se l'ottimizzazione era recente, potresti essere in grado di parlare con lo sviluppatore e scoprire esattamente quale fosse il problema e quanto lentamente l'applicazione era prima dell'ottimizzazione. Questo può dirti molto se vale la pena fare l'ottimizzazione e per quali condizioni era necessaria l'ottimizzazione (un rapporto che copre un anno intero ad esempio potrebbe non essere diventato lento fino a settembre o ottobre, se stai testando la tua modifica a febbraio, la lentezza potrebbe non essere ancora evidente e il test non è valido).

Se l'ottimizzazione è piuttosto vecchia, i metodi più recenti potrebbero anche essere più veloci e più leggibili.

In definitiva questa è una domanda per il tuo capo. Richiede tempo per il refactoring di qualcosa che è stato ottimizzato e per assicurarsi che il cambiamento non abbia influito sul risultato finale e che funzioni anche o almeno in modo accettabile rispetto al vecchio modo. Potrebbe desiderare che trascorriate il vostro tempo in altre aree invece di intraprendere un'attività ad alto rischio per risparmiare qualche minuto di tempo di programmazione. Oppure può concordare sul fatto che il codice è difficile da capire e ha bisogno di frequenti interventi e che ora sono disponibili metodi migliori.


6

se la profilazione mostra che l'ottimizzazione non è necessaria (non è in una sezione critica) o ha anche un tempo di esecuzione peggiore (a causa di una cattiva ottimizzazione prematura), quindi sostituirlo con il codice leggibile che è più facile da mantenere

assicurarsi inoltre che il codice si comporti allo stesso modo con i test appropriati


5

Pensalo dal punto di vista aziendale. Quali sono i costi del cambiamento? Quanto tempo è necessario per apportare la modifica e quanto risparmierete a lungo termine rendendo il codice più facile da estendere o mantenere? Ora allega un cartellino del prezzo a quel tempo e confrontalo con il denaro perso riducendo le prestazioni. Forse è necessario aggiungere o aggiornare un server per compensare le prestazioni perse. Forse il prodotto non soddisfa più i requisiti e non può più essere venduto. Forse non c'è perdita. Forse il cambiamento aumenta la robustezza e fa risparmiare tempo altrove. Ora prendi la tua decisione.

In una nota a margine, in alcuni casi potrebbe essere possibile mantenere entrambe le versioni di un frammento. È possibile scrivere un test che genera valori di input casuali e verificare i risultati con l'altra versione. Utilizzare la soluzione "intelligente" per verificare il risultato di una soluzione perfettamente comprensibile e ovviamente corretta e quindi ottenere qualche rassicurazione (ma nessuna prova) che la nuova soluzione è equivalente a quella precedente. Oppure fai il contrario e controlla il risultato del codice complicato con il codice dettagliato e documentando così l'intenzione dietro l'hacking in modo inequivocabile.


4

Fondamentalmente, ti stai chiedendo se il refactoring è un'impresa degna di nota. La risposta è sicuramente sì.

Ma...

... devi farlo con attenzione. Sono necessari test unitari, di integrazione, funzionali e prestazionali per qualsiasi codice che si sta effettuando il refactoring. Devi essere sicuro che testano davvero tutte le funzionalità richieste. Hai bisogno della possibilità di eseguirli facilmente e ripetutamente. Una volta ottenuto ciò, dovresti essere in grado di sostituire i componenti con nuovi componenti contenenti funzionalità equivalenti.

Martin Fowler ha scritto il libro su questo.


3

Non dovresti cambiare il codice di lavoro e di produzione senza una buona ragione. "Refactoring" non è un buon motivo sufficiente a meno che non si può fare il vostro lavoro senza che refactoring. Anche se ciò che stai facendo è correggere i bug all'interno del codice difficile stesso, dovresti prenderti il ​​tempo per capirlo e apportare il minimo cambiamento possibile. Se il codice è così difficile da capire, non sarai in grado di capirlo completamente, e quindi qualsiasi modifica apportata avrà effetti collaterali imprevedibili: bug, in altre parole. Maggiore è il cambiamento, maggiore è la probabilità che tu causi problemi.

Ci sarebbe un'eccezione a questo: se il codice incomprensibile avesse un set completo di unit test, potresti rifattorizzarlo. Dato che non ho mai visto o sentito parlare di codice incomprensibile con test unitari completi, scrivi prima i test unitari, ottieni l'accordo delle persone necessarie sul fatto che tali test unitari rappresentino effettivamente ciò che il codice dovrebbe fare e quindi apportare modifiche al codice . L'ho fatto una o due volte; è un dolore al collo e molto costoso, ma alla fine produce buoni risultati.


3

Se è solo un breve pezzo di codice che fa qualcosa di relativamente semplice in un modo difficile da capire, cambierei la "comprensione rapida" in un commento esteso e / o un'implementazione alternativa inutilizzata, come

#ifdef READABLE_ALT_IMPLEMENTATION

   double x=0;
   for(double n: summands)
     x += n;
   return x;

#else

   auto subsum = [&](int lb, int rb){
          double x=0;
          while(lb<rb)
            x += summands[lb++];
          return x;
        };
   double x_fin=0;
   for(double nsm: par_eval( subsum
                           , partitions(n_threads, 0, summands.size()) ) )
     x_fin += nsm;
   return x_fin;

#endif

3

La risposta è, senza perdita di generalità, sì. Aggiungi sempre codice moderno quando vedi codice difficile da leggere ed elimina il codice errato nella maggior parte dei casi. Uso il seguente processo:

  1. Cerca il test delle prestazioni e il supporto delle informazioni di profilazione. Se non esiste un test delle prestazioni, ciò che può essere affermato senza prove può essere respinto senza prove. Afferma che il tuo codice moderno è più veloce e rimuovi il vecchio codice. Se qualcuno discute (anche te stesso) chiedi loro di scrivere il codice di profilazione per dimostrare quale è più veloce.
  2. Se esiste il codice di profilazione, scrivi comunque il codice moderno. Denominalo in qualche modo <function>_clean(). Quindi, "corsa" il tuo codice contro il codice errato. Se il tuo codice è migliore, rimuovi il vecchio codice.
  3. Se il vecchio codice è più veloce, lascia comunque il tuo codice moderno. Serve come buona documentazione per ciò che l'altro codice è destinato a fare e poiché il codice "race" è presente, è possibile continuare a eseguirlo per documentare le caratteristiche delle prestazioni e le differenze tra i due percorsi. Puoi anche test unitario per differenze nel comportamento del codice. È importante sottolineare che un giorno il codice moderno batterà il codice "ottimizzato", garantito. È quindi possibile rimuovere il codice errato.

QED.


3

Se potessi insegnare al mondo una cosa (riguardo al software) prima di morire, insegnerei che "Performance contro X" è un falso dilemma.

Il refactoring è in genere noto come vantaggio per la leggibilità e l'affidabilità, ma può altrettanto facilmente supportare l'ottimizzazione. Quando gestisci il miglioramento delle prestazioni come una serie di refactoring, puoi rispettare la regola del campeggio e velocizzare l'applicazione. In realtà, almeno secondo me, spetta eticamente a te farlo.

Ad esempio, l'autore di questa domanda ha riscontrato un codice folle. Se questa persona leggesse il mio codice, troverebbero che la parte pazza è lunga 3-4 righe. È in un metodo da solo, e il nome e la descrizione del metodo indicano COSA sta facendo il metodo. Il metodo conterrebbe 2-6 righe di commenti in linea che descrivono COME il codice pazzo ottiene la risposta giusta, nonostante il suo aspetto discutibile.

Compartimentalizzato in questo modo, sei libero di scambiare le implementazioni di questo metodo come preferisci. In effetti, è probabilmente così che ho scritto la versione pazza per cominciare. Sei il benvenuto per provare, o almeno chiedere alternative. La maggior parte delle volte scoprirai che l'implementazione ingenua è notevolmente peggiore (di solito mi preoccupo solo di un miglioramento 2-10x), ma compilatori e librerie cambiano sempre e chissà cosa potresti trovare oggi che non era disponibile quando la funzione è stata scritta?


Una chiave importante per l'efficienza in molti casi è che un codice faccia il maggior lavoro possibile in modi che possono essere eseguiti in modo efficiente. Una delle cose che mi infastidisce con .NET è che non esistono meccanismi efficaci per copiare una parte di una raccolta in un'altra. La maggior parte delle raccolte memorizza grandi gruppi di elementi consecutivi (se non il tutto) in matrici, quindi ad esempio la copia degli ultimi 5.000 articoli da un elenco di 50.000 articoli dovrebbe decomporsi in alcune operazioni di copia in blocco (se non solo uno) più alcune altre passaggi eseguiti al massimo una manciata di volte ciascuno.
supercat,

Sfortunatamente, anche nei casi in cui dovrebbe essere possibile eseguire tali operazioni in modo efficiente, sarà spesso necessario disporre di loop "ingombranti" per 5.000 iterazioni (e in alcuni casi 45.000!). Se un'operazione può essere ridotta a cose come copie di array di massa, allora queste possono essere ottimizzate a livelli estremi producendo un grande guadagno. Se ogni iterazione di loop deve eseguire una dozzina di passaggi, è difficile ottimizzarne uno particolarmente bene.
supercat,

2

Probabilmente non è una buona idea toccarlo - se il codice è stato scritto in quel modo per motivi di prestazioni, significa che cambiarlo potrebbe riportare problemi di prestazioni che erano stati precedentemente risolti.

Se non decide di cambiare le cose per essere più leggibile ed estendibile: Prima di effettuare un cambiamento, punto di riferimento il vecchio codice sotto pesante carico. Ancora meglio se riesci a trovare un vecchio documento o un ticket di problemi che descrivono il problema di prestazioni che questo codice dall'aspetto strano dovrebbe risolvere. Quindi, dopo aver apportato le modifiche, eseguire nuovamente i test delle prestazioni. Se non è molto diverso, o comunque entro parametri accettabili, probabilmente è OK.

A volte può succedere che quando cambiano altre parti di un sistema, questo codice ottimizzato per le prestazioni non ha più bisogno di ottimizzazioni così pesanti, ma non c'è modo di saperlo per certo senza test rigorosi.


1
Uno dei ragazzi con cui lavoro ora ama ottimizzare le cose in aree che gli utenti colpiscono una volta al mese, se così spesso. Ci vuole tempo e non di rado causa altri problemi perché gli piace codificare e impegnare e lasciare che il QA o altre funzioni a valle effettivamente testino. : / Per essere onesti, è generalmente veloce, rapido e preciso, ma queste "ottimizzazioni" penny-ante rendono solo le cose più difficili per il resto della squadra e le loro morti permanenti sarebbero una buona cosa.
DaveE,

@DaveE: queste ottimizzazioni vengono applicate perché o problemi di prestazioni reali? O questo sviluppatore lo fa solo perché può? Immagino che se sai che le ottimizzazioni non avranno un impatto, puoi tranquillamente sostituirle con un codice più leggibile, ma mi fiderei solo di qualcuno che è un esperto del sistema per farlo.
FrustratedWithFormsDesigner l'

Hanno finito perché può. Di solito in genere salva alcuni cicli, ma quando l'interazione dell'utente con l'elemento del programma richiede un certo numero di secondi (da 15 a 300 ish), radere un decimo di secondo di autonomia alla ricerca di "efficienza" è sciocco. Soprattutto quando le persone che lo seguono devono impiegare del tempo reale per capire cosa ha fatto. Questa è un'applicazione PowerBuilder originariamente costruita 16 anni fa, quindi data la genesi delle cose la mentalità è forse comprensibile, ma si rifiuta di aggiornare la sua mentalità alla realtà attuale.
DaveE,

@DaveE: Penso di essere più d'accordo con il ragazzo con cui lavori che con te. Se non mi è permesso sistemare cose che sono lente per nessuna ragione, diventerò pazzo. Se vedo una riga di C ++ che usa ripetutamente l'operatore + per assemblare una stringa, o codice che si apre e legge / dev / urandom ogni volta attraverso il ciclo solo perché qualcuno ha dimenticato di impostare un flag, allora lo aggiusto. Essendo fanatico di questo sono riuscito a mantenere la velocità, quando altre persone avrebbero lasciato scorrere un microsecondo alla volta.
Zan Lynx,

1
Bene, dovremo concordare di non essere d'accordo. Trascorrere un'ora a cambiare qualcosa per risparmiare secondi frazionari in fase di esecuzione per una funzione che si esegue davvero occasionalmente e lasciare il codice in forma di grattacapo per gli altri sviluppatori ... non è giusto. Se queste fossero funzioni che venivano eseguite ripetutamente nelle parti più stressanti dell'app, bene e dandy. Ma non è questo il caso che sto descrivendo. Questo è un codice davvero gratuito per il finagling per nessun altro motivo se non quello di dire "Ho fatto questa cosa che UserX fa una volta alla settimana leggermente più veloce". Nel frattempo, stiamo pagando il lavoro che deve essere svolto.
DaveE,

2

Il problema qui è distinguere "ottimizzato" da leggibile ed estendibile, ciò che noi come utenti vediamo come codice ottimizzato e ciò che il compilatore vede come ottimizzato sono due cose diverse. Il codice che stai modificando potrebbe non essere affatto un collo di bottiglia, e quindi anche se il codice è "snello" non è nemmeno necessario che sia "ottimizzato". Oppure, se il codice è abbastanza vecchio, potrebbero esserci delle ottimizzazioni apportate dal compilatore agli built-in che rendono l'utilizzo di una struttura incorporata semplice nuova ugualmente o più efficiente del vecchio codice.

E il codice "magro" illeggibile non è sempre ottimizzato.

Ero convinto che il codice intelligente / snello fosse un buon codice, ma a volte approfittando delle regole oscure del linguaggio ferito piuttosto che di aiuto nella creazione del codice, sono stato morso più spesso che non in qualsiasi lavoro incorporato quando provavo a sii intelligente perché il compilatore trasforma il tuo codice intelligente in qualcosa di completamente inutilizzabile dall'hardware incorporato.


2

Non sostituirò mai il codice ottimizzato con il codice leggibile perché non posso scendere a compromessi con le prestazioni e sceglierò di utilizzare i commenti appropriati in ogni sezione in modo che chiunque possa comprendere la logica implementata in quella sezione ottimizzata che risolverà entrambi i problemi.

Quindi, il codice sarà ottimizzato + anche i commenti appropriati lo renderanno leggibile.

NOTA: è possibile rendere leggibile un codice ottimizzato con l'aiuto di commenti appropriati, ma non è possibile rendere un codice leggibile uno ottimizzato.


Sarei stanco di questo approccio in quanto tutto ciò che serve è una persona che modifica il codice per dimenticare di mantenere il commento sincronizzato. Improvvisamente ogni recensione successiva andrà via pensando di eseguire X mentre in realtà fa Y.
John D

2

Ecco un esempio per vedere la differenza tra codice semplice e codice ottimizzato: https://stackoverflow.com/a/11227902/1396264

verso la fine della risposta sostituisce semplicemente:

if (data[c] >= 128)
    sum += data[c];

con:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

Per essere onesti, non ho idea di cosa sia stata sostituita l'istruzione if, ma poiché il risponditore dice che alcune operazioni bit per bit danno lo stesso risultato (ho intenzione di prendere la sua parola) .

Questo viene eseguito in meno di un quarto del tempo originale (11,54 secondi contro 2,5 secondi)


1

La domanda principale qui è: è necessaria l'ottimizzazione?

In tal caso, non è possibile sostituirlo con un codice più lento e più leggibile. Dovrai aggiungere commenti ecc. Per renderlo più leggibile.

Se il codice non deve essere ottimizzato, non dovrebbe esserlo (al punto da influire sulla leggibilità) e puoi ricodificarlo per renderlo più leggibile.

TUTTAVIA - assicurati di sapere esattamente cosa fa il codice e come testarlo completamente prima di iniziare a cambiare le cose. Ciò include l'utilizzo massimo ecc. Se non è necessario comporre un set di casi di test ed eseguirli prima e dopo, non si ha il tempo di eseguire il refactoring.


1

Questo è il modo in cui faccio le cose: prima lo faccio funzionare in codice leggibile, quindi lo ottimizzo. Conservo la fonte originale e documento i miei passaggi di ottimizzazione.

Quindi, quando devo aggiungere una funzione, torno al mio codice leggibile, aggiungo la funzione e seguo i passaggi di ottimizzazione che ho documentato. Poiché hai documentato, è davvero semplice e veloce riaprimizzare il tuo codice con la nuova funzionalità.


0

La leggibilità di IMHO è più importante del codice ottimizzato perché nella maggior parte dei casi la micro-ottimizzazione non ne vale la pena.

Articolo sulle micro-ottimizzazioni senza senso :

Come la maggior parte di noi, sono stanco di leggere post di blog su micro-ottimizzazioni senza senso come la sostituzione di stampa con eco, ++ $ i con $ i ++ o doppie virgolette con virgolette singole. Perché? Perché il 99.999999% delle volte, è irrilevante.

"print" utilizza un altro codice operativo rispetto a "echo" perché restituisce effettivamente qualcosa. Possiamo concludere che l'eco è più veloce della stampa. Ma un codice operativo non costa nulla, davvero niente.

Ho provato una nuova installazione di WordPress. Lo script si interrompe prima di terminare con un "Errore di bus" sul mio laptop, ma il numero di codici operativi era già di oltre 2,3 milioni. È stato detto abbastanza.


0

L'ottimizzazione è relativa. Per esempio:

Prendi in considerazione una lezione con un gruppo di membri BOOL:

// no nitpicking over BOOL vs bool allowed
class Pear {
 ...
 BOOL m_peeled;
 BOOL m_sliced;
 BOOL m_pitted;
 BOOL m_rotten;
 ...
};

Potresti essere tentato di convertire i campi BOOL in bitfield:

class Pear {
 ...
 BOOL m_peeled:1;
 BOOL m_sliced:1;
 BOOL m_pitted:1;
 BOOL m_rotten:1;
 ...
};

Poiché un BOOL viene digitato come INT (che su piattaforme Windows è un numero intero a 32 bit con segno), questo richiede sedici byte e li racchiude in uno. È un risparmio del 93%! Chi potrebbe lamentarsene?

Questo presupposto:

Poiché un BOOL viene digitato come INT (che su piattaforme Windows è un numero intero a 32 bit con segno), questo richiede sedici byte e li racchiude in uno. È un risparmio del 93%! Chi potrebbe lamentarsene?

porta a:

La conversione di una BOOL in un campo a bit singolo ha salvato tre byte di dati ma ti ha costato otto byte di codice quando al membro viene assegnato un valore non costante. Allo stesso modo, l'estrazione del valore diventa più costosa.

Quello che era

 push [ebx+01Ch]      ; m_sliced
 call _Something@4    ; Something(m_sliced);

diventa

 mov  ecx, [ebx+01Ch] ; load bitfield value
 shl  ecx, 30         ; put bit at top
 sar  ecx, 31         ; move down and sign extend
 push ecx
 call _Something@4    ; Something(m_sliced);

La versione bitfield è più grande di nove byte.

Sediamoci e facciamo un po 'di aritmetica. Supponiamo che a ciascuno di questi campi bitfield si acceda sei volte nel codice, tre volte per la scrittura e tre volte per la lettura. Il costo nella crescita del codice è di circa 100 byte. Non sarà esattamente 102 byte perché l'ottimizzatore potrebbe essere in grado di sfruttare i valori già presenti nei registri per alcune operazioni e le istruzioni aggiuntive potrebbero avere costi nascosti in termini di ridotta flessibilità dei registri. La differenza effettiva potrebbe essere maggiore, potrebbe essere inferiore, ma per un calcolo retro busta chiamiamolo 100. Nel frattempo, il risparmio di memoria era di 15 byte per classe. Pertanto, il punto di pareggio è sette. Se il tuo programma crea meno di sette istanze di questa classe, il costo del codice supera il risparmio di dati: l'ottimizzazione della memoria è stata una de-ottimizzazione della memoria.

Riferimenti

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.