Perché usare un approccio OO invece di un'enorme dichiarazione "switch"?


59

Sto lavorando in un negozio .Net, C # e ho un collega che continua a insistere sul fatto che dovremmo usare istruzioni Switch gigantesche nel nostro codice con molti "Casi" piuttosto che più approcci orientati agli oggetti. La sua argomentazione risale costantemente al fatto che un'istruzione Switch si compila in una "tabella di salto della cpu" ed è quindi l'opzione più veloce (anche se in altre cose al nostro team viene detto che non ci interessa la velocità).

Onestamente non ho una discussione contro questo ... perché non so di che diavolo sta parlando.
Ha ragione?
Sta solo parlando il culo?
Sto solo cercando di imparare qui.


7
Puoi verificare se ha ragione usando qualcosa come .NET Reflector per guardare il codice assembly e cercare la "tabella di salto della CPU".
FrustratedWithFormsDesigner

5
"Passa alla compilazione di un'istruzione" cpu jump table "Così fa il dispaccio del metodo peggiore con tutte le funzioni pure-virtuali. Nessuna funzione virtuale è semplicemente collegata direttamente. Hai scaricato del codice da confrontare?
S.Lott

64
Il codice dovrebbe essere scritto per le PERSONE, non per le macchine, altrimenti faremmo tutto in assemblaggio.
maple_shaft

8
Se è un gran disastro, citalo Knuth: "Dovremmo dimenticare piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali".
DaveE,

12
Manutenibilità. Altre domande con risposte di una sola parola con cui posso aiutarti?
Matt Ellen,

Risposte:


48

Probabilmente è un vecchio hacker C e sì, sta parlando dal suo culo. .Net non è C ++; il compilatore .Net continua a migliorare e gli hack più intelligenti sono controproducenti, se non oggi, nella prossima versione .Net. Piccole funzioni sono preferibili perché .Net JIT-s ogni funzione una volta prima di essere utilizzata. Quindi, se alcuni casi non vengono mai colpiti durante un ciclo di vita di un programma, quindi non sono previsti costi per la compilazione di JIT. Comunque, se la velocità non è un problema, non dovrebbero esserci ottimizzazioni. Scrivi prima per il programmatore, per il secondo compilatore. Il tuo collega non sarà facilmente convinto, quindi dimostrerei empiricamente che un codice meglio organizzato è in realtà più veloce. Vorrei scegliere uno dei suoi peggiori esempi, riscriverli in un modo migliore e quindi assicurarsi che il codice sia più veloce. Cherry-pick, se è necessario. Quindi eseguilo qualche milione di volte, profilo e mostralo.

MODIFICARE

Bill Wagner ha scritto:

Voce 11: Comprendi l'attrazione di piccole funzioni (efficace C # Seconda edizione) Ricorda che tradurre il tuo codice C # in codice eseguibile dalla macchina è un processo in due fasi. Il compilatore C # genera IL che viene consegnato in assembly. Il compilatore JIT genera il codice macchina per ciascun metodo (o gruppo di metodi, quando è coinvolto l'inline), se necessario. Le piccole funzioni rendono molto più semplice per il compilatore JIT l'ammortamento di tale costo. Le piccole funzioni hanno anche maggiori probabilità di essere candidati per l'allineamento. Non è solo piccolo: il flusso di controllo più semplice conta altrettanto. Un numero inferiore di rami di controllo all'interno delle funzioni semplifica la registrazione delle variabili da parte del compilatore JIT. Non è solo una buona pratica scrivere codice più chiaro; è il modo in cui crei codice più efficiente in fase di esecuzione.

EDIT2:

Quindi ... apparentemente un'istruzione switch è più veloce e migliore di una serie di istruzioni if ​​/ else, perché un confronto è logaritmico e un altro è lineare. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Bene, il mio approccio preferito alla sostituzione di un'enorme istruzione switch è con un dizionario (o talvolta anche un array se sto accendendo enum o piccoli ints) che sta mappando i valori alle funzioni che vengono chiamate in risposta ad essi. Farlo costringe a rimuovere un sacco di cattivi stati di spaghetti condivisi, ma questa è una buona cosa. Un'istruzione switch di grandi dimensioni è di solito un incubo di manutenzione. Quindi ... con array e dizionari, la ricerca richiederà un tempo costante e ci sarà poca memoria aggiuntiva sprecata.

Non sono ancora convinto che l'istruzione switch sia migliore.


47
Non preoccuparti di dimostrarlo più velocemente. Questa è ottimizzazione prematura. Il millisecondo che potresti salvare è nulla rispetto a quell'indice che hai dimenticato di aggiungere al database che ti costa 200ms. Stai combattendo la battaglia sbagliata.
Rein Henrichs,

27
@Job e se avesse davvero ragione? Il punto non è che ha torto, il punto è che ha ragione e non importa .
Rein Henrichs,

2
Anche se aveva ragione circa il 100% dei casi, sta ancora perdendo tempo.
Jeremy,

6
Voglio distogliere lo sguardo cercando di leggere quella pagina che hai collegato.
Attaccando Hobo

3
Cos'è l'odio C ++? Anche i compilatori C ++ stanno migliorando, e i grandi switch sono altrettanto cattivi in ​​C ++ come in C #, e per lo stesso motivo. Se sei circondato da ex programmatori C ++ che ti danno dolore non è perché sono programmatori C ++, ma perché sono cattivi programmatori.
Sebastian Redl,

39

A meno che il collega non sia in grado di dimostrare che questa alterazione offre un reale beneficio misurabile sulla scala dell'intera applicazione, è inferiore al vostro approccio (cioè il polimorfismo), che in realtà fornisce un tale vantaggio: la manutenibilità.

La microottimizzazione dovrebbe essere eseguita solo dopo aver bloccato le strozzature. L'ottimizzazione prematura è la radice di tutti i mali .

La velocità è quantificabile. Ci sono poche informazioni utili in "l'approccio A è più veloce dell'approccio B". La domanda è " Quanto più veloce? ".


2
Assolutamente vero. Non affermare mai che qualcosa è più veloce, misurare sempre. E misurare solo quando quella parte dell'applicazione è il collo di bottiglia delle prestazioni.
Kilian Foth,

6
-1 per "L'ottimizzazione prematura è la radice di tutti i mali." Si prega di visualizzare l' intera citazione, non solo una parte che distorce l'opinione di Knuth.
alternativa

2
@mathepic: non l'ho presentato intenzionalmente come preventivo. Questa frase, così com'è, è la mia opinione personale, sebbene ovviamente non sia una mia creazione. Anche se si può notare che i ragazzi di c2 sembrano considerare proprio quella parte la saggezza fondamentale.
back2dos,

8
@alternative La citazione completa di Knuth "Non c'è dubbio che il graal dell'efficienza porti all'abuso. I programmatori sprecano enormi quantità di tempo pensando o preoccupandosi della velocità delle parti non critiche dei loro programmi e questi tentativi di efficienza hanno effettivamente un forte impatto negativo quando si considerano il debug e la manutenzione. Dovremmo dimenticare le piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali ". Descrive perfettamente il collega dell'OP. IMHO back2dos ha riassunto bene la citazione con "l'ottimizzazione prematura è la radice di tutti i mali"
MarkJ

2
@MarkJ il 97% delle volte
alternativa il

27

A chi importa se è più veloce?

A meno che tu non stia scrivendo software in tempo reale, è improbabile che la minima quantità di accelerazione che potresti ottenere dal fare qualcosa in un modo completamente folle possa fare molta differenza per il tuo cliente. Non vorrei nemmeno combattere contro questo sul fronte della velocità, questo ragazzo chiaramente non ascolterà alcun argomento sull'argomento.

La manutenibilità, tuttavia, è lo scopo del gioco, e un'enorme dichiarazione switch non è nemmeno leggermente mantenibile, come spieghi i diversi percorsi attraverso il codice a un nuovo ragazzo? La documentazione dovrà essere lunga quanto il codice stesso!

Inoltre, hai la completa incapacità di testare l'unità in modo efficace (troppi percorsi possibili, per non parlare della probabile mancanza di interfacce ecc.), Il che rende il tuo codice ancora meno gestibile.

[Per quanto riguarda gli interessi: il JITter funziona meglio con metodi più piccoli, quindi dichiarazioni di commutazione giganti (e i loro metodi intrinsecamente grandi) danneggeranno la tua velocità in assiemi di grandi dimensioni, IIRC.]


1
+ un grande esempio di ottimizzazione prematura.
ShaneC,

Sicuramente questo.
DeadMG

+1 per "un'istruzione switch gigante non è nemmeno leggermente mantenibile"
Korey Hinton,

2
Un'enorme dichiarazione di switch è molto più facile da comprendere per il nuovo ragazzo: tutti i possibili comportamenti sono raccolti proprio lì in una bella lista. Le chiamate indirette sono estremamente difficili da seguire, nel peggiore dei casi (puntatore a funzione) è necessario cercare nell'intera base di codice le funzioni della firma giusta e le chiamate virtuali sono solo un po 'meglio (cercare funzioni con il nome e la firma giusti e legato per eredità). Ma la manutenibilità non riguarda l'essere di sola lettura.
Ben Voigt,

14

Allontanati dall'istruzione switch ...

Questo tipo di dichiarazione switch dovrebbe essere evitato come una pestilenza perché viola il principio aperto chiuso . Obbliga il team ad apportare modifiche al codice esistente quando è necessario aggiungere nuove funzionalità, invece di aggiungere semplicemente nuovo codice.


11
Questo viene fornito con un avvertimento. Esistono operazioni (funzioni / metodi) e tipi. Quando aggiungi una nuova operazione, devi solo cambiare il codice in un posto per le istruzioni switch (aggiungi una nuova funzione con istruzione switch), ma dovresti aggiungere quel metodo a tutte le classi nel caso OO (viola aperto / principio chiuso). Se aggiungi nuovi tipi, devi toccare ogni istruzione switch, ma nel caso OO aggiungeresti solo un'altra classe. Pertanto, per prendere una decisione informata devi sapere se aggiungerai più operazioni ai tipi esistenti o aggiungerai altri tipi.
Scott Whitlock,

3
Se è necessario aggiungere più operazioni ai tipi esistenti in un paradigma OO senza violare OCP, credo che sia questo lo scopo del modello di visitatore.
Scott Whitlock,

3
@Martin: se vuoi, chiamami per nome, ma questo è un compromesso ben noto. Ti rimando a Clean Code di RC Martin. Rivisita il suo articolo su OCP, spiegando ciò che ho descritto sopra. Non è possibile progettare contemporaneamente per tutti i requisiti futuri. Devi scegliere se è più probabile che aggiunga più operazioni o più tipi. OO favorisce l'aggiunta di tipi. È possibile utilizzare OO per aggiungere più operazioni se si modellano operazioni come classi, ma ciò sembra entrare nel modello di visitatore, che ha i suoi problemi (in particolare l'overhead).
Scott Whitlock,

8
@Martin: hai mai scritto un parser? È abbastanza comune avere grandi switch-case che attivano il token successivo nel buffer lookahead. È possibile sostituire tali switch con chiamate di funzione virtuale al token successivo, ma sarebbe un incubo di manutenzione. È raro, ma a volte la custodia è in realtà la scelta migliore, perché mantiene il codice che dovrebbe essere letto / modificato insieme nelle immediate vicinanze.
Nikie,

1
@Martin: hai usato parole come "mai", "sempre" e "Poppycock", quindi stavo assumendo che tu stia parlando di tutti i casi senza eccezioni, non solo del caso più comune. (E a proposito: le persone scrivono ancora i parser a mano. Ad esempio, il parser CPython è ancora scritto a mano, IIRC.)
nikie

8

Sono sopravvissuto all'incubo noto come la massiccia macchina a stati finiti manipolata da enormi dichiarazioni di commutazione. Ancora peggio, nel mio caso, l'FSM comprendeva tre DLL C ++ ed era abbastanza chiaro che il codice era stato scritto da qualcuno esperto in C.

Le metriche di cui devi preoccuparti sono:

  • Velocità di fare un cambiamento
  • Velocità nel trovare il problema quando succede

Mi è stato assegnato il compito di aggiungere una nuova funzionalità a quel set di DLL e sono stato in grado di convincere il management che mi ci vorrebbe tanto tempo per riscrivere le 3 DLL come una DLL correttamente orientata agli oggetti come sarebbe per me patch di scimmia e la giuria rig la soluzione in ciò che era già lì. La riscrittura è stata un enorme successo, in quanto non solo supportava le nuove funzionalità, ma era molto più facile da estendere. In effetti, un'attività che normalmente richiederebbe una settimana per assicurarsi che non si rompa nulla finirebbe per richiedere alcune ore.

E i tempi di esecuzione? Non c'è stato aumento o diminuzione della velocità. Ad essere onesti, le nostre prestazioni sono state limitate dai driver di sistema, quindi se la soluzione orientata agli oggetti fosse effettivamente più lenta non lo sapremmo.

Cosa c'è di sbagliato nelle enormi istruzioni switch per un linguaggio OO?

  • Il flusso di controllo del programma viene rimosso dall'oggetto a cui appartiene e posizionato all'esterno dell'oggetto
  • Molti punti di controllo esterno si traducono in molti punti che è necessario rivedere
  • Non è chiaro dove sia memorizzato lo stato, in particolare se l'interruttore si trova all'interno di un loop
  • Il confronto più veloce non è affatto un confronto (puoi evitare la necessità di molti confronti con un buon design orientato agli oggetti)
  • È più efficiente scorrere tra i tuoi oggetti e chiamare sempre lo stesso metodo su tutti gli oggetti piuttosto che cambiare il tuo codice in base al tipo di oggetto o enum che codifica il tipo.

8

Non compro l'argomento performance; si tratta di manutenibilità del codice.

MA: a volte , un'istruzione switch gigante è più facile da mantenere (meno codice) di un gruppo di piccole classi che sovrascrivono le funzioni virtuali di una classe base astratta. Ad esempio, se dovessi implementare un emulatore di CPU, non implementeresti la funzionalità di ciascuna istruzione in una classe separata: la inseriresti semplicemente in uno swtich gigante sull'opcode, eventualmente chiamando le funzioni di aiuto per istruzioni più complesse.

Regola empirica: se l'interruttore viene in qualche modo eseguito sul TYPE, probabilmente dovresti usare l'ereditarietà e le funzioni virtuali. Se l'interruttore viene eseguito su un VALORE di un tipo fisso (ad es. Il codice operativo dell'istruzione, come sopra), è OK lasciarlo così com'è.


5

Non puoi convincermi che:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

È significativamente più veloce di:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

Inoltre, la versione OO è solo più gestibile.


8
Per alcune cose e per piccole quantità di azioni, la versione OO è molto più sciocca. Deve avere un qualche tipo di fabbrica per convertire un valore nella creazione di una IAction. In molti casi è molto più leggibile semplicemente attivare quel valore.
Zan Lynx,

@Zan Lynx: il tuo argomento è troppo generico. La creazione dell'oggetto IAction è tanto difficile quanto il recupero dell'intero dell'azione non più difficile, né più facile. Quindi possiamo avere una vera conversazione senza essere troppo generici. Prendi in considerazione una calcolatrice. Qual è la differenza di complessità qui? La risposta è zero Come tutte le azioni pre-create. Ottieni l'input dall'utente ed è già un'azione.
Martin York,

3
@Martin: stai assumendo un'app calcolatrice GUI. Prendiamo invece un'app calcolatrice per tastiera scritta per C ++ su un sistema incorporato. Ora hai un intero di codice di scansione da un registro hardware. Cosa è meno complesso?
Zan Lynx,

2
@Martin: Non vedi come intero -> tabella di ricerca -> creazione di un nuovo oggetto -> la funzione di chiamata virtuale è più complicata dell'intero -> interruttore -> funzione? Come non lo vedi?
Zan Lynx,

2
@Martin: Forse lo farò. Nel frattempo, spiega come ottenere l'oggetto IAction per richiamare action () da un numero intero senza una tabella di ricerca.
Zan Lynx,

4

Ha ragione sul fatto che il codice macchina risultante sarà probabilmente più efficiente. Il compilatore essenziale trasforma un'istruzione switch in una serie di test e rami, che saranno relativamente poche istruzioni. È molto probabile che il codice risultante da approcci più astratti richieda più istruzioni.

TUTTAVIA : È quasi certamente il caso che la tua particolare applicazione non debba preoccuparsi di questo tipo di micro-ottimizzazione, altrimenti non utilizzeresti .net. Per le applicazioni embedded molto limitate o per il lavoro intenso della CPU dovresti sempre lasciare che il compilatore gestisca l'ottimizzazione. Concentrati sulla scrittura di codice pulito e gestibile. Questo ha quasi sempre un grande valore rispetto a qualche decimo di nanosecondo in termini di tempo di esecuzione.


3

Uno dei motivi principali per utilizzare le classi anziché le istruzioni switch è che le istruzioni switch tendono a portare a un enorme file con molta logica. Questo è sia un incubo di manutenzione che un problema con la gestione dei sorgenti poiché devi estrarre e modificare quel file enorme invece di un diverso file di classe più piccolo


3

un'istruzione switch nel codice OOP è una forte indicazione delle classi mancanti

provalo in entrambi i modi ed esegui alcuni semplici test di velocità; è probabile che la differenza non sia significativa. Se lo sono e il codice è critico in termini di tempo, mantenere l'istruzione switch


3

Normalmente odio la parola "ottimizzazione prematura", ma questo puzza. Vale la pena notare che Knuth ha usato questa famosa citazione nel contesto del push per usare le gotoistruzioni al fine di accelerare il codice in aree critiche . Questa è la chiave: percorsi critici .

Stava suggerendo di usare gotoper accelerare il codice ma avvertendo contro quei programmatori che vorrebbero fare questo tipo di cose sulla base di intuizioni e superstizioni per il codice che non è nemmeno critico.

Favorire le switchdichiarazioni il più possibile uniformemente in una base di codice (indipendentemente dal fatto che venga gestito o meno un carico pesante) è il classico esempio di ciò che Knuth chiama il programmatore "scemo e sciocco" che trascorre tutto il giorno a lottare per mantenere il proprio "ottimizzato "codice che si è trasformato in un incubo di debug a seguito del tentativo di risparmiare centesimi di sterline. Tale codice è raramente gestibile e tanto meno efficiente anche in primo luogo.

Ha ragione?

Ha ragione dal punto di vista dell'efficienza di base. Nessun compilatore che io sappia può ottimizzare il codice polimorfico che coinvolge oggetti e invio dinamico meglio di un'istruzione switch. Non finirai mai con una LUT o salti la tabella al codice incorporato dal codice polimorfico, poiché tale codice tende a fungere da barriera di ottimizzazione per il compilatore (non saprà quale funzione chiamare fino al momento in cui il dispacciamento dinamico si verifica).

È più utile non pensare a questo costo in termini di tabelle di salto, ma più in termini di barriera di ottimizzazione. Per il polimorfismo, la chiamata Base.method()non consente al compilatore di sapere quale funzione finirà per essere chiamata se methodè virtuale, non sigillata e può essere ignorata. Dal momento che non sa quale funzione verrà effettivamente chiamata in anticipo, non può ottimizzare la chiamata di funzione e utilizzare più informazioni nel prendere decisioni di ottimizzazione, dal momento che in realtà non sa quale funzione verrà chiamata in l'ora in cui il codice viene compilato.

Gli ottimizzatori sono al meglio quando possono scrutare una chiamata di funzione ed effettuare ottimizzazioni che appiattiscono completamente il chiamante e chiamano, o almeno ottimizzano il chiamante per lavorare in modo più efficiente con il chiamante. Non possono farlo se non sanno quale funzione verrà effettivamente chiamata in anticipo.

Sta solo parlando il culo?

L'uso di questo costo, che spesso equivale a centesimi, per giustificare la trasformazione in uno standard di codifica applicato in modo uniforme è generalmente molto sciocco, soprattutto per i luoghi che hanno un bisogno di estensibilità. Questa è la cosa principale da tenere d'occhio con i veri ottimizzatori prematuri: vogliono trasformare i problemi di prestazioni minori in standard di codifica applicati uniformemente in una base di codice, senza riguardo per la manutenibilità di sorta.

Mi offendo un po 'la citazione del "vecchio hacker C" usata nella risposta accettata, dato che sono una di quelle. Non tutti coloro che hanno codificato per decenni a partire da hardware molto limitato si sono trasformati in un ottimizzatore prematuro. Eppure ho incontrato e lavorato anche con quelli. Ma quei tipi non misurano mai cose come errori di filiale o mancati riscontri nella cache, pensano di conoscerli meglio e basano le loro nozioni di inefficienza in una complessa base di codice di produzione basata su superstizioni che non valgono oggi e talvolta non sono mai state vere. Le persone che hanno davvero lavorato in settori critici per le prestazioni spesso capiscono che l'ottimizzazione efficace è un'efficace definizione delle priorità, e cercare di generalizzare uno standard di codifica degradante della manutenibilità per risparmiare centesimi è un'assegnazione delle priorità molto inefficace.

I penny sono importanti quando si ha una funzione economica che non fa così tanto lavoro, che viene chiamata un miliardo di volte in un ciclo molto stretto e critico per le prestazioni. In tal caso, finiamo per risparmiare 10 milioni di dollari. Non vale la pena radere centesimi quando hai una funzione chiamata due volte per la quale il solo corpo costa migliaia di dollari. Non è saggio passare il tempo a contrattare sui penny durante l'acquisto di un'auto. Vale la pena contrattare per centesimi se si acquista un milione di lattine di soda da un produttore. La chiave per un'ottimizzazione efficace è comprendere questi costi nel loro giusto contesto. Qualcuno che cerca di risparmiare centesimi su ogni singolo acquisto e suggerisce che tutti gli altri cercano di contrattare sui penny, indipendentemente da ciò che stanno acquistando, non è un abile ottimizzatore.


2

Sembra che il tuo collega sia molto preoccupato per le prestazioni. Potrebbe essere che in alcuni casi una grande struttura case / switch funzionerà più velocemente, ma speriamo che voi ragazzi facciate un esperimento facendo test di cronometraggio sulla versione OO e sulla versione switch / case. Immagino che la versione di OO abbia meno codice ed è più facile da seguire, comprendere e mantenere. Discuterei prima per la versione OO (poiché la manutenzione / leggibilità dovrebbe essere inizialmente più importante) e prenderei in considerazione la versione switch / case solo se la versione OO ha seri problemi di prestazioni e si può dimostrare che un switch / case farà un miglioramento significativo.


1
Insieme ai test di temporizzazione, un dump del codice può aiutare a mostrare come funziona il dispacciamento del metodo C ++ (e C #).
S.Lott

2

Un vantaggio di manutenibilità del polimorfismo che nessuno ha menzionato è che sarai in grado di strutturare il tuo codice in modo molto più corretto usando l'ereditarietà se passi sempre sullo stesso elenco di casi, ma a volte diversi casi vengono gestiti allo stesso modo e a volte non sono

Per esempio. se si passa tra Dog, Cate Elephant, e, talvolta, Doge Catha lo stesso caso, entrambi ereditare da una classe astratta si può fare DomesticAnimale mettere quelli funzione nella classe astratta.

Inoltre, sono rimasto sorpreso dal fatto che diverse persone abbiano usato un parser come esempio di dove non avresti usato il polimorfismo. Per un parser simile ad un albero questo è sicuramente l'approccio sbagliato, ma se hai qualcosa come l'assemblaggio, in cui ogni riga è in qualche modo indipendente e inizi con un codice operativo che indica come interpretare il resto della riga, utilizzerei totalmente il polimorfismo e una fabbrica. Ogni classe può implementare funzioni come ExtractConstantso ExtractSymbols. Ho usato questo approccio per un interprete BASIC giocattolo.


Un interruttore può anche ereditare comportamenti, attraverso il suo caso predefinito. "... estende BaseOperationVisitor" diventa "default: BaseOperation (nodo)"
Samuel Danielson

0

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

Donald Knuth


0

Anche se questo non è male per la manutenibilità, non credo che sarà migliore per le prestazioni. Una chiamata di funzione virtuale è semplicemente un'ulteriore indiretta (lo stesso del caso migliore per un'istruzione switch), quindi anche in C ++ le prestazioni dovrebbero essere approssimativamente uguali. In C #, dove tutte le chiamate di funzione sono virtuali, l'istruzione switch dovrebbe essere peggiore, poiché si ha lo stesso overhead di chiamata di funzione virtuale in entrambe le versioni.


1
Manca "non"? In C #, non tutte le chiamate di funzione sono virtuali. C # non è Java.
Ben Voigt,

0

Il tuo collega non sta parlando dal suo retro, per quanto riguarda il commento relativo ai tavoli da salto. Tuttavia, usarlo per giustificare la scrittura di codice errato è dove sbaglia.

Il compilatore C # converte le istruzioni switch con pochi casi in una serie di if / else, quindi non è più veloce dell'uso di if / else. Il compilatore converte le istruzioni switch più grandi in un dizionario (la tabella di salto a cui fa riferimento il collega). Per ulteriori dettagli, vedere questa risposta a una domanda di overflow dello stack sull'argomento .

Un'istruzione switch di grandi dimensioni è difficile da leggere e mantenere. Un dizionario di "casi" e funzioni è molto più facile da leggere. Poiché questo è ciò in cui viene trasformato l'interruttore, tu e il tuo collega vi consigliamo di usare direttamente i dizionari.


0

Non sta necessariamente parlando dal suo culo. Almeno nelle switchistruzioni C e C ++ può essere ottimizzato per saltare le tabelle mentre non l'ho mai visto accadere con un dispacciamento dinamico in una funzione che ha accesso solo a un puntatore di base. Per lo meno, quest'ultimo richiede un ottimizzatore molto più intelligente che esamina molto più codice circostante per capire esattamente quale sottotipo viene utilizzato da una chiamata di funzione virtuale attraverso un puntatore / riferimento di base.

Inoltre, l'invio dinamico funge spesso da "barriera di ottimizzazione", il che significa che il compilatore spesso non sarà in grado di incorporare il codice e allocare in modo ottimale i registri per ridurre al minimo gli sversamenti di stack e tutte le cose fantasiose, dal momento che non riesce a capire cosa la funzione virtuale verrà chiamata tramite il puntatore di base per incorporarla e fare tutta la sua magia di ottimizzazione. Non sono sicuro che tu voglia anche che l'ottimizzatore sia così intelligente e cerchi di ottimizzare le chiamate di funzione indiretta, dal momento che ciò potrebbe potenzialmente portare a molti rami di codice che devono essere generati separatamente in un determinato stack di chiamate (una funzione che le chiamate foo->f()avrebbero per generare un codice macchina totalmente diverso da quello che chiamabar->f() attraverso un puntatore di base, e la funzione che chiama quella funzione dovrebbe quindi generare due o più versioni di codice, e così via - la quantità di codice macchina generato sarebbe esplosiva - forse non così male con una traccia JIT che genera il codice al volo mentre viene tracciato attraverso percorsi di esecuzione a caldo).

Tuttavia, poiché molte risposte hanno fatto eco, questa è una cattiva ragione per favorire un carico di switchaffermazioni, anche se è più rapido di qualche importo marginale. Inoltre, quando si tratta di micro-efficienze, cose come la ramificazione e l'inline hanno di solito una priorità piuttosto bassa rispetto a cose come i modelli di accesso alla memoria.

Detto questo, sono saltato qui con una risposta insolita. Voglio fare un caso per la manutenibilità delle switchaffermazioni su una soluzione polimorfica quando, e solo quando, sai per certo che ci sarà solo un posto che deve eseguire switch.

Un primo esempio è un gestore di eventi centrale. In quel caso in genere non ci sono molti posti in cui si svolgono eventi, solo uno (perché è "centrale"). In questi casi, non beneficiate dell'estensibilità fornita da una soluzione polimorfica. Una soluzione polimorfica è utile quando ci sono molti posti che farebbero la switchdichiarazione analogica . Se sai per certo che ce ne sarà solo uno, switchun'istruzione con 15 casi può essere molto più semplice della progettazione di una classe base ereditata da 15 sottotipi con funzioni sovrascritte e una fabbrica per istanziarli, solo per poi essere utilizzata in una funzione nell'intero sistema. In questi casi, l'aggiunta di un nuovo sottotipo è molto più noiosa dell'aggiunta di caseun'istruzione a una funzione. Semmai, direi per la manutenibilità, non per le prestazioni,switch dichiarazioni in questo caso singolare in cui non beneficiate di estensibilità di sorta.

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.