Domanda riguardante la leggibilità rispetto al tempo di elaborazione

0

Sto creando un diagramma di flusso per un programma con più passaggi sequenziali. Ogni passaggio dovrebbe essere eseguito se il passaggio precedente ha esito positivo. Io uso un linguaggio di programmazione basato su c in modo che il lay-out sia simile a questo:

METODO 1:

if(step_one_succeeded())
{
    if(step_two_succeeded())
    {
        if(step_three_succeeded())
        {
            //etc. etc.
        }   
    }
}

Se il mio programma avesse più di 15 passaggi, il codice risultante sarebbe terribilmente ostile da leggere. Così ho cambiato il mio design e implementato un errorcode globale che continuo a passare per riferimento, rendere tutto più leggibile. Il codice risultante sarebbe qualcosa del tipo:

METODO 2:

int _no_error = 0;

step_one(_no_error);
if(_no_error == 0) step_two(_no_error);
if(_no_error == 0) step_three(_no_error);
if(_no_error == 0) step_two(_no_error);

La complessità ciclomatica rimane la stessa. Ora diciamo che ci sono N numero di passi. E supponiamo che il controllo di una condizione sia lungo 1 orologio e che l'esecuzione di un passo non richieda tempo.

La velocità di elaborazione di Method1 può essere ovunque tra 1 e N.

La velocità di elaborazione di Method2 tuttavia è sempre uguale a N-1.

Quindi Method1 sarà più veloce la maggior parte del tempo. Il che mi porta alla mia domanda, è una cattiva pratica sacrificare il tempo per rendere il codice più leggibile? E perché (non)?

    
posta Jordy 24.06.2013 - 13:34
fonte

5 risposte

12

Sacrificare la leggibilità del codice per risparmiare alcuni cicli di clock è un chiaro segnale di ottimizzazione prematura.

Il tuo primo obiettivo dovrebbe essere sempre quello di scrivere il codice più leggibile / manutenibile che dia prestazioni ragionevoli. Solo dopo che le misurazioni hanno indicato che i test N condition rappresentano effettivamente un collo di bottiglia per le prestazioni complessive e non ci sono ottimizzazioni possibili che potrebbero produrre un effetto più ampio (come l'utilizzo di un algoritmo diverso), quindi potresti prendere in considerazione la possibilità di sacrificare la leggibilità per ottenere le prestazioni richieste.

Anche se ti trovi nella situazione estremamente rara che stai scrivendo una libreria che deve sempre dare le massime prestazioni, dovresti considerare attentamente se la perdita di leggibilità vale poche istruzioni e se un buon compilatore di ottimizzazione non genererebbe lo stesso codice per entrambi comunque.

    
risposta data 24.06.2013 - 13:49
fonte
9

And let's assume that checking a condition is 1 clock long and performing a step doesn't take up time.

No.

Non facciamo supposizioni che distorcano completamente la discussione. In realtà, i passaggi richiedono tempo, molto probabilmente molto più tempo rispetto a tutte le condizioni di controllo.

Il che significa che le considerazioni sulle prestazioni sono irrilevanti .

E questo è il motivo per cui l'ottimizzazione prematura è, in effetti, un cattivo anche quando le prestazioni sono molto importanti .

    
risposta data 24.06.2013 - 14:02
fonte
3

I compilatori di solito tagliano e tagliano e ricostruiscono completamente qualsiasi codice che compilano. Poiché il codice leggibile è più comune, le ottimizzazioni di solito sono ottimizzate per questo, in modo che il codice leggibile abbia effettivamente maggiori possibilità di essere ottimizzato dal compilatore.

Nota a margine, evita di tornare per riferimento. È più difficile da leggere e più difficile da ottimizzare per il compilatore. La variabile dovrebbe sempre essere impostata come _no_error = _no_error && step_one() (che, accidentalmente, agisce come condizione, quindi non ne hai bisogno, ma non è in linguaggio C ++ )

.

Infine, non considero molto più leggibile la _no_error (oltre al brutto nome di quella variabile, i simboli che iniziano con _ sono riservati per le estensioni della libreria). Un codice leggibile in qualsiasi linguaggio che supporti RAII (C ++, D, Rust) o equivalente (finalmente) (C #, Java) è:

if(!step_one())
    return;
if(!step_two())
    return;
...

Nella semplice vecchia C, è preferibile usare goto , se hai bisogno di una ripulitura, altrimenti puoi ovviamente usare anche return direttamente:

if(!step_one())
    goto failed;
if(!step_two())
    goto failed;
...
failed:
clean_up();
    
risposta data 24.06.2013 - 14:12
fonte
2

Se dici esattamente e concisamente ciò che desideri, il compilatore può produrre il codice più efficiente. Quasi tutte le semplici ottimizzazioni che facevamo a mano sono ora eseguite automaticamente dai compilatori.

Non hai bisogno di una variabile di stato. Ogni funzione restituisce true in caso di successo e false in caso di errore, quindi scrivi:

if (step_one()
    && step_two()
    && step_three()
    && ...) {
  /* success */
  ...
} else {
  /* failure */
  ...
}
...

Nella maggior parte dei linguaggi di programmazione imperativi, (C, Java, C #, Python, Perl, Ruby, LISP, ecc.) l'operatore e ( && o and ) valuta le clausole da sinistra a destra e si ferma non appena uno è falso.

    
risposta data 24.06.2013 - 16:12
fonte
1

Se hai molti passaggi e devi uscire non appena si fallisce, prendi in considerazione la possibilità di mettere i puntatori di funzione in un array ed eseguirli tutti, come questo:

// The array
int (*steps[2]) ();

// Assign functions
steps[0] = step_one;
steps[1] = step_two;

// Call them
for (int i = 0; i < 2; i++) {
     if (!(*steps[i])()) 
         break;
}

Questo approccio ti consente di configurare durante il tempo di esecuzione in quale ordine devi eseguirli.

    
risposta data 24.06.2013 - 15:38
fonte

Leggi altre domande sui tag