leggibilità rispetto a codice più breve nel ritorno dalla funzione [chiusa]

4

In qualcosa di semplice come

int sq(int x) { int y = x*x; return y; }

vs

int sq(int x) { return (x*x); }

la funzione precedente richiede un passaggio IL aggiuntivo.

EDIT: codice IL del primo esempio, utilizzando il compilatore C # corrente da VS 2013 Update 4 (.NET Framework 4.5.1), codice dalla versione di versione ottimizzata:

  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0   // These two instructions
  IL_0004:  ldloc.0   // seem to be unneccessary
  IL_0005:  ret

e dal secondo esempio:

  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  ret

Quel passo aggiuntivo di IL è indicativo di dati copiati in memoria? Questa differenza è ancora presente quando si utilizza qualcosa apparentemente meglio ottimizzato? (C?)

Se si prima o entrambi: posso usare quest'ultimo, specialmente quando i dati restituiti richiedono molto spazio e presumibilmente un sacco di tempo da spostare in memoria? O dovrei favorire il primo, perché è più leggibile? LINQ semplifica l'uso di intere funzioni all'interno di una linea long return (), ma il mio esempio si trova di fronte alla stessa scelta: C. Velocità o leggibilità?

In generale, inseriresti l'ultimo bit di matematica tra parentesi nella linea di ritorno?

    
posta Nick Alexander 21.12.2014 - 03:57
fonte

4 risposte

13

Prima di tutto, le micro-ottimizzazioni come questa raramente hanno senso. Dovresti concentrarti sulla leggibilità e preoccuparti di questi tipi di ottimizzazioni solo quando hai identificato il codice che devi effettivamente ottimizzare mediante il profiling.

Ora, se sei ancora interessato alle prestazioni, guardare a IL non ha molto senso, perché non è IL che viene eseguito.

Invece, ciò che dovresti fare è misurare .

Un'altra opzione sarebbe guardare il codice macchina generato (di solito x86). Ma il problema è che le CPU sono molto complicate e può essere molto difficile capire quale versione del codice sarà più veloce.

Nota: osservare il codice macchina non è semplice come aprire la finestra Disassembly. È necessario eseguire la modalità Release e accertarsi che CLR ottimizzi effettivamente il codice (deselezionando "Supreme l'ottimizzazione JIT sul caricamento del modulo" o avviando senza il debugger collegato: Ctrl + F5 , e allegandolo in seguito, ad es. Usando Debugger.Break() e selezionando per eseguire il debug del codice).

Se lo fai, molto probabilmente noterai che entrambe le versioni del metodo saranno in linea, il che rende difficile vedere quali istruzioni appartengono al tuo metodo (ma entrambe le versioni generano lo stesso codice macchina per me).

Se vuoi confrontare le due versioni del metodo in isolamento, puoi usare [MethodImpl(MethodImplOptions.NoInlining)] . Con questo, il codice che sto ottenendo per entrambe le versioni del metodo è lo stesso:

push        ebp  
mov         ebp,esp  
mov         eax,edx  
imul        eax,edx  
pop         ebp  
ret  
    
risposta data 21.12.2014 - 05:21
fonte
16

Ogni compilatore che si rispetti da solo compilerà entrambi gli esempi con lo stesso IL. Inoltre, il primo non è più leggibile, dal momento che si dispone di una variabile garbage senza informazioni. È solo rumore che diminuisce leggibilità. Se quel temporaneo aveva effettivamente un buon nome che forniva al lettore alcune informazioni utili, allora il primo modo sarebbe (di solito) migliore.

Ma in generale, preferisci la leggibilità alla velocità (che è solo appena correlato alla lunghezza del codice).

    
risposta data 21.12.2014 - 04:09
fonte
4

L'IL che stai guardando non è necessariamente indicativo del codice nativo che il compilatore JIT genererà in fase di esecuzione (o che genererà ngen se precompilerai i tuoi binari nativi).

L'IL in questo caso è ovviamente un po 'rappresentativo del codice originale.

EDIT: i commenti postati dopo che l'OP ha pubblicato l'IL e lo smontaggio ha notato che l'assembly è un build di debug, quindi le ottimizzazioni sono disattivate e il JIT probabilmente ottimizzerà questo aspetto se compilerai una versione di rilascio.

EDIT: E sono assolutamente d'accordo con i commenti sul fatto che il debugging della variante "assegna alla variabile, restituisce la variabile" sia più semplice, per il semplice fatto che puoi controllare il risultato del compito prima del restituisce il metodo.

Tuttavia, ogni compilatore (C #, VB.NET, FORTRAN, COBOL, F #, ecc.) è una bestia diversa, trasformando quelle lingue in un comune IL, che viene poi trasformato dal JIT nel codice nativo per la CPU il CLR si trova in esecuzione.

IL è l'acronimo di "Intermediate Language", dopotutto, non per "Fully Optimized Native Machine Language". Immagino che il ritorno sull'investimento dalla creazione di IL completamente ottimizzato quando il JIT stesso sia già un compilatore ottimizzante potrebbe non valerne la pena nella maggior parte dei casi.

Inoltre, potrebbe essere interessante pubblicare l'IL effettivo da ciascun esempio.

Credo che non mi preoccuperei veramente di questo.

Probabilmente è un evento raro che stai andando a superare in astuzia le ottimizzazioni integrate nel compilatore. Quelle ottimizzazioni sono ciò che quei ragazzi (e / o ragazze) si stanno concentrando su tutto il tempo.

Mi preoccuperei di più del codice corretto, leggibile dall'uomo, facile da eseguire il debug e della regola 80/20. L'80% dei problemi di rendimento sarà nel 20% del codice. In realtà, probabilmente è più come 95/5.

Verifica il tuo codice. Esegui test e cronometrali, quindi ottimizza le parti che sono effettivamente problematiche. Scommetto che ti pranzi che i bit del problema sono spesso in posti che non ti aspetti, e un sacco di codice che pensavi sarebbe stato un problema di prestazioni non sarà, perché funziona raramente o non è in un percorso super-critico. E scommetterò che il JIT ottimizza via qualunque cosa tu sia preoccupata con questo esempio, comunque. ; -)

    
risposta data 21.12.2014 - 08:26
fonte
1

Sfiderò direttamente che il primo esempio è più leggibile. In questo codice, è molto chiaro cosa sta succedendo, prendi un int x e restituisci x*x .

int sq(int x) {
    return (x*x);
}

L'aggiunta di un'istruzione extra ( y=x*x; return y ) non aggiunge nulla alla leggibilità, e in effetti mi fa pensare a cosa potrei mancare (ad esempio, c'erano linee che hai rimosso), perché stai assegnando una nuova variabile.

Tuttavia, nell'argomento generale "leggibilità e velocità" suggerirei che la leggibilità supera tutti gli altri. Nella maggior parte dei casi, il tempo del programmatore è molto più costoso del calcolo o della memorizzazione. CodeGolf.StackExchange è un esempio esemplare di questo, poiché è riempito con molto di soluzioni molto veloci e molto intelligenti ai problemi, ma il tempo di capire realmente come funzionano alcuni di loro è abbastanza lungo.

"Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you have proven that's where the bottleneck is." — Rob Pike

Le ottimizzazioni nel codice per migliorare le prestazioni del bytecode dovrebbero essere considerate solo dopo che ogni altra parte del codice è stata profilata per identificare potenziali colli di bottiglia . Se l'ottimizzazione del tuo bytecode rende un'operazione un po 'ingenua O(n!) un po' più veloce per ogni n , trovare una soluzione O(n^2) nel codice ti garantisce risultati molto migliori.

Anche allora, quando inizi a studiare l'operazione bytecode per spremere le prestazioni extra, il costo di aggiungere un core extra a un server o di acquistare un po 'di tempo in più sul server potrebbe essere minimo rispetto al tempo richiesto per le modifiche al codice e le indagini per una soluzione ottimale per bytecode - la ragione per cui dico che se una soluzione ottimale per bytecode fosse banalmente semplice, sarebbe già stata incorporata nel compilatore / interprete.

    
risposta data 22.12.2014 - 04:16
fonte

Leggi altre domande sui tag