Variabili temporanee o no: scegli CPU Time o RAM? [duplicare]

2

In un caso in cui più variabili sono impostate uguali a un'operazione utilizzando il risultato di un'altra operazione, è meglio creare una variabile temporanea per il risultato della seconda operazione (questo salverebbe la memoria) o è meglio creare una variabile temporanea per il risultato della seconda operazione (questo farebbe risparmiare tempo)?

Ad esempio:

// Option 1 (memory)
totalRevenue += numSold * price;
totalProfit += numSold * price - numSold * cost;
return numSold * price;

// Option 2 (time)
saleRevenue = numSold * price;
totalRevenue += saleRevenue;
totalProfit += saleRevenue - numSold * cost;
return saleRevenue;

Comprendo che questa scelta, specialmente nell'esempio sopra, è piuttosto banale. Il mio sospetto è che abbia più senso creare la variabile temporanea, ma non posso fare a meno di pensare che sia uno spreco. No, la memoria non è molto scarsa al giorno d'oggi, ma mi sento come se l'opzione 1 sprecasse meno tempo rispetto all'opzione 2 della memoria. Ho ragione?

Perché scegliere un modo rispetto all'altro?

    
posta jhschwartz 17.11.2015 - 15:54
fonte

3 risposte

5

Questa ottimizzazione si chiama eliminazione della sottoespressione comune ed è incorporata in quasi tutti gli ottimizzatori.

L'effettivo ritiro di troppe eliminazioni è la pressione del registro, a.k.a. esaurimento dei registri per contenere tutti i valori temporanei. Questo potrebbe significare spargere il temporaneo nello stack che potrebbe incorrere in un errore di cache quando lo si legge di nuovo in seguito e causare un enorme rallentamento in quella che dovrebbe essere una funzione che fa solo un po 'di matematica. Anche se il semplice ricalcolo sarebbe più veloce.

Gli ottimizzatori possono convertire in entrambe le direzioni secondo necessità. Quindi non preoccuparti troppo fino a quando il profiling non indica che la funzione è un hotspot.

    
risposta data 17.11.2015 - 16:25
fonte
2

Devi mettere in conto più cose: la leggibilità. L'opzione 2 sembra molto più leggibile dell'opzione 1.

Oltre alla leggibilità, in questo esempio, vorrei passare all'opzione 2 per un motivo che spiegherò di seguito.

Ecco una rappresentazione del tuo codice in Java e il bytecode prodotto:

public class Main {

    public static int totalRevenue;
    public static int totalProfit;

    public static int option1(int numSold, int price, int cost) {
        // Option 1 (memory)
        totalRevenue += numSold * price;
        totalProfit += numSold * price - numSold * cost;
        return numSold * price;
    }

    public static int option2(int numSold, int price, int cost) {
        // Option 2 (time)
        int saleRevenue = numSold * price;
        totalRevenue += saleRevenue;
        totalProfit += saleRevenue - numSold * cost;
        return saleRevenue;
    }
}

Bytecode:

public class Main {
  public static int totalRevenue;

  public static int totalProfit;

  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static int option1(int, int, int);
    Code:
       0: getstatic     #2                  // Field totalRevenue:I
       3: iload_0
       4: iload_1
       5: imul
       6: iadd
       7: putstatic     #2                  // Field totalRevenue:I
      10: getstatic     #3                  // Field totalProfit:I
      13: iload_0
      14: iload_1
      15: imul
      16: iload_0
      17: iload_2
      18: imul
      19: isub
      20: iadd
      21: putstatic     #3                  // Field totalProfit:I
      24: iload_0
      25: iload_1
      26: imul
      27: ireturn

  public static int option2(int, int, int);
    Code:
       0: iload_0
       1: iload_1
       2: imul
       3: istore_3
       4: getstatic     #2                  // Field totalRevenue:I
       7: iload_3
       8: iadd
       9: putstatic     #2                  // Field totalRevenue:I
      12: getstatic     #3                  // Field totalProfit:I
      15: iload_3
      16: iload_0
      17: iload_2
      18: imul
      19: isub
      20: iadd
      21: putstatic     #3                  // Field totalProfit:I
      24: iload_3
      25: ireturn
}

Per motivi di comprensibilità, iload_ * carica i dati dallo stack e istore_ * memorizza i dati nello stack.

Quindi puoi notare che hai davvero salvato un'allocazione di memoria, ma passi preziosi cicli della CPU.

Ma ecco la ragione: Le allocazioni di memoria funzionano in blocchi, non a livello di byte . Ogni volta che viene eseguito un thread, verrà allocata una certa quantità di memoria da utilizzare nelle operazioni di stack. Quindi, a meno che tu non sia abbastanza fortunato da avere questi 4 byte di allocazione della memoria di overflow per questo blocco (e che ti richiede di impostare una dimensione dello stack più grande), finirai per spendere la stessa quantità di memoria per eseguire entrambi gli esempi. Ma nel secondo, passerai più cicli facendo calcoli matematici.

    
risposta data 17.11.2015 - 18:59
fonte
0

L'unica situazione in cui lo stile è importante se una o più condizioni sono vere:

  • La "operazione" implica una chiamata di funzione non banale, non puramente aritmetica (come una ricerca nel database, una ricerca lineare / binaria per un elemento all'interno di un array, ecc.) o se hai delle buone ragioni credere che l'operazione richieda più di una frazione di secondo,
  • Oppure, il risultato dell'operazione richiede decine o centinaia di megabyte o più,
  • Oppure, se stai utilizzando un compilatore molto inefficiente e non ottimizzante e stai prendendo di mira una piattaforma di elaborazione molto lenta.

(L'elenco non è esaustivo e può essere modificato in qualsiasi momento. Chiunque abbia il privilegio di modifica-risposta è benvenuto in modifica questo elenco senza chiedere. Grazie per i contributi. check la domanda duplicata prima.)

    
risposta data 17.11.2015 - 16:53
fonte

Leggi altre domande sui tag