Quando ottimizzare la memoria rispetto alla velocità delle prestazioni per un metodo?


107

Di recente ho intervistato su Amazon. Durante una sessione di programmazione, l'intervistatore mi ha chiesto perché ho dichiarato una variabile in un metodo. Ho spiegato il mio processo e mi ha sfidato a risolvere lo stesso problema con meno variabili. Ad esempio (questo non proviene dall'intervista), ho iniziato con il metodo A, quindi l' ho migliorato con il metodo B, rimuovendolo int s. Era contento e disse che questo avrebbe ridotto l'utilizzo della memoria con questo metodo.

Capisco la logica che sta dietro, ma la mia domanda è:

Quando è appropriato utilizzare il metodo A rispetto al metodo B e viceversa?

Puoi vedere che il Metodo A avrà un uso maggiore della memoria, poiché int sè dichiarato, ma deve eseguire solo un calcolo, ad es a + b. D'altra parte, il metodo B ha un utilizzo della memoria inferiore, ma deve eseguire due calcoli, cioè a + bdue volte. Quando uso una tecnica sull'altra? Oppure, una delle tecniche è sempre preferita all'altra? Quali sono gli aspetti da considerare nella valutazione dei due metodi?

Metodo A:

private bool IsSumInRange(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

Metodo B:

private bool IsSumInRange(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

229
Sono disposto a scommettere che un compilatore moderno genererà lo stesso assembly per entrambi i casi.
17 del 26

12
Ho ripristinato la domanda allo stato originale, poiché la tua modifica ha invalidato la mia risposta - per favore non farlo! Se fai una domanda su come migliorare il tuo codice, allora non cambiare la domanda migliorando il codice nel modo mostrato - questo fa sembrare le risposte insignificanti.
Doc Brown,

76
Aspetta un secondo, hanno chiesto di sbarazzarsi di int sessere totalmente a posto con quei numeri magici per i limiti superiore e inferiore?
null

34
Ricorda: profilo prima dell'ottimizzazione. Con i compilatori moderni, il metodo A e il metodo B possono essere ottimizzati con lo stesso codice (utilizzando livelli di ottimizzazione più elevati). Inoltre, con i moderni processori, potrebbero avere istruzioni che eseguono più dell'aggiunta in un'unica operazione.
Thomas Matthews,

142
Nessuno dei due; ottimizzare per la leggibilità.
Andy,

Risposte:


148

Invece di speculare su ciò che può o non può accadere, diamo un'occhiata, vero? Dovrò usare C ++ poiché non ho un compilatore C # a portata di mano (anche se vedi l'esempio C # da VisualMelon ), ma sono sicuro che gli stessi principi si applicano indipendentemente.

Includeremo le due alternative che hai incontrato nell'intervista. Includeremo anche una versione che utilizza abscome suggerito da alcune delle risposte.

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

Ora compilalo senza alcuna ottimizzazione: g++ -c -o test.o test.cpp

Ora possiamo vedere esattamente cosa genera: objdump -d test.o

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

Possiamo vedere dagli indirizzi dello stack (ad esempio, -0x4in mov %edi,-0x4(%rbp)rispetto a -0x14in mov %edi,-0x14(%rbp)) che IsSumInRangeWithVar()utilizza 16 byte extra nello stack.

Poiché IsSumInRangeWithoutVar()non alloca spazio nello stack per archiviare il valore intermedio s, è necessario ricalcolarlo, facendo sì che questa implementazione sia più lunga di 2 istruzioni.

Divertente, IsSumInRangeSuperOptimized()assomiglia molto IsSumInRangeWithoutVar(), tranne che confronta -1000 prima e 1000 secondi.

Ora cerchiamo di compilare solo con le ottimizzazioni più basilari: g++ -O1 -c -o test.o test.cpp. Il risultato:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

Lo guarderesti: ogni variante è identica . Il compilatore è in grado di fare qualcosa di abbastanza intelligente: abs(a + b) <= 1000equivale a a + b + 1000 <= 2000considerare setbeun confronto senza segno, quindi un numero negativo diventa un numero positivo molto grande. L' leaistruzione può effettivamente eseguire tutte queste aggiunte in un'unica istruzione ed eliminare tutti i rami condizionali.

Per rispondere alla tua domanda, quasi sempre la cosa da ottimizzare non è la memoria o la velocità, ma la leggibilità . Leggere il codice è molto più difficile che scriverlo, e leggere il codice che è stato modificato per "ottimizzare" è molto più difficile che leggere il codice che è stato scritto per essere chiaro. Il più delle volte, queste "ottimizzazioni" hanno un impatto trascurabile, o come in questo caso esattamente zero impatto effettivo sulle prestazioni.


Domanda di follow-up, cosa cambia quando questo codice è in una lingua interpretata anziché compilata? Quindi, l'ottimizzazione è importante o ha lo stesso risultato?

Misuriamo! Ho trascritto gli esempi in Python:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Esegui con Python 3.5.2, questo produce l'output:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

Lo smontaggio in Python non è terribilmente interessante, dal momento che il "compilatore" bytecode non fa molto in termini di ottimizzazione.

Le prestazioni delle tre funzioni sono quasi identiche. Potremmo essere tentati di seguirlo a IsSumInRangeWithVar()causa del suo guadagno di velocità marginale. Anche se aggiungerò mentre stavo provando parametri diversi timeit, a volte IsSumInRangeSuperOptimized()sono uscito più velocemente, quindi sospetto che possano essere fattori esterni responsabili della differenza, piuttosto che qualsiasi vantaggio intrinseco di qualsiasi implementazione.

Se questo è davvero un codice critico per le prestazioni, un linguaggio interpretato è semplicemente una scelta molto scadente. Eseguendo lo stesso programma con pypy, ottengo:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

Il solo utilizzo di pypy, che utilizza la compilazione JIT per eliminare molte delle spese generali dell'interprete, ha prodotto un miglioramento delle prestazioni di 1 o 2 ordini di grandezza. Sono rimasto piuttosto scioccato nel vedere IsSumInRangeWithVar()un ordine di grandezza più veloce degli altri. Quindi ho cambiato l'ordine dei benchmark e ho eseguito di nuovo:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

Quindi sembra che in realtà non sia nulla sull'implementazione che lo rende veloce, ma piuttosto l'ordine in cui eseguo il benchmarking!

Mi piacerebbe approfondire questo aspetto più profondamente, perché onestamente non so perché questo accada. Ma credo che il punto sia stato sollevato: le microottimizzazioni come se dichiarare un valore intermedio come variabile o meno sono raramente rilevanti. Con un linguaggio interpretato o un compilatore altamente ottimizzato, il primo obiettivo è ancora quello di scrivere un codice chiaro.

Se potrebbe essere necessaria un'ulteriore ottimizzazione, benchmark . Ricorda che le migliori ottimizzazioni non provengono dai piccoli dettagli ma dal più grande quadro algoritmico: pypy sarà un ordine di grandezza più veloce per una valutazione ripetuta della stessa funzione di cpython perché utilizza algoritmi più veloci (compilatore JIT vs interpretazione) per valutare programma. E c'è anche l'algoritmo codificato da considerare: una ricerca attraverso un albero B sarà più veloce di un elenco collegato.

Dopo essersi assicurati che si sta utilizzando gli strumenti giusti e gli algoritmi per il lavoro, essere pronti a tuffarsi in profondità nei dettagli del sistema. I risultati possono essere molto sorprendenti, anche per sviluppatori esperti, ed è per questo che è necessario disporre di un benchmark per quantificare le modifiche.


6
Per fornire un esempio in C #: SharpLab produce un asm identico per entrambi i metodi (Desktop CLR v4.7.3130.00 (clr.dll) su x86)
VisualMelon,

2
@VisualMelon è abbastanza divertente il controllo positivo: "return (((a + b)> = -1000) && ((a + b) <= 1000));" dà un risultato diverso. : sharplab.io/…
Pieter B,

12
La leggibilità può potenzialmente rendere anche un programma più facile da ottimizzare. Il compilatore può facilmente riscrivere per usare una logica equivalente come sopra, solo se riesce davvero a capire cosa stai cercando di fare. Se usi molti bithack della vecchia scuola , fai avanti e indietro tra ints e puntatori, riutilizzi l'archiviazione mutabile ecc., Potrebbe essere molto più difficile per il compilatore dimostrare che una trasformazione è equivalente e lascerà semplicemente ciò che hai scritto , che può essere non ottimale.
Leushenko,

1
@Corey vedi modifica.
Phil Frost,

2
@Corey: questa risposta in realtà ti dice esattamente cosa ho scritto nella mia risposta: non c'è differenza quando usi un compilatore decente, e invece ti concentri sulla leggibilità. Certo, sembra meglio fondato - forse mi credi ora.
Doc Brown,

67

Per rispondere alla domanda dichiarata:

Quando ottimizzare la memoria rispetto alla velocità delle prestazioni per un metodo?

Ci sono due cose che devi stabilire:

  • Cosa sta limitando la tua applicazione?
  • Dove posso recuperare la maggior parte di quella risorsa?

Per rispondere alla prima domanda, devi sapere quali sono i requisiti di prestazione per la tua applicazione. Se non ci sono requisiti prestazionali, non c'è motivo di ottimizzare in un modo o nell'altro. I requisiti di prestazione ti aiutano a raggiungere il posto di "abbastanza buono".

Il metodo che hai fornito da solo non causerebbe problemi di prestazioni da solo, ma forse all'interno di un ciclo e l'elaborazione di una grande quantità di dati, devi iniziare a pensare in modo leggermente diverso su come stai affrontando il problema.

Rilevare cosa sta limitando l'applicazione

Inizia a guardare il comportamento della tua applicazione con un monitor delle prestazioni. Tieni d'occhio CPU, disco, rete e utilizzo della memoria mentre è in esecuzione. Uno o più oggetti verranno massimizzati mentre tutto il resto viene utilizzato moderatamente, a meno che non si raggiunga il perfetto equilibrio, ma ciò non accade quasi mai).

Quando hai bisogno di guardare più in profondità, in genere dovresti usare un profiler . Esistono profili di memoria e profili di processo e misurano cose diverse. L'atto della profilazione ha un impatto significativo sulle prestazioni, ma stai strumentando il tuo codice per scoprire cosa non va.

Diciamo che vedi il picco dell'utilizzo della CPU e del disco. Innanzitutto verifichi la presenza di "hot spot" o codice che viene chiamato più spesso degli altri o richiede una percentuale significativamente più lunga di elaborazione.

Se non riesci a trovare alcun punto attivo, inizieresti a guardare la memoria. Forse stai creando più oggetti del necessario e la tua garbage collection sta facendo gli straordinari.

Prestazioni di recupero

Pensa in modo critico. Il seguente elenco di modifiche è in ordine di quanto ritorno sull'investimento otterrai:

  • Architettura: cercare i punti di strozzamento della comunicazione
  • Algoritmo: potrebbe essere necessario modificare il modo in cui elaborate i dati
  • Hot spot: minimizzare la frequenza con cui si chiama il hot spot può produrre un grande bonus
  • Micro ottimizzazioni: non è comune, ma a volte devi davvero pensare a piccole modifiche (come nell'esempio che hai fornito), in particolare se è un hot spot nel tuo codice.

In situazioni come questa, devi applicare il metodo scientifico. Elaborare un'ipotesi, apportare le modifiche e testarla. Se raggiungi i tuoi obiettivi prestazionali, hai finito. In caso contrario, vai alla prossima cosa nell'elenco.


Rispondere alla domanda in grassetto:

Quando è appropriato utilizzare il metodo A rispetto al metodo B e viceversa?

Onestamente, questo è l'ultimo passo per cercare di gestire i problemi di prestazioni o di memoria. L'impatto del metodo A rispetto al metodo B sarà molto diverso a seconda della lingua e della piattaforma (in alcuni casi).

Quasi ogni linguaggio compilato con un ottimizzatore decente a metà strada genererà un codice simile con una di queste strutture. Tuttavia, tali ipotesi non rimangono necessariamente vere nei linguaggi proprietari e di giocattoli che non hanno un ottimizzatore.

Precisamente, ciò avrà un impatto migliore a seconda che sumsi tratti di una variabile di stack o di una variabile di heap. Questa è una scelta di implementazione del linguaggio. In C, C ++ e Java, ad esempio, le primitive numeriche come an intsono variabili di stack per impostazione predefinita. Il tuo codice non ha più impatto sulla memoria assegnando a una variabile di stack di quanto avresti con un codice completamente incorporato.

Altre ottimizzazioni che è possibile trovare nelle librerie C (in particolare quelle più vecchie) in cui è possibile decidere tra la copia di una matrice bidimensionale prima o attraverso la prima è un'ottimizzazione dipendente dalla piattaforma. Richiede una certa conoscenza di come il chipset a cui si sta indirizzando ottimizzi al meglio l'accesso alla memoria. Ci sono sottili differenze tra le architetture.

In conclusione, l'ottimizzazione è una combinazione di arte e scienza. Richiede un po 'di pensiero critico, nonché un certo grado di flessibilità nel modo in cui affrontate il problema. Cerca grandi cose prima di incolpare piccole cose.


2
Questa risposta si concentra maggiormente sulla mia domanda e non viene coinvolta nei miei esempi di codifica, ovvero Metodo A e Metodo B.
Corey P

18
Sento che questa è la risposta generica a "Come si affrontano i colli di bottiglia delle prestazioni", ma sarebbe difficile trovare un utilizzo relativo della memoria da una particolare funzione in base al fatto che avesse 4 o 5 variabili usando questo metodo. Mi chiedo anche quanto sia rilevante questo livello di ottimizzazione quando il compilatore (o l'interprete) può o meno ottimizzarlo.
Eric

@Eric, come ho già detto, l'ultima categoria di miglioramento delle prestazioni sarebbe la tua micro-ottimizzazione. L'unico modo per indovinare se avrà qualche impatto è misurare le prestazioni / la memoria in un profiler. È raro che questi tipi di miglioramenti abbiano un payoff, ma nei problemi di prestazioni sensibili ai tempi che hai nei simulatori un paio di cambiamenti ben piazzati come quello può essere la differenza tra colpire il tuo obiettivo di tempismo e non. Penso di poter contare da una parte sul numero di volte che è stato ripagato in oltre 20 anni di lavoro sul software, ma non è zero.
Berin Loritsch,

@BerinLoritsch Ancora una volta, in generale sono d'accordo con te, ma in questo caso specifico non lo sono. Ho fornito la mia risposta, ma non ho visto personalmente alcuno strumento che possa contrassegnare o addirittura darti modi per identificare potenzialmente problemi di prestazioni relativi alla dimensione della memoria dello stack di una funzione.
Eric

@DocBrown, l'ho risolto. Per quanto riguarda la seconda domanda, sono praticamente d'accordo con te.
Berin Loritsch,

45

"questo ridurrebbe la memoria" - em, no. Anche se questo fosse vero (cosa che, per qualsiasi compilatore decente non lo è), la differenza sarebbe molto probabilmente trascurabile per qualsiasi situazione del mondo reale.

Tuttavia, consiglierei di usare il metodo A * (metodo A con una leggera modifica):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

ma per due motivi completamente diversi:

  • dando alla variabile sun nome esplicativo, il codice diventa più chiaro

  • evita di avere la stessa logica di sommatoria due volte nel codice, quindi il codice diventa più ASCIUTTO, il che significa meno errori soggetti a modifiche.


36
Lo ripulirei ancora di più e andrei con "somma di ritorno> -1000 && somma <1000;".
17 del 26

36
@Corey qualsiasi ottimizzatore decente utilizzerà un registro CPU per la sumvariabile, portando così a zero l'utilizzo della memoria. E anche in caso contrario, questa è solo una singola parola di memoria in un metodo "foglia". Considerando quanto Java o C # incredibilmente dispendiosi in termini di memoria possano altrimenti essere dovuti al loro GC e al modello di oggetti, una intvariabile locale letteralmente non utilizza alcuna memoria evidente. Questa è inutile micro-ottimizzazione.
amon,

10
@Corey: se è " un po ' più complesso", probabilmente non diventerà "un notevole utilizzo della memoria". Forse se costruisci un esempio davvero più complesso, ma questo lo rende una domanda diversa. Nota anche, solo perché non crei una variabile specifica per un'espressione, per risultati intermedi complessi, l'ambiente di runtime può comunque creare internamente oggetti temporanei, quindi dipende completamente dai dettagli della lingua, dell'ambiente, del livello di ottimizzazione e qualunque cosa tu chiami "evidente".
Doc Brown,

8
Oltre ai punti di cui sopra, sono abbastanza sicuro che C # / Java scelga di archiviare sumsarebbe un dettaglio di implementazione e dubito che chiunque possa fare un caso convincente sul fatto che un trucco sciocco come evitare un locale intporterebbe a questo o quella quantità di utilizzo della memoria a lungo termine. La leggibilità dell'IMO è più importante. La leggibilità può essere soggettiva, ma FWIW, personalmente preferirei che tu non eseguissi lo stesso calcolo due volte, non per l'utilizzo della CPU, ma perché devo solo ispezionare la tua aggiunta una volta quando cerco un bug.
jrh

2
... nota anche che le lingue raccolte in modo immondizia in generale sono un imprevedibile "mare agitato di memoria" che (per C # comunque) potrebbe essere ripulito solo quando necessario , ricordo di aver creato un programma che allocava gigabyte di RAM e si avviava solo " ripulendo "dopo se stesso quando la memoria è diventata scarsa. Se il GC non deve funzionare, potrebbe volerci del dolce tempo e salvare la CPU per questioni più urgenti.
jrh

35

Puoi fare di meglio di entrambi

return (abs(a + b) > 1000);

La maggior parte dei processori (e quindi dei compilatori) può eseguire abs () in una singola operazione. Non hai solo meno somme, ma anche meno confronti, che sono generalmente più costosi dal punto di vista computazionale. Rimuove anche la ramificazione, che è molto peggio per la maggior parte dei processori perché impedisce la pipeline possibile.

L'intervistatore, come hanno affermato altre risposte, ha una vita vegetale e non ha alcuna attività di condurre un colloquio tecnico.

Detto questo, la sua domanda è valida. E la risposta a quando ottimizzi e come, è quando hai dimostrato che è necessario e l'hai profilata per dimostrare esattamente quali parti ne hanno bisogno . Knuth ha affermato che l'ottimizzazione prematura è la radice di tutto il male, perché è troppo facile tentare di placcare sezioni non importanti o apportare modifiche (come l'intervistatore) che non hanno alcun effetto, mentre mancano i luoghi che ne hanno davvero bisogno. Fino a quando non hai prove concrete è davvero necessario, la chiarezza del codice è l'obiettivo più importante.

Modifica FabioTurati sottolinea correttamente che questo è il senso logico opposto rispetto all'originale, (errore mio!), E che questo illustra un ulteriore impatto dalla citazione di Knuth in cui rischiamo di violare il codice mentre proviamo a ottimizzarlo.


2
@Corey, sono abbastanza sicuro che Graham inserisca la richiesta "mi ha sfidato a risolvere lo stesso problema con meno variabili" come previsto. Se fossi l'intervistatore, mi aspetterei quella risposta, non a+bentrare ife farlo due volte. Lo capisci male "Era contento e ha detto che questo avrebbe ridotto l'utilizzo della memoria con questo metodo" - è stato gentile con te, nascondendo la sua delusione per questa spiegazione insignificante sulla memoria. Non dovresti prenderti sul serio per fare domande qui. Hai trovato lavoro? Suppongo tu non abbia fatto :-(
Sinatr

1
Stai applicando 2 trasformazioni contemporaneamente: hai trasformato le 2 condizioni in 1, usando abs(), e ne hai anche una singola return, invece di averne una quando la condizione è vera ("if branch") e un'altra quando è falsa ( "else branch"). Quando cambi codice in questo modo, fai attenzione: c'è il rischio di scrivere inavvertitamente una funzione che restituisce true quando dovrebbe restituire false e viceversa. È esattamente quello che è successo qui. So che ti stavi concentrando su un'altra cosa e hai fatto un buon lavoro. Tuttavia, questo avrebbe potuto facilmente costarti il ​​lavoro ...
Fabio Turati,

2
@FabioTurati Ben individuato - grazie! Aggiornerò la risposta. Ed è un buon punto su refactoring e ottimizzazione, che rende la citazione di Knuth ancora più rilevante. Dovremmo dimostrare di aver bisogno dell'ottimizzazione prima di assumerci il rischio.
Graham,

2
La maggior parte dei processori (e quindi dei compilatori) può eseguire abs () in una singola operazione. Sfortunatamente non è il caso degli interi. ARM64 ha un negazione condizionale che può usare se i flag sono già impostati da un addse ARM ha predicato reverse-sub ( rsblt= reverse-sub se meno-tha) ma tutto il resto richiede più istruzioni extra per implementare abs(a+b)o abs(a). godbolt.org/z/Ok_Con mostra output asm x86, ARM, AArch64, PowerPC, MIPS e RISC-V. È solo trasformando il confronto in un controllo della portata (unsigned)(a+b+999) <= 1998Uche gcc può ottimizzarlo come nella risposta di Phil.
Peter Cordes,

2
Il codice "migliorato" in questa risposta è ancora errato, poiché produce una risposta diversa per IsSumInRange(INT_MIN, 0). Il codice originale restituisce falseperché INT_MIN+0 > 1000 || INT_MIN+0 < -1000; ma il codice "nuovo e migliorato" restituisce trueperché abs(INT_MIN+0) < 1000. (O, in alcune lingue,
genererà

16

Quando è appropriato utilizzare il metodo A rispetto al metodo B e viceversa?

L'hardware è economico; i programmatori sono costosi . Quindi il costo del tempo che voi due avete sprecato per questa domanda è probabilmente molto peggio di una delle due risposte.

Indipendentemente da ciò, i compilatori più moderni troverebbero un modo per ottimizzare la variabile locale in un registro (invece di allocare spazio nello stack), quindi i metodi sono probabilmente identici in termini di codice eseguibile. Per questo motivo, la maggior parte degli sviluppatori sceglierebbe l'opzione che comunica l'intenzione in modo più chiaro (vedi Scrivere codice davvero ovvio (ROC) ). A mio avviso, sarebbe il metodo A.

D'altra parte, se questo è puramente un esercizio accademico, puoi avere il meglio di entrambi i mondi con il Metodo C:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=bè un trucco accurato ma devo menzionare (nel caso in cui non sia implicito dal resto della risposta), dalla mia esperienza i metodi che confondono con i parametri possono essere molto difficili da debug e mantenere.
jrh

1
Sono d'accordo @jrh. Sono un forte sostenitore del ROC, e quel genere di cose è tutt'altro.
John Wu,

3
"L'hardware è economico; i programmatori sono costosi." Nel mondo dell'elettronica di consumo, questa affermazione è falsa. Se vendi milioni di unità, è un ottimo investimento spendere $ 500.000 in costi di sviluppo aggiuntivi per risparmiare $ 0,10 sui costi hardware per unità.
Bart van Ingen Schenau,

2
@JohnWu: hai semplificato il ifcontrollo, ma hai dimenticato di invertire il risultato del confronto; la tua funzione ora sta tornando truequando nona + b è nell'intervallo. O aggiungi a all'esterno della condizione ( ) o distribuisci i test di inversione per ottenere O per fare in modo che il controllo della portata scorra bene,!return !(a > 1000 || a < -1000)!return a <= 1000 && a >= -1000;return -1000 <= a && a <= 1000;
ShadowRanger

1
@JohnWu: ancora leggermente fuori dai casi limite, la logica distribuita richiede <=/ >=, non </ >(con </ >, 1000 e -1000 sono considerati fuori portata, il codice originale li ha trattati come nell'intervallo).
ShadowRanger,

11

Vorrei ottimizzare per la leggibilità. Metodo X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

Piccoli metodi che fanno solo 1 cosa ma sono facili da ragionare.

(Questa è una preferenza personale, mi piacciono i test positivi anziché quelli negativi, il tuo codice originale sta effettivamente testando se il valore NON è al di fuori dell'intervallo.)


5
Questo. (I commenti di cui sopra sono stati valutati in modo simile per quanto riguarda: leggibilità). 30 anni fa, quando stavamo lavorando con macchine con meno di 1 MB di RAM, era necessario spremere le prestazioni - proprio come il problema y2k, ottenere qualche centinaio di migliaia di record che sprecano alcuni byte di memoria a causa di varchi inutilizzati e riferimenti, ecc. e si somma rapidamente quando hai solo 256k di RAM. Ora che abbiamo a che fare con macchine che hanno più gigabyte di RAM, risparmiare anche pochi MB di RAM rispetto alla leggibilità e alla manutenibilità del codice non è un buon affare.
Ivanivan,

@ivanivan: non credo che il "problema y2k" riguardasse davvero la memoria. Dal punto di vista dell'immissione dei dati, l'immissione di due cifre è più efficiente dell'inserimento di quattro cifre e mantenere le cose immesse è più semplice che convertirle in un'altra forma.
supercat

10
Ora devi tracciare 2 funzioni per vedere cosa sta succedendo. Non puoi prenderlo al valore nominale, perché non puoi dire dal nome se questi sono limiti inclusivi o esclusivi. E se aggiungi tali informazioni, il nome della funzione è più lungo del codice per esprimerlo.
Peter,

1
Ottimizza la leggibilità e crea funzioni piccole e facili da ragionare, sicuramente d'accordo. Ma non sono d'accordo con forza che la ridenominazione ae bper number1e number2aiuti la leggibilità in qualsiasi modo. Anche la tua denominazione delle funzioni è incoerente: perché IsSumInRangecodificare l'intervallo se lo IsValueInRangeaccetta come argomenti?
lasciato il

La prima funzione può traboccare. (Come il codice di altre risposte). Sebbene la complessità del codice sicuro da overflow sia un argomento per metterlo in funzione.
philipxy,

6

In breve, non credo che la domanda abbia molta rilevanza nell'attuale informatica, ma da una prospettiva storica è un esercizio di pensiero interessante.

Il tuo intervistatore è probabilmente un fan del Mythical Man Month. Nel libro, Fred Brooks sostiene che i programmatori avranno generalmente bisogno di due versioni di funzioni chiave nella loro cassetta degli attrezzi: una versione ottimizzata per la memoria e una versione ottimizzata per la CPU. Fred ha basato questo sulla sua esperienza nella guida allo sviluppo del sistema operativo IBM System / 360 in cui le macchine possono avere fino a 8 kilobyte di RAM. In tali macchine, la memoria richiesta per le variabili locali nelle funzioni potrebbe essere potenzialmente importante, soprattutto se il compilatore non le ha effettivamente ottimizzate (o se il codice è stato scritto direttamente nel linguaggio assembly).

Nell'era attuale, penso che sarebbe difficile trovare un sistema in cui la presenza o l'assenza di una variabile locale in un metodo farebbe una notevole differenza. Perché una variabile abbia importanza, il metodo dovrebbe essere ricorsivo con una ricorsione profonda prevista. Anche in questo caso, è probabile che la profondità dello stack venga superata causando eccezioni di overflow dello stack prima che la variabile stessa causasse un problema. L'unico vero scenario in cui potrebbe essere un problema è con array molto grandi, allocati nello stack in un metodo ricorsivo. Ma è anche improbabile poiché penso che la maggior parte degli sviluppatori penserebbe due volte a copie non necessarie di array di grandi dimensioni.


4

Dopo l'assegnazione s = a + b; le variabili aeb non vengono più utilizzate. Pertanto, non viene utilizzata memoria per s se non si utilizza un compilatore completamente danneggiato dal cervello; la memoria utilizzata comunque per aeb viene riutilizzata.

Ma l'ottimizzazione di questa funzione è una totale assurdità. Se potessi risparmiare spazio, sarebbero forse 8 byte mentre la funzione è in esecuzione (che viene recuperata quando la funzione ritorna), quindi assolutamente inutile. Se potessi risparmiare tempo, sarebbe un singolo numero di nanosecondi. L'ottimizzazione di questo è una totale perdita di tempo.


3

Le variabili del tipo di valore locale sono allocate nello stack o (più probabilmente per parti così piccole di codice) utilizzano i registri nel processore e non riescono mai a vedere alcuna RAM. In entrambi i casi hanno vita breve e nulla di cui preoccuparsi. Si inizia a considerare l'uso della memoria quando è necessario bufferizzare o mettere in coda gli elementi di dati in raccolte potenzialmente grandi e di lunga durata.

Quindi dipende da cosa ti interessa di più per la tua applicazione. Velocità di elaborazione? Tempo di risposta? Impronta di memoria? Manutenibilità? Coerenza nel design? Dipende interamente da te.


4
Nitpicking: .NET almeno (la lingua del post non è specificata) non fornisce alcuna garanzia in merito all'allocazione delle variabili locali "nello stack". Vedi "lo stack è un dettaglio di implementazione" di Eric Lippert.
jrh

1
@jrh Le variabili locali nello stack o nell'heap possono essere un dettaglio di implementazione, ma se qualcuno voleva davvero una variabile nello stack c'è stackalloce ora Span<T>. Forse utile in un punto caldo, dopo la profilazione. Inoltre, alcuni dei documenti intorno alle strutture implicano che i tipi di valore potrebbero essere nello stack mentre i tipi di riferimento non lo saranno. Comunque, nella migliore delle ipotesi potresti evitare un po 'di GC.
Bob,

2

Come hanno già detto altre risposte, devi pensare a cosa stai ottimizzando.

In questo esempio, sospetto che qualsiasi compilatore decente genererebbe un codice equivalente per entrambi i metodi, quindi la decisione non avrebbe alcun effetto sul tempo di esecuzione o sulla memoria!

Quello che fa effetto è la leggibilità del codice. (Il codice deve essere letto dagli umani, non solo dai computer.) Non c'è troppa differenza tra i due esempi; quando tutte le altre cose sono uguali, considero la brevità una virtù, quindi probabilmente sceglierei il Metodo B. Ma tutte le altre cose sono raramente uguali e, in un caso più complesso del mondo reale, potrebbe avere un grande effetto.

Cose da considerare:

  • L'espressione intermedia ha effetti collaterali? Se chiama funzioni impure o aggiorna variabili, ovviamente la duplicazione sarebbe una questione di correttezza, non solo di stile.
  • Quanto è complessa l'espressione intermedia? Se esegue molti calcoli e / o chiama funzioni, il compilatore potrebbe non essere in grado di ottimizzarlo e ciò influirebbe sulle prestazioni. (Anche se, come ha detto Knuth , "dovremmo dimenticare piccole efficienze, diciamo circa il 97% delle volte".)
  • La variabile intermedia ha qualche significato ? Potrebbe essere dato un nome che aiuta a spiegare cosa sta succedendo? Un nome breve ma informativo potrebbe spiegare meglio il codice, mentre uno insignificante è solo rumore visivo.
  • Quanto dura l'espressione intermedia? Se lungo, la duplicazione potrebbe rendere il codice più lungo e più difficile da leggere (specialmente se forza un'interruzione di riga); in caso contrario, la duplicazione potrebbe essere più breve di tutti.

1

Come molte delle risposte hanno sottolineato, il tentativo di sintonizzare questa funzione con compilatori moderni non farà alcuna differenza. Molto probabilmente un ottimizzatore può trovare la soluzione migliore (vota in alto alla risposta che ha mostrato il codice dell'assemblatore per dimostrarlo!). Hai affermato che il codice nell'intervista non era esattamente il codice che ti era stato chiesto di confrontare, quindi forse l'esempio reale ha un po 'più senso.

Ma diamo un'altra occhiata a questa domanda: questa è una domanda di intervista. Quindi il vero problema è, come dovresti rispondere supponendo che tu voglia provare a ottenere il lavoro?

Supponiamo anche che l'intervistatore sappia di cosa stanno parlando e stanno solo cercando di vedere quello che sai.

Vorrei menzionare che, ignorando l'ottimizzatore, il primo potrebbe creare una variabile temporanea nello stack, mentre il secondo non lo farebbe, ma eseguirà il calcolo due volte. Pertanto, il primo utilizza più memoria ma è più veloce.

Potresti menzionare che comunque, un calcolo potrebbe richiedere una variabile temporanea per memorizzare il risultato (in modo che possa essere confrontato), quindi che tu chiami quella variabile o no potrebbe non fare alcuna differenza.

Vorrei quindi menzionare che in realtà il codice sarebbe stato ottimizzato e molto probabilmente sarebbe stato generato un codice macchina equivalente poiché tutte le variabili sono locali. Tuttavia, dipende da quale compilatore stai utilizzando (non molto tempo fa potevo ottenere un utile miglioramento delle prestazioni dichiarando una variabile locale come "finale" in Java).

Potresti dire che lo stack in ogni caso vive nella sua pagina di memoria, quindi a meno che la tua variabile aggiuntiva non abbia causato lo stack di overflow della pagina, in realtà non allocherà più memoria. Se trabocca, vorrà comunque una pagina completamente nuova.

Vorrei menzionare che un esempio più realistico potrebbe essere la scelta di utilizzare una cache per contenere o meno i risultati di molti calcoli e ciò solleverebbe una questione di CPU vs memoria.

Tutto ciò dimostra che sai di cosa stai parlando.

Vorrei lasciare fino alla fine per dire che sarebbe meglio concentrarsi sulla leggibilità invece. Sebbene sia vero in questo caso, nel contesto dell'intervista può essere interpretato come "Non conosco le prestazioni, ma il mio codice si legge come una storia di Janet e John ".

Quello che non dovresti fare è eliminare le solite insipide istruzioni su come l'ottimizzazione del codice non sia necessaria, non ottimizzare fino a quando non hai profilato il codice (questo indica solo che non puoi vedere il codice cattivo per te), l'hardware costa meno dei programmatori , e per favore, per favore, non citare Knuth "prematuro blah blah ...".

Le prestazioni del codice sono un vero problema in molte organizzazioni e molte organizzazioni hanno bisogno di programmatori che lo capiscano.

In particolare, con organizzazioni come Amazon, parte del codice ha un'enorme influenza. Uno snippet di codice può essere distribuito su migliaia di server o milioni di dispositivi e può essere chiamato miliardi di volte al giorno ogni giorno dell'anno. Potrebbero esserci migliaia di frammenti simili. La differenza tra un algoritmo cattivo e uno buono può essere facilmente un fattore di mille. Fai i numeri e moltiplica tutto questo: fa la differenza. Il costo potenziale per l'organizzazione di codice non performante può essere molto significativo o addirittura fatale se un sistema esaurisce la capacità.

Inoltre, molte di queste organizzazioni lavorano in un ambiente competitivo. Quindi non puoi semplicemente dire ai tuoi clienti di acquistare un computer più grande se il software del tuo concorrente funziona già bene sull'hardware che hanno o se il software funziona su un telefono cellulare e non può essere aggiornato. Alcune applicazioni sono particolarmente critiche per le prestazioni (vengono in mente giochi e app mobili) e possono vivere o morire in base alla loro velocità di risposta o velocità.

Ho lavorato personalmente per oltre due decenni su molti progetti in cui i sistemi hanno fallito o sono stati inutilizzabili a causa di problemi di prestazioni e sono stato chiamato per ottimizzare quei sistemi e in tutti i casi è stato a causa di un codice errato scritto da programmatori che non capivano l'impatto di ciò che stavano scrivendo. Inoltre, non è mai un pezzo di codice, è sempre ovunque. Quando arrivo, è troppo tardi per iniziare a pensare alle prestazioni: il danno è stato fatto.

Comprendere le prestazioni del codice è una buona abilità avere allo stesso modo della comprensione della correttezza e dello stile del codice. Viene fuori pratica. I guasti alle prestazioni possono essere tanto gravi quanto i guasti funzionali. Se il sistema non funziona, non funziona. Non importa il perché. Allo stesso modo, le prestazioni e le funzionalità che non vengono mai utilizzate sono entrambe negative.

Quindi, se l'intervistatore ti chiede delle prestazioni, consiglierei di provare a dimostrare quante più conoscenze possibili. Se la domanda sembra negativa, fai cortesemente capire perché pensi che non sarebbe un problema in quel caso. Non citare Knuth.


0

Dovresti prima ottimizzare per correttezza.

La tua funzione non riesce per i valori di input vicini a Int.MaxValue:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

Questo restituisce vero perché la somma trabocca a -400. Inoltre, la funzione non funziona per a = int.MinValue + 200. (aggiunge erroneamente fino a "400")

Non sapremo cosa cercava l'intervistatore a meno che lui o lei non intervenisse, ma "l'overflow è reale" .

In una situazione di intervista, porre domande per chiarire la portata del problema: quali sono i valori di input massimo e minimo consentiti? Una volta che li hai, puoi lanciare un'eccezione se il chiamante invia valori al di fuori dell'intervallo. Oppure (in C #), puoi utilizzare una sezione {} selezionata, che genererebbe un'eccezione in caso di overflow. Sì, è più lavoro e complicato, ma a volte è quello che serve.


I metodi erano solo esempi. Non sono stati scritti per essere corretti, ma per illustrare la vera domanda. Grazie per l'input però!
Corey P,

Penso che la domanda dell'intervista sia diretta allo spettacolo, quindi è necessario rispondere all'intento della domanda. L'intervistatore non sta chiedendo del comportamento ai limiti. Ma interessante punto laterale comunque.
rghome,

1
@Corey I buoni intervistatori come domande a 1) valutano l'abilità del candidato in merito alla questione, come suggerito da rghome anche qui 2) come un'apertura ai problemi più grandi (come la non corretta correttezza funzionale) e alla profondità delle conoscenze correlate - questo è molto di più nelle successive interviste di carriera - buona fortuna.
chux,

0

La tua domanda avrebbe dovuto essere: "Devo ottimizzare tutto questo?".

Le versioni A e B differiscono in un dettaglio importante che rende A preferibile, ma non è correlato all'ottimizzazione: non si ripete il codice.

L'attuale "ottimizzazione" si chiama eliminazione della sottoespressione comune, che è praticamente ciò che fa ogni compilatore. Alcuni eseguono questa ottimizzazione di base anche quando le ottimizzazioni sono disattivate. Quindi questa non è veramente un'ottimizzazione (il codice generato sarà quasi certamente esattamente lo stesso in ogni caso).

Ma se non è un'ottimizzazione, allora perché è preferibile? Va bene, non ripeti il ​​codice, a chi importa!

Bene, prima di tutto, non hai il rischio di sbagliare accidentalmente metà della clausola condizionale. Ma soprattutto, qualcuno che legge questo codice può immediatamente capire cosa stai cercando di fare, invece di if((((wtf||is||this||longexpression))))un'esperienza. Ciò che il lettore vede è if(one || theother)che è una buona cosa. Non di rado, capita che tu sia l'altra persona che legge il tuo codice tre anni dopo e pensi "WTF significa?". In tal caso è sempre utile se il tuo codice comunica immediatamente quale fosse l'intento. Con una sottoespressione comune chiamata correttamente, questo è il caso.
Inoltre, se in qualsiasi momento in futuro, decidi che, ad esempio, devi passare a+ba a-b, devi cambiarne unoposizione, non due. E non c'è rischio di (di nuovo) sbagliare il secondo per sbaglio.

Per quanto riguarda la tua vera domanda, per cosa dovresti ottimizzare, prima di tutto il tuo codice dovrebbe essere corretto . Questa è la cosa assolutamente più importante. Il codice che non è corretto è un codice errato, anche più se nonostante sia errato "funziona bene", o almeno sembra che funzioni bene. Successivamente, il codice dovrebbe essere leggibile (leggibile da qualcuno che non lo conosce).
Per quanto riguarda l'ottimizzazione ... certamente non si dovrebbe scrivere deliberatamente codice anti-ottimizzato, e certamente non sto dicendo che non dovresti pensare al design prima di iniziare (come scegliere l'algoritmo giusto per il problema, non il meno efficiente).

Ma per la maggior parte delle applicazioni, la maggior parte delle volte, le prestazioni che si ottengono dopo aver eseguito il codice corretto e leggibile usando un algoritmo ragionevole attraverso un compilatore ottimizzato vanno bene, non c'è davvero bisogno di preoccuparsi.

In caso contrario, ovvero se le prestazioni dell'applicazione in effetti non soddisfano i requisiti, e solo allora , dovresti preoccuparti di fare ottimizzazioni locali come quella che hai tentato. Preferibilmente, però, riconsidererei l'algoritmo di livello superiore. Se si chiama una funzione 500 volte anziché 50.000 volte a causa di un algoritmo migliore, questo ha un impatto maggiore rispetto al salvataggio di tre cicli di clock su una micro-ottimizzazione. Se non ti fermi per diverse centinaia di cicli su un accesso casuale alla memoria per tutto il tempo, questo ha un impatto maggiore rispetto a fare alcuni calcoli economici extra, ecc. Ecc.

L'ottimizzazione è una questione difficile (puoi scrivere interi libri a riguardo e non finire mai), e passare del tempo a ottimizzare ciecamente un determinato punto (senza nemmeno sapere se questo è il collo di bottiglia!) È di solito tempo perso. Senza la profilazione, l'ottimizzazione è molto difficile da ottenere.

Ma come regola generale, quando stai volando alla cieca e hai solo bisogno / voglia di fare qualcosa , o come strategia di default generale, suggerirei di ottimizzare per "memoria".
L'ottimizzazione per la "memoria" (in particolare la localizzazione spaziale e i modelli di accesso) di solito porta un vantaggio perché, a differenza di una volta in cui tutto era "un po 'lo stesso", oggi accedere alla RAM è tra le cose più costose (a corto di lettura dal disco!) che puoi in linea di principio fare. Considerando che ALU, invece, è economico e sta diventando più veloce ogni settimana. La larghezza di banda e la latenza della memoria non migliorano altrettanto rapidamente. Una buona localizzazione e buoni schemi di accesso possono facilmente fare una differenza 5x (20x in esempi estremi, contrapposti) in fase di esecuzione rispetto ai modelli di accesso errato in applicazioni ad alto contenuto di dati. Sii gentile con le tue cache e sarai una persona felice.

Per mettere in prospettiva il paragrafo precedente, considera quanto ti costano le diverse cose che puoi fare. L'esecuzione di qualcosa di simile a+brichiede (se non ottimizzato) uno o due cicli, ma la CPU di solito può avviare diverse istruzioni per ciclo e può eseguire il pipeline di istruzioni non dipendenti in modo più realistico in modo da costare solo circa mezzo ciclo o meno. Idealmente, se il compilatore è bravo nella pianificazione e, a seconda della situazione, potrebbe costare zero.
Il recupero dei dati ("memoria") ti costa 4-5 cicli se sei fortunato ed è in L1, e circa 15 cicli se non sei così fortunato (colpo L2). Se i dati non sono affatto nella cache, sono necessarie diverse centinaia di cicli. Se il tuo schema di accesso casuale supera le capacità del TLB (facile da fare con solo ~ 50 voci), aggiungi altre centinaia di cicli. Se il tuo schema di accesso casuale causa effettivamente un errore di pagina, ti costa qualche decine di migliaia di cicli nel migliore dei casi e diversi milioni nel peggiore.
Ora pensaci, qual è la cosa che vuoi evitare più urgentemente?


0

Quando ottimizzare la memoria rispetto alla velocità delle prestazioni per un metodo?

Dopo avere ottenuto il funzionalità destra prima . Quindi la selettività si preoccupa delle micro ottimizzazioni.


Come domanda di intervista per quanto riguarda le ottimizzazioni, il codice provoca la solita discussione ma non raggiunge l'obiettivo di livello superiore del codice È funzionalmente corretto?

Sia C ++ che C e altri considerano l' intoverflow come un problema dal a + b. Non è ben definito e C lo chiama comportamento indefinito . Non è specificato per "avvolgere", anche se questo è il comportamento comune.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

Una tale funzione chiamata IsSumInRange()dovrebbe essere ben definita ed eseguire correttamente per tutti i intvalori di a,b. Il crudo a + bnon lo è. La soluzione CA potrebbe utilizzare:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

Il codice di cui sopra potrebbe essere ottimizzato utilizzando un tipo intero più ampia int, se disponibili, come sotto o distribuendo il sum > N, sum < -Ntest all'interno della if (a >= 0)logica. Tuttavia, tali ottimizzazioni potrebbero non portare veramente a un codice "più veloce" emesso dato a un compilatore intelligente né essere degno della manutenzione aggiuntiva di essere intelligenti.

  long long sum a;
  sum += b;

Anche l'utilizzo abs(sum)è soggetto a problemi quando sum == INT_MIN.


0

Di che tipo di compilatori stiamo parlando e che tipo di "memoria"? Perché nel tuo esempio, assumendo un ragionevole ottimizzatore, l'espressione a+bdeve generalmente essere memorizzata in un registro (una forma di memoria) prima di eseguire tale aritmetica.

Quindi se stiamo parlando di un compilatore stupido che si incontra a+bdue volte, assegnerà più registri (memoria) nel tuo secondo esempio, perché il tuo primo esempio potrebbe archiviare quell'espressione una volta in un singolo registro mappato sulla variabile locale, ma noi stai parlando di compilatori molto sciocchi a questo punto ... a meno che tu non stia lavorando con un altro tipo di compilatore sciocco che impila fuoriuscite ogni singola variabile in tutto il luogo, nel qual caso forse il primo causerebbe più dolore da ottimizzare rispetto a il secondo*.

Voglio ancora grattarlo e pensare che il secondo probabilmente userà più memoria con un compilatore stupido anche se è incline a impilare gli sversamenti, perché potrebbe finire per allocare tre registri per a+be versare ae baltro ancora. Se stiamo parlando di ottimizzazione più primitiva poi la cattura a+bdi sprobabilmente "aiutare" si utilizzano meno registri / fuoriuscite di stack.

Tutto ciò è estremamente speculativo in modi piuttosto stupidi in assenza di misurazioni / disassemblaggi e anche negli scenari peggiori, questo non è un caso di "memoria contro prestazioni" (perché anche tra i peggiori ottimizzatori che posso pensare, non stiamo parlando su tutto tranne che sulla memoria temporanea come stack / register), è puramente un caso di "performance" nella migliore delle ipotesi, e tra tutti gli ottimizzatori ragionevoli i due sono equivalenti, e se uno non utilizza un ottimizzatore ragionevole, perché è ossessionato dall'ottimizzazione di natura così microscopica e misure particolarmente assenti? È come concentrarsi a livello di assembly di selezione delle istruzioni / allocazione dei registri che non mi aspetterei mai che chiunque cerchi di mantenere la propria produttività quando si utilizza, per esempio, un interprete che impila tutto.

Quando ottimizzare la memoria rispetto alla velocità delle prestazioni per un metodo?

Per quanto riguarda questa domanda se posso affrontarla in modo più ampio, spesso non trovo i due diametralmente opposti. Soprattutto se i tuoi schemi di accesso sono sequenziali e data la velocità della cache della CPU, spesso una riduzione della quantità di byte elaborati in sequenza per input non banali si traduce (fino a un certo punto) nell'aratro di quei dati più velocemente. Naturalmente ci sono dei punti di rottura in cui se i dati sono molto, molto più piccoli in cambio di modo, molte più istruzioni, potrebbe essere più veloce elaborare sequenzialmente in forma più grande in cambio di meno istruzioni.

Ma ho scoperto che molti sviluppatori tendono a sottovalutare quanto una riduzione dell'uso della memoria in questi tipi di casi possa tradursi in riduzioni proporzionali del tempo impiegato per l'elaborazione. È umanamente intuitivo tradurre i costi delle prestazioni in istruzioni piuttosto che l'accesso alla memoria al punto da raggiungere grandi LUT in un vano tentativo di accelerare alcuni piccoli calcoli, solo per scoprire prestazioni degradate con l'accesso aggiuntivo alla memoria.

Per i casi di accesso sequenziale attraverso alcuni enormi array (non parlando di variabili scalari locali come nel tuo esempio), vado secondo la regola che meno memoria da attraversare in sequenza si traduce in prestazioni migliori, specialmente quando il codice risultante è più semplice di altrimenti, fino a quando non lo fa fino a quando le mie misurazioni e il mio profiler non mi diranno diversamente, e importa, allo stesso modo presumo che leggere sequenzialmente un file binario più piccolo su disco sarebbe più veloce da attraversare di uno più grande (anche se quello più piccolo richiede alcune istruzioni in più ), fino a quando tale ipotesi non si applica più alle mie misurazioni.

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.