Il graduale spostamento della metodologia di scrittura del codice ha influito sulle prestazioni del sistema? E dovrei preoccuparmi?


35

TD; DR:

C'era un po 'di confusione su ciò che stavo chiedendo, quindi ecco l'idea alla base della domanda:

Ho sempre voluto che la domanda fosse quella che è. Potrei non averlo articolato bene in origine. Ma l'intento è sempre stato " è un codice modulare, separato, accoppiato, disaccoppiato, refactored " marcatamente più lento per sua natura rispetto al codice " monolitico unità singola, fare tutto in un posto, un file, strettamente accoppiato ". Il resto sono solo dettagli e varie manifestazioni di questo che mi sono imbattuto allora o ora o lo farò più tardi. È sicuramente più lento su una certa scala. Come un disco non deframmentato, devi raccogliere i pezzi da ogni parte. È più lento. Di sicuro. Ma dovrei preoccuparmi?

E la domanda non riguarda ...

non si tratta di micro-ottimizzazione, ottimizzazione prematura, ecc. Non si tratta di "ottimizzare questa o quella parte fino alla morte".

Quindi cos'è?

Riguarda la metodologia generale e le tecniche e i modi di pensare alla scrittura del codice emersi nel tempo:

  • "inserisci questo codice nella tua classe come dipendenza"
  • "scrivi un file per classe"
  • "separa la tua vista dal tuo database, controller, dominio".
  • non scrivere spaghetti omogenei a codice singolo, ma scrivere molti componenti modulari separati che lavorano insieme

Riguarda il modo e lo stile del codice che è attualmente - entro questo decennio - visto e sostenuto nella maggior parte dei framework, sostenuto nelle convenzioni, trasmesso attraverso la comunità. È un passaggio nel pensare dai "blocchi monolitici" ai "microservizi". E da ciò deriva il prezzo in termini di prestazioni e sovraccarico a livello di macchina, e anche alcune spese generali a livello di programmatore.

Segue la domanda originale:

Nel campo dell'Informatica, ho notato un notevole cambiamento nel modo di pensare quando si tratta di programmazione. Mi capita spesso di vedere il consiglio che va così:

  • scrivere un codice più piccolo dal punto di vista funzionale (più testabile e gestibile in questo modo)
  • rielaborare il codice esistente in blocchi di codice sempre più piccoli fino a quando la maggior parte dei metodi / funzioni sono lunghi solo poche righe ed è chiaro qual è il loro scopo (che crea più funzioni rispetto a un blocco monolitico più grande)
  • scrivere funzioni che fanno solo una cosa: separazione delle preoccupazioni, ecc. (che di solito crea più funzioni e più frame in una pila)
  • creare più file (una classe per file, più classi a fini di decomposizione, per scopi di livello come MVC, architettura di dominio, modelli di progettazione, OO, ecc., che crea più chiamate al file system)

Questo è un cambiamento rispetto alle pratiche di codifica "vecchie" o "obsolete" o "spaghetti" in cui hai metodi che si estendono su 2500 linee e grandi classi e oggetti divini che fanno tutto.

La mia domanda è questa:

quando la chiamata si riduce al codice della macchina, a 1 e 0, alle istruzioni di assemblaggio, ai piatti dell'HDD, dovrei essere preoccupato del fatto che anche il mio codice OO perfettamente separato dalla classe con varietà di funzioni e metodi refactored da piccoli a piccoli molto più spese generali?

Dettagli

Anche se non ho familiarità con il modo in cui il codice OO e le sue chiamate di metodo vengono gestiti in ASM alla fine, e come le chiamate DB e le chiamate del compilatore si traducono nel movimento del braccio dell'attuatore su un piatto HDD, ho qualche idea. Suppongo che ogni chiamata di funzione aggiuntiva, chiamata di oggetto o chiamata "#include" (in alcune lingue), generi un set aggiuntivo di istruzioni, aumentando così il volume del codice e aggiungendo vari overhead di "cablaggio del codice", senza l'aggiunta di codice "utile" . Immagino anche che si possano fare buone ottimizzazioni ad ASM prima che venga effettivamente eseguito sull'hardware, ma che l'ottimizzazione può fare anche solo così tanto.

Quindi, la mia domanda: quanto sovraccarico (nello spazio e nella velocità) fa codice ben separato (codice che è suddiviso in centinaia di file, classi e modelli di progettazione, ecc.) Effettivamente introdotto rispetto ad avere "un grande metodo che contiene tutto in un unico file monolitico ", a causa di questo sovraccarico?

AGGIORNAMENTO per chiarezza:

Suppongo che prendere lo stesso codice e dividerlo, riformattarlo, disaccoppiarlo in sempre più funzioni e oggetti e metodi e classi comporterà il passaggio di un numero sempre maggiore di parametri tra pezzi di codice più piccoli. Perché di sicuro, il codice di refactoring deve mantenere attivo il thread e ciò richiede il passaggio di parametri. Più metodi o più classi o più modelli di progettazione di Metodi di fabbrica, si traducono in un sovraccarico maggiore nel passaggio di vari bit di informazioni più di quanto non avvenga in una singola classe o metodo monolitico.

È stato detto da qualche parte (citazione da TBD) che fino al 70% di tutto il codice è costituito dall'istruzione MOV di ASM: caricare i registri della CPU con le variabili appropriate, non il calcolo vero e proprio. Nel mio caso, si carica il tempo della CPU con le istruzioni PUSH / POP per fornire il collegamento e il passaggio dei parametri tra vari pezzi di codice. Più piccolo rendi le tue parti di codice, maggiore sarà il "collegamento" ambientale. Sono preoccupato che questo collegamento si aggiunga al gonfiore e al rallentamento del software e mi chiedo se dovrei preoccuparmi di questo e quanto, se non del tutto, perché le generazioni attuali e future di programmatori che stanno costruendo software per il prossimo secolo , dovrà convivere e consumare software creato utilizzando queste pratiche.

AGGIORNAMENTO: più file

Sto scrivendo un nuovo codice ora che sta lentamente sostituendo il vecchio codice. In particolare ho notato che una delle vecchie classi era un file di ~ 3000 righe (come menzionato in precedenza). Ora sta diventando un insieme di 15-20 file situati in varie directory, inclusi i file di test e non incluso il framework PHP che sto usando per unire alcune cose. Stanno arrivando anche altri file. Quando si tratta di I / O su disco, il caricamento di più file è più lento rispetto al caricamento di un file di grandi dimensioni. Ovviamente non tutti i file vengono caricati, vengono caricati in base alle esigenze e esistono opzioni di memorizzazione nella cache del disco e di memorizzazione nella memoria, eppure credo che ciò loading multiple filesrichieda una maggiore elaborazione rispetto loading a single filealla memoria. Lo sto aggiungendo alla mia preoccupazione.

AGGIORNAMENTO: Dipendenza Iniettare tutto

Tornando a questo dopo un po '.. Penso che la mia domanda sia stata fraintesa. O forse ho scelto di fraintendere alcune risposte. Non sto parlando di microottimizzazione come alcune risposte hanno individuato, (almeno penso che chiamare ciò di cui sto parlando di microottimizzazione sia un termine improprio) ma del movimento di "Codice refactor per allentare l'accoppiamento stretto", nel suo insieme , ad ogni livello del codice. Sono venuto da Zend Con proprio di recente, dove questo stile di codice è stato uno dei punti cardine e centrali della convenzione. Separare la logica dalla vista, dalla vista dal modello, dal modello dal database e, se possibile, separare i dati dal database. Dipendenza: Inietti tutto, il che a volte significa semplicemente aggiungere un codice di cablaggio (funzioni, classi, piastra di caldaia) che non fa nulla, ma funge da punto di aggancio / aggancio, raddoppiando facilmente la dimensione del codice nella maggior parte dei casi.

AGGIORNAMENTO 2: La "separazione del codice in più file" influisce in modo significativo sulle prestazioni (a tutti i livelli di elaborazione)

In che modo la filosofia di compartmentalize your code into multiple filesimpatto dell'elaborazione odierna (prestazioni, utilizzo del disco, gestione della memoria, attività di elaborazione della CPU)?

Io sto parlando di

Prima...

In un passato ipotetico ma non molto distante, potresti facilmente scrivere un monoblocco di un file che ha modello e visualizza e controlla spaghetti o non-spaghetti, ma che esegue tutto una volta che è già caricato. Facendo alcuni benchmark in passato usando il codice C ho scoperto che è MOLTO più veloce caricare in memoria un singolo file da 900 Mb ed elaborarlo in grossi pezzi piuttosto che caricare un mucchio di file più piccoli ed elaborarli in un pasto di pace più piccolo pezzi alla fine fanno lo stesso lavoro.

.. E adesso*

Oggi mi trovo a guardare il codice che mostra un libro mastro, che ha caratteristiche come ... se un articolo è un "ordine", mostra il blocco HTML dell'ordine. Se un elemento pubblicitario può essere copiato, stampa il blocco HTML che visualizza un'icona e i parametri HTML dietro di esso che consente di effettuare la copia. Se l'elemento può essere spostato verso l'alto o verso il basso, visualizzare le frecce HTML appropriate. Ecc. Posso, tramite Zend Framework, crearepartial()chiamate, che essenzialmente significa "chiama una funzione che accetta i tuoi parametri e li inserisce in un file HTML separato che chiama anche". A seconda di quanto dettagliato desidero ottenere, posso creare funzioni HTML separate per le parti più piccole del libro mastro. Uno per freccia su, freccia giù, uno per "posso copiare questo elemento", ecc. Crea facilmente diversi file solo per visualizzare una piccola parte della pagina web. Prendendo il mio codice e il codice Zend Framework dietro le quinte, il sistema / stack probabilmente chiama circa 20-30 file diversi.

Che cosa?

Sono interessato agli aspetti, all'usura della macchina che viene creata compartimentando il codice in molti file separati più piccoli.

Ad esempio, caricare più file significa trovarli in vari punti del file system e in vari punti dell'HDD fisico, il che significa più tempo di ricerca e lettura dell'HDD.

Per CPU probabilmente significa più cambio di contesto e caricamento di vari registri.

In questo sottoblocco (aggiornamento n. 2) sono interessato più strettamente al modo in cui l'utilizzo di più file per svolgere le stesse attività che potrebbero essere eseguite in un singolo file, influisce sulle prestazioni del sistema.

Utilizzo dell'API Zend Form vs HTML semplice

Ho usato l'API Zend Form con le più recenti e moderne pratiche OO, per creare un modulo HTML con convalida, trasformandolo POSTin oggetti di dominio.

Mi ci sono voluti 35 file per farlo.

35 files = 
    = 10 fieldsets x {programmatic fieldset + fieldset manager + view template} 
    + a few supporting files

Tutto ciò potrebbe essere sostituito con alcuni semplici file HTML + PHP + JS + CSS, forse un totale di 4 file leggeri.

È meglio? Ne vale la pena? ... Immagina di caricare 35 file + numerosi file di libreria Zend Zramework che li fanno funzionare, contro 4 semplici file.


1
Domanda fantastica. Farò un po 'di benchmarking (prendimi un giorno o giù di lì per trovare alcuni buoni casi). Tuttavia, i miglioramenti della velocità a questo livello comportano un costo enorme per la leggibilità e i costi di sviluppo. La mia ipotesi iniziale è che il risultato è un aumento trascurabile delle prestazioni.
Dan Sabin,

7
@Dan: Puoi inserirlo nel tuo calendario per fare un benchmark del codice dopo 1, 5 e 10 anni di manutenzione. Se ricordo che
ricercherò

1
Sì, questo è il vero kicker. Penso che siamo tutti d'accordo nel fare meno ricerche e le chiamate di funzione sono più veloci. Tuttavia, non riesco a immaginare un caso in cui ciò fosse preferito a cose come l'addestramento facile e costante di nuovi membri del team.
Dan Sabin,

2
Per chiarire, hai requisiti di velocità specifici per il tuo progetto. O vuoi solo che il tuo codice "vada più veloce!" Se è quest'ultimo non me ne preoccuperei, il codice che è abbastanza veloce ma facile da mantenere è molto meglio del codice che è più veloce che abbastanza veloce ma è un casino.
Cormac Mulhall

4
L'idea di evitare le chiamate di funzione per motivi di prestazioni è esattamente il tipo di folle matto che pensa Dijkstra contro la sua famosa citazione sull'ottimizzazione pre-matura. Seriamente, non posso
RibaldEddie

Risposte:


10

La mia domanda è questa: quando la chiamata si riduce al codice macchina, a 1 e 0, alle istruzioni di assemblaggio, dovrei preoccuparmi del fatto che il mio codice separato dalla classe con varietà di funzioni da piccole a minuscole genera un sovraccarico extra?

La mia risposta è sì, dovresti. Non perché hai molte piccole funzioni (una volta il sovraccarico delle funzioni di chiamata era ragionevolmente significativo e potresti rallentare il tuo programma facendo un milione di piccole chiamate in loop, ma oggi i compilatori li incorporeranno per te e ciò che resta è preso cura dagli algoritmi di previsione della CPU, quindi non preoccuparti di questo) ma perché introdurrai il concetto di stratificazione eccessiva nei tuoi programmi quando la funzionalità è troppo piccola per ingannare sensatamente nella tua testa. Se hai componenti più grandi puoi essere ragionevolmente sicuro che non stanno eseguendo lo stesso lavoro più e più volte, ma puoi rendere il tuo programma così minuziosamente granulare che potresti trovarti incapace di capire veramente i percorsi delle chiamate, e in questo finire con qualcosa che funziona a malapena (ed è a malapena mantenibile).

Ad esempio, ho lavorato in un posto che mi ha mostrato un progetto di riferimento per un servizio web con 1 metodo. Il progetto comprendeva 32 file .cs - per un singolo servizio Web! Ho pensato che fosse troppo complicata, anche se ogni parte era minuscola e facilmente comprensibile da sola, quando si trattava di descrivere il sistema generale, mi sono trovato rapidamente a dover rintracciare le chiamate solo per vedere cosa diavolo stava facendo (lì c'erano anche troppe astrazioni coinvolte, come ti aspetteresti). Il mio servizio web sostitutivo era di 4 file .cs.

non ho misurato le prestazioni in quanto immagino che sarebbe stato più o meno lo stesso nel complesso, ma posso garantire che il mio fosse significativamente più economico da mantenere. Quando tutti parlano del tempo del programmatore che è più importante del tempo della CPU, quindi crea mostri complessi che costano molto tempo al programmatore sia in sviluppo che in manutenzione, devi chiederti che stanno inventando scuse per comportamenti scorretti.

È stato detto da qualche parte (citazione da TBD) che fino al 70% di tutto il codice è costituito dall'istruzione MOV di ASM: caricare i registri della CPU con le variabili appropriate, non il calcolo vero e proprio.

Questo è ciò che fanno le CPU, spostano i bit dalla memoria ai registri, li aggiungono o sottraggono e quindi li rimettono in memoria. Tutto il calcolo si riduce praticamente a quello. Intendiamoci, una volta avevo un programma molto multi-thread che impiegava la maggior parte del suo tempo a cambiare contesto (ovvero salvare e ripristinare lo stato di registro dei thread) di quanto non funzionasse sul codice thread. Una semplice serratura nel posto sbagliato ha davvero rovinato la performance lì, ed è stato anche un pezzo di codice così innocuo.

Quindi il mio consiglio è: trovare una via di mezzo sensata tra i due estremi che faccia apparire il tuo codice buono agli altri umani e testare il sistema per vedere se funziona bene. Utilizzare le funzionalità del sistema operativo per assicurarsi che funzioni come previsto con CPU, memoria, disco e I / O di rete.


Penso che questo mi parli di più in questo momento. Mi suggerisce che una buona via di mezzo sia iniziare con concetti di mappatura mentale per codificare pezzi mentre seguono e usando determinati concetti (come DI), piuttosto che rimanere legati alla decomposizione del codice esoterico e impazzire (cioè DI tutto se ti serve o no).
Dennis,

1
Personalmente, trovo che più codice "" moderno "sia un po 'più facile da profilare ... Immagino che più un codice sia gestibile, è anche più facile da profilare, ma c'è un limite, in cui spezzare le cose in piccoli pezzi le rende meno
gestibile

25

Senza estrema cura, la micro ottimizzazione come queste preoccupazioni porta a un codice non mantenibile.

Inizialmente sembra una buona idea, il profiler ti dice che il codice è più veloce e V & V / Test / QA dice anche che funziona. Presto vengono rilevati bug, vengono richiesti requisiti di modifica e miglioramenti che non sono mai stati considerati.

Durante la vita di un progetto il codice si degrada e diventa meno efficiente. Il codice mantenibile diventerà più efficiente della sua controparte non mantenibile in quanto degraderà più lentamente. Il motivo è che il codice costruisce l'entropia quando viene modificato -

Il codice non mantenibile ha rapidamente più codice morto, percorsi ridondanti e duplicazione. Questo porta a più bug, creando un ciclo di degrado del codice - comprese le sue prestazioni. In breve tempo, gli sviluppatori hanno poca fiducia che le modifiche che stanno apportando siano corrette. Questo li rallenta, rende cauti e generalmente porta a ancora più entropia poiché affrontano solo i dettagli che possono vedere

Il codice gestibile, con piccoli moduli e test unitari è più facile da modificare, il codice non più necessario è più facile da identificare e rimuovere. Il codice che è rotto è anche più facile da identificare, può essere riparato o sostituito con fiducia.

Quindi alla fine si riduce alla gestione del ciclo di vita e non è così semplice come "questo è più veloce, quindi sarà sempre più veloce".

Soprattutto, il codice corretto lento è infinitamente più veloce del codice errato veloce.


Thankx. Per spingerlo un po 'verso dove stavo andando, non sto parlando di micro-ottimizzazione, ma di una mossa globale verso la scrittura di codice più piccolo, che incorpora più iniezione di dipendenza e quindi più parti di funzionalità esterne e più "parti mobili" del codice in generale che tutti devono essere collegati insieme per funzionare. Tendo a pensare che questo produca più lanugine di collegamento / connettore / passaggio variabile / MOV / PUSH / POP / CALL / JMP a livello hardware. Vedo anche un valore nel passaggio alla leggibilità del codice, anche se a scapito del sacrificio di cicli di elaborazione a livello hardware per "fluff".
Dennis,

10
Evitare le chiamate di funzione per motivi di prestazioni è assolutamente una micro-ottimizzazione! Sul serio. Non riesco a pensare a un esempio migliore di micro-ottimizzazione. Quali prove hai che la differenza di prestazioni conta davvero per il tipo di software che stai scrivendo? Sembra che tu non ne abbia.
RibaldEddie

17

Per quanto ne so, come fai notare con inline, su forme di livello inferiore di codice come C ++ può fare la differenza, ma dico leggermente.

Il sito lo riassume: non esiste una risposta facile . Dipende dal sistema, dipende da ciò che l'applicazione sta facendo, dipende dalla lingua, dipende dal compilatore e dall'ottimizzazione.

C ++ per esempio, inline può aumentare le prestazioni. Molte volte potrebbe non fare nulla o eventualmente ridurre le prestazioni, ma personalmente non l'ho mai incontrato, anche se ho sentito parlare di storie. Inline non è altro che un suggerimento per il compilatore da ottimizzare, che può essere ignorato.

È probabile che, se stai sviluppando programmi di livello superiore, l'overhead non dovrebbe essere un problema se ce n'è uno per cominciare. I compilatori sono estremamente intelligenti in questi giorni e dovrebbero comunque gestire queste cose. Molti programmatori hanno un codice per vivere: non fidarti mai del compilatore. Se questo è il tuo caso, anche le lievi ottimizzazioni che ritieni importanti possono essere. Ma tieni presente che ogni lingua differisce in questo senso. Java esegue automaticamente l'ottimizzazione in fase di esecuzione. In Javascript, inline per la tua pagina web (al contrario di file separati) è un impulso e ogni milisecondo per una pagina web potrebbe contare ma questo è più un problema di I / O.

Ma su programmi di livello inferiore in cui il programmatore potrebbe fare molto lavoro con il codice macchina insieme a qualcosa come C ++, l'ascolto potrebbe fare la differenza. I giochi sono un buon esempio di dove il pipelining della CPU è fondamentale, specialmente su console, e qualcosa come inline può aggiungere un po 'qua e là.

Una buona lettura in linea in particolare: http://www.gotw.ca/gotw/033.htm


Venendo dal punto di vista della mia domanda, non mi concentro tanto sull'allineamento di per sé, ma sul "cablaggio codificato" che occupa il processo di CPU, bus e I / O che collega vari pezzi di codice. Mi chiedo se c'è qualche punto in cui c'è il 50% o più del codice di cablaggio e il 50% del codice effettivo che si desidera eseguire. Immagino che ci sia molta lanugine anche nel codice più stretto che si possa scrivere, e sembra essere un dato di fatto. Gran parte del codice effettivo che viene eseguito a livello di bit e byte è la logistica: spostare i valori da un posto a un altro, saltare in un posto o nell'altro e solo qualche volta fare aggiungendo ..
Dennis,

... sottrazione o altra funzione correlata al business. Proprio come lo srotolamento dei loop può accelerare alcuni loop a causa del minor sovraccarico allocato per l'incremento delle variabili, la scrittura di funzioni più grandi probabilmente può aggiungere un po 'di velocità, a condizione che il tuo caso d'uso sia impostato per questo. La mia preoccupazione qui è più generale, vedendo molti consigli per scrivere pezzi di codice più piccoli, aumenta questo cablaggio, beneficiando al contempo di aggiungere la leggibilità (si spera) e a spese di gonfiamento a livello micro.
Dennis,

3
@Dennis - una cosa da considerare è che in un linguaggio OO, potrebbe esserci MOLTA piccola correlazione tra ciò che scrive il programmatore (a + b) e quale codice viene generato (una semplice aggiunta di due registri? Passa prima dalla memoria? Cast, quindi la funzione chiama nell'operatore di un oggetto +?). Quindi "piccole funzioni" a livello di programmatore possono essere tutt'altro che piccole una volta rese nel codice macchina.
Michael Kohne,

2
@Dennis Posso dire che scrivere il codice ASM (direttamente, non compilato) per Windows segue le linee di "mov, mov, invoke, mov, mov, invoke". Con invoke essendo una macro che esegue una chiamata racchiusa in push / pops ... Occasionalmente farai chiamate di funzione nel tuo codice ma è sminuita da tutte le chiamate del sistema operativo.
Brian Knoblauch,

12

Questa è una modifica rispetto alle pratiche di codice "vecchio" o "cattivo" in cui hai metodi che si estendono su 2500 linee e grandi classi che fanno tutto.

Non credo che qualcuno abbia mai pensato che fosse una buona pratica. E dubito che le persone che lo hanno fatto lo abbiano fatto per motivi di performance.

Penso che la famosa citazione di Donald Knuth sia molto rilevante qui:

Dobbiamo dimenticare le piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali.

Quindi, nel 97% del tuo codice, usa solo buone pratiche, scrivi piccoli metodi (quanto è piccola una questione di opinione, non penso che tutti i metodi dovrebbero essere solo poche righe), ecc. Per il restante 3%, dove le prestazioni importa, misuralo . E se le misurazioni mostrano che avere molti piccoli metodi in realtà rallenta significativamente il codice, allora dovresti combinarli in metodi più grandi. Ma non scrivere codice non mantenibile solo perché potrebbe essere più veloce.


11

Devi stare attento ad ascoltare i programmatori esperti e il pensiero attuale. Le persone che si occupano da anni di enormi software hanno qualcosa da offrire.

Nella mia esperienza, ecco cosa porta a rallentamenti e non sono piccoli. Sono ordini di grandezza:

  • Il presupposto che qualsiasi riga di codice impieghi all'incirca il tempo di qualsiasi altra. Ad esempio cout << endlcontro a = b + c. Il primo richiede migliaia di volte in più rispetto al secondo. Stackexchange ha molte domande sul modulo "Ho provato diversi modi per ottimizzare questo codice, ma non sembra fare la differenza, perché no?" quando c'è una vecchia chiamata di funzione nel mezzo di essa.

  • Il presupposto che qualsiasi chiamata di funzione o metodo, una volta scritta, è ovviamente necessario. Funzioni e metodi sono facili da chiamare e la chiamata è in genere piuttosto efficiente. Il problema è che sono come carte di credito. Ti tentano di spendere più di quanto desideri e tendono a nascondere ciò che hai speso. Inoltre, il software di grandi dimensioni ha strati su strati di astrazione, quindi anche se vi è solo il 15% di scarto su ogni strato, su 5 strati che si combinano con un fattore di rallentamento di 2. La risposta a questa non è rimuovere funzionalità o scrivere funzioni più grandi, è disciplinarsi per stare attenti a questo problema ed essere disposti e in grado di sradicarlo .

  • Generalità al galoppo. Il valore dell'astrazione è che può farti fare di più con meno codice - almeno questa è la speranza. Questa idea può essere spinta agli estremi. Il problema con troppa generalità è che ogni problema è specifico e quando lo risolvi con astrazioni generali, quelle astrazioni non sono necessariamente in grado di sfruttare le proprietà specifiche del tuo problema. Ad esempio, ho visto una situazione in cui una classe di coda di priorità, che poteva essere efficiente a grandi dimensioni, veniva utilizzata quando la lunghezza non superava mai 3!

  • Struttura dei dati al galoppo. OOP è un paradigma molto utile, ma non incoraggia a minimizzare la struttura dei dati - piuttosto incoraggia a cercare di nasconderne la complessità. Ad esempio, esiste il concetto di "notifica" in cui se l'origine A viene modificato in qualche modo, A emette un evento di notifica in modo che B e C possano anche modificarsi in modo da mantenere coerente l'insieme. Questo può propagarsi su molti livelli e aumentare enormemente il costo della modifica. Quindi è del tutto possibile che la modifica ad A possa essere annullata subito dopoo cambiato in un'altra modifica, il che significa che lo sforzo speso nel tentativo di mantenere coerente l'ensemble deve essere fatto ancora una volta. Confermando che è la probabilità di bug in tutti questi gestori di notifica, e circolarità, ecc. È molto meglio cercare di mantenere la struttura dei dati normalizzata, in modo che qualsiasi modifica debba essere effettuata in un unico posto. Se i dati non normalizzati non possono essere evitati, è meglio avere passaggi periodici per riparare l'incoerenza, piuttosto che fingere che possano essere mantenuti coerenti con un breve guinzaglio.

... quando penso a più lo aggiungerò.


8

La risposta breve è "sì". E, generalmente, il codice sarà un po 'più lento.

Ma a volte un corretto refactoring OO-ish rivelerà ottimizzazioni che rendono il codice più veloce. Ho lavorato a un progetto in cui abbiamo creato un algoritmo Java complesso molto più OO-ish, con strutture dati adeguate, getter, ecc. Invece di matrici di oggetti nidificati disordinati. Ma, isolando e limitando meglio l'accesso alle strutture di dati, siamo riusciti a passare da array giganti di doppi (con valori null per risultati vuoti) a array più organizzati di doppi, con NaN per risultati vuoti. Ciò ha prodotto un guadagno di 10 volte in termini di velocità.

Addendum: in generale, un codice più piccolo e meglio strutturato dovrebbe essere più suscettibile al multi-threading, il modo migliore per ottenere importanti accelerazioni.


1
Non vedo perché il passaggio da Doubles a doubles richiederebbe un codice strutturato migliore.
svick

Wow, un downvote? Il codice client originale non si occupava di Double.NaNs, ma controllava i valori null per rappresentare valori vuoti. Dopo la ristrutturazione, potremmo gestirlo (tramite incapsulamento) con i getter dei vari risultati dell'algoritmo. Certo, avremmo potuto riscrivere il codice client, ma è stato più semplice.
user949300

Per la cronaca, non ero quello che ha votato in negativo questa risposta.
svick,

7

Tra l'altro, la programmazione riguarda i compromessi . Sulla base di questo fatto, propendo a rispondere di sì, potrebbe essere più lento. Ma pensa a cosa ottieni in cambio. Ottenere un codice leggibile, riutilizzabile e facilmente modificabile supera facilmente ogni possibile inconveniente.

Come menzionato da @ user949300 , è più facile individuare aree che possono essere migliorate in modo algoritmico o architettonico con tale approccio; il miglioramento di questi è di solito molto più vantaggioso ed efficace rispetto al non avere OO possibile o overhead di chiamata di funzione (che è già solo un rumore, scommetto).


Immagino anche che si possano fare buone ottimizzazioni ad ASM prima che venga effettivamente eseguito sull'hardware.

Ogni volta che qualcosa del genere mi viene in mente, ricordo che i decenni trascorsi dalle persone più intelligenti che lavorano ai compilatori stanno probabilmente rendendo strumenti come GCC molto migliori di me nel generare codice macchina. A meno che tu non stia lavorando su una sorta di roba correlata ai microcontrollori, ti suggerisco di non preoccupartene.

Suppongo che l'aggiunta di sempre più funzioni e sempre più oggetti e classi in un codice comporteranno un numero sempre maggiore di parametri che passano tra pezzi di codice più piccoli.

Supponendo che qualsiasi cosa durante l'ottimizzazione sia una perdita di tempo, sono necessari fatti sulle prestazioni del codice. Trova dove trascorri la maggior parte del tempo con strumenti specializzati, ottimizzalo, itera.


Per riassumere tutto; lascia che il compilatore faccia il suo lavoro, concentrati su cose importanti come il miglioramento dei tuoi algoritmi e delle strutture dei dati. Tutti gli schemi che hai citato nella tua domanda esistono per aiutarti, usali.

PS: Questi 2 discorsi di Crockford mi sono saltati in testa e penso che siano in qualche modo correlati. La prima è una storia di CS super breve (che è sempre buona conoscenza con qualsiasi scienza esatta); e la seconda riguarda il motivo per cui rifiutiamo le cose buone.


Mi piace molto questa risposta di più. Gli esseri umani sono orribili nel provare a indovinare il compilatore e fare scatti in corrispondenza dei colli di bottiglia. ovviamente la complessità del tempo big-O è qualcosa di cui dovresti essere a conoscenza, ma il grande metodo spaghetti a 100 linee contro una chiamata al metodo di fabbrica + alcuni invii di metodi virtuali non dovrebbero mai essere nemmeno una discussione. la prestazione non è affatto interessante in quel caso. inoltre, un grande vantaggio per notare che "l'ottimizzazione" senza fatti concreti e misurazioni è una perdita di tempo.
Sara

4

Credo che le tendenze che identifichi siano veritiere sullo sviluppo di software: il tempo del programmatore è più costoso del tempo della CPU. Finora, i computer sono diventati solo più veloci ed economici, ma per cambiare un groviglio di un'applicazione può richiedere centinaia se non migliaia di ore-uomo. Dati i costi di stipendi, indennità, spazi per uffici, ecc., È più conveniente avere un codice che potrebbe essere eseguito un po 'più lentamente ma è più rapido e sicuro da cambiare.


1
Sono d'accordo, ma i dispositivi mobili stanno diventando così popolari che penso che siano una grande eccezione. Sebbene la potenza di elaborazione stia aumentando, non è possibile creare un'app per iPhone e aspettarsi di poter aggiungere memoria come è possibile su un server web.
JeffO,

3
@JeffO: Non sono d'accordo - I dispositivi mobili con processori quad core sono ora normali, le prestazioni (soprattutto perché influiscono sulla durata della batteria), mentre una preoccupazione è meno importante della stabilità. Un telefono cellulare o tablet lento riceve recensioni scadenti, uno instabile viene macellato. Le app mobili sono molto dinamiche, cambiando quasi quotidianamente: la fonte deve tenere il passo.
Mattnz,

1
@JeffO: Per quello che vale, la macchina virtuale utilizzata su Android è piuttosto male. Secondo i benchmark potrebbe essere un ordine di grandezza più lento del codice nativo (mentre il meglio della razza è di solito solo un po 'più lento). Indovina, a nessuno importa. Scrivere l'applicazione è veloce e la CPU è lì a giocherellare con i pollici e aspettare comunque che l'utente entri il 90% delle volte.
Jan Hudec,

C'è di più in termini di prestazioni rispetto ai benchmark CPU grezzi. Il mio telefono Android funziona bene, tranne quando l'AV sta eseguendo la scansione di un aggiornamento e quindi sembra bloccarsi più a lungo di quanto mi piaccia, ed è un modello RAM quad-core da 2 Gb! Oggi la larghezza di banda (sia di rete che di memoria) è probabilmente il principale collo di bottiglia. La tua CPU superveloce sta probabilmente modificando il pollice del 99% delle volte e l'esperienza complessiva è ancora scarsa.
gbjbaanb,

4

Bene, più di 20 anni fa, che suppongo che tu non stia chiamando nuovo e non "vecchio o cattivo", la regola era di mantenere le funzioni abbastanza piccole da adattarsi a una pagina stampata. Avevamo quindi delle stampanti ad aghi quindi il numero di linee era in qualche modo fissato in genere solo una o due scelte per il numero di linee per pagina ... decisamente inferiore a 2500 linee.

Stai chiedendo a molti lati del problema, manutenibilità, prestazioni, testabilità, leggibilità. Quanto più ti inclini verso le prestazioni tanto meno gestibile e leggibile diventerà il codice, quindi devi trovare il tuo livello di comfort che può e varierà per ogni singolo programmatore.

Per quanto riguarda il codice prodotto dal compilatore (se lo si desidera, codice macchina), maggiore è la funzione, maggiori sono le possibilità di dover riversare valori intermedi nei registri nello stack. Quando si utilizzano i frame dello stack, il consumo dello stack è in blocchi più grandi. Più piccole sono le funzioni, maggiore è la possibilità che i dati rimangano maggiormente nei registri e minore è la dipendenza dallo stack. Pezzi più piccoli di stack necessari naturalmente per funzione. I frame stack presentano vantaggi e svantaggi per le prestazioni. Funzioni più piccole significano maggiore impostazione e pulizia delle funzioni. Naturalmente dipende anche da come compilate, da quali opportunità offrite al compilatore. Potresti avere 250 funzioni di linea 10 anziché una funzione di linea 2500, l'unica funzione di linea 2500 che il compilatore otterrà se può / sceglie di ottimizzare su tutto. Ma se prendi quelle 250 funzioni di 10 righe e le dividi in 2, 3, 4, 250 file separati, compila ciascun file separatamente, il compilatore non sarà in grado di ottimizzare quasi il codice morto che potrebbe avere. la linea di fondo qui è che ci sono pro e contro per entrambi e non è possibile stabilire una regola generale su questo o quello è il modo migliore.

Funzioni di dimensioni ragionevoli, qualcosa che una persona può vedere su uno schermo o una pagina (con un carattere ragionevole), è qualcosa che possono consumare meglio e capire del codice piuttosto grande. Ma se è solo una piccola funzione con chiamate a molte altre piccole funzioni che chiamano molte altre piccole funzioni, hai bisogno di più finestre o browser per capire quel codice in modo da non comprare nulla sul lato della leggibilità.

il modo unix è di usare il mio termine, lego block ben lucidati. Perché dovresti usare una funzione nastro così tanti anni dopo che abbiamo smesso di usare i nastri? Perché il BLOB ha fatto il suo lavoro molto bene e sul retro possiamo sostituire l'interfaccia del nastro con un'interfaccia di file e trarre vantaggio dalla carne del programma. Perché riscrivere il software di masterizzazione cdrom solo perché scsi è scomparso quando l'interfaccia dominante è stata sostituita da ide (per poi tornare in seguito). Prendi di nuovo il vantaggio dei blocchi secondari lucidati e sostituisci un'estremità con un nuovo blocco di interfaccia (capisci anche che i progettisti hardware hanno semplicemente affrontato un blocco di interfaccia sui progetti hardware in alcuni casi facendo in modo che un'unità scsi abbia un'interfaccia ide per scsi. , costruire blocchi lego lucidati di dimensioni ragionevoli, ciascuno con uno scopo ben definito e input e output ben definiti. puoi avvolgere i test attorno a quei blocchi lego e quindi prendere lo stesso blocco e avvolgere le interfacce dell'interfaccia utente e del sistema operativo intorno allo stesso blocco e blocco, in teoria, essendo ben collaudato e ben compreso, non sarà necessario eseguire il debug, solo i nuovi blocchi aggiuntivi aggiunti ad ogni estremità. purché tutte le interfacce dei blocchi siano ben progettate e le funzionalità ben comprese, è possibile creare molte cose con la minima o qualsiasi colla. proprio come con i blocchi lego blu e rosso e nero e giallo di dimensioni e forme conosciute puoi fare molte cose. purché tutte le interfacce dei blocchi siano ben progettate e le funzionalità ben comprese, è possibile creare molte cose con la minima o qualsiasi colla. proprio come con i blocchi lego blu e rosso e nero e giallo di dimensioni e forme conosciute puoi fare molte cose. purché tutte le interfacce dei blocchi siano ben progettate e le funzionalità ben comprese, è possibile creare molte cose con la minima o qualsiasi colla. proprio come con i blocchi lego blu e rosso e nero e giallo di dimensioni e forme conosciute puoi fare molte cose.

Ogni individuo è diverso, la loro definizione di lucido, ben definito, testato e leggibile varia. Ad esempio, non è irragionevole per un professore dettare regole di programmazione non perché possano essere o meno dannose per te come professionista, ma in alcuni casi per facilitare il lavoro di lettura e classificazione del codice sul professore o sugli assistenti degli studenti laureati ... È altrettanto probabile, professionalmente, scoprire che ogni lavoro può avere regole diverse per vari motivi, di solito una o poche persone al potere hanno la loro opinione su qualcosa, giusto o sbagliato e con quel potere possono dettare che lo fai in quel modo (o uscire, essere licenziato o in qualche modo arrivare al potere). Queste regole si basano spesso sull'opinione in quanto si basano su una sorta di fatto su leggibilità, prestazioni, testabilità, portabilità.


"Quindi devi trovare il tuo livello di comfort che può e varierà per ogni singolo programmatore" Penso che il livello di ottimizzazione dovrebbe essere deciso dai requisiti prestazionali di ogni pezzo di codice, non da ciò che piace a ciascun programmatore.
svick

Il compilatore è sicuro, supponendo che il programmatore abbia detto al compilatore di ottimizzare i compilatori, generalmente l'impostazione predefinita non viene ottimizzata (nella riga di comando l'IDE può avere un valore predefinito diverso se utilizzato). Ma il programmatore potrebbe non sapere abbastanza per ottimizzare le dimensioni della funzione e con altrettanti target dove diventa inutile? Le prestazioni del tuning manuale tendono ad avere un effetto negativo sulla leggibilità e, in particolare, sulla manutenibilità, la testabilità può andare in entrambi i modi.
old_timer

2

Dipende da quanto sia intelligente il tuo compilatore. In generale, provare a superare in astuzia l'ottimizzatore è una cattiva idea e potrebbe effettivamente derubare il compilatore di opportunità da ottimizzare. Per i principianti, probabilmente non hai idea di cosa possa fare e la maggior parte di ciò che fai influenza effettivamente quanto bene lo faccia.

L'ottimizzazione prematura è la nozione di programmatori che cercano di farlo e finiscono con un codice difficile da mantenere che non si trovava sul percorso critico di ciò che stavano cercando di fare. Cercare di spremere quanta più CPU possibile quando la maggior parte delle volte la tua app viene effettivamente bloccata in attesa di eventi IO, è qualcosa che vedo molto per esempio.

La cosa migliore è programmare la correttezza e usare un profiler per trovare i colli di bottiglia delle prestazioni effettive e correggerli analizzando ciò che si trova sul percorso critico e se può essere migliorato. Spesso alcune semplici correzioni fanno molto.


0

[elemento tempo del computer]

Il refactoring verso accoppiamenti più lenti e funzioni più piccole influisce sulla velocità del codice?

Sì, lo fa. Ma spetta a interpreti, compilatori e compilatori JIT eliminare questo codice "giuntura / cablaggio", e alcuni lo fanno meglio di altri, ma altri no.

La preoccupazione per più file si aggiunge al sovraccarico di I / O, quindi influisce anche sulla velocità (in tempo di computer).

[elemento velocità umana]

(E dovrei preoccuparmene?)

no non dovresti preoccupartene. Computer e circuiti sono molto veloci in questi giorni e altri fattori prendono il sopravvento, come la latenza della rete, l'I / O del database e la memorizzazione nella cache.

Quindi il rallentamento 2x - 4x nell'esecuzione del codice nativo stesso viene spesso annullato da quegli altri fattori.

Per quanto riguarda il caricamento di più file, ciò viene spesso gestito da varie soluzioni di memorizzazione nella cache. Potrebbe essere necessario più tempo per caricare le cose e unirle per la prima volta, ma per la prossima volta, per i file statici, la memorizzazione nella cache funziona come se si stesse caricando un singolo file. La memorizzazione nella cache è una soluzione per il caricamento di più file.


0

LA RISPOSTA (nel caso in cui tu l'abbia persa)

Sì, dovresti preoccuparti, ma preoccupati di come scrivi il codice e non delle prestazioni.

In breve

Non preoccuparti delle prestazioni

Nel contesto della domanda, compilatori ed interpreti più intelligenti si occupano già di questo

Fai attenzione a scrivere codice gestibile

Codice in cui i costi di manutenzione sono al livello di ragionevole comprensione umana. cioè non scrivere 1000 funzioni più piccole che rendono il codice incomprensibile anche se capisci ognuna, e non scrivere 1 funzione oggetto dio che è troppo grande per capire, ma scrivi 10 funzioni ben progettate che hanno senso per un essere umano, e sono facile da mantenere.

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.