Le variabili temporanee rallentano il mio programma?

Supponiamo di avere il seguente codice C:

int i = 5; int j = 10; int result = i + j; 

Se lo sto ripetendo molte volte, sarebbe più veloce usare int result = 5 + 10 ? Spesso creo variabili temporanee per rendere il mio codice più leggibile, ad esempio, se le due variabili sono state ottenute da un array usando una lunga espressione per calcolare gli indici. Questo cattivo rendimento è saggio in C? Che dire delle altre lingue?

Un moderno compilatore ottimizzante dovrebbe ottimizzare quelle variabili, ad esempio se usiamo il seguente esempio in godbolt con gcc usando i -std=c99 -O3 ( -std=c99 -O3 dal vivo ):

 #include  void func() { int i = 5; int j = 10; int result = i + j; printf( "%d\n", result ) ; } 

risulterà nel seguente assembly:

 movl $15, %esi 

per il calcolo di i + j , questa è una forma di propagazione costante .

Nota, ho aggiunto la printf modo da avere un effetto collaterale, altrimenti func sarebbe stata ottimizzata per:

 func: rep ret 

Queste ottimizzazioni sono consentite dalla regola as-if , che richiede solo che il compilatore emuli il comportamento osservabile di un programma. Questo è trattato nella bozza di standard C99 sezione 5.1.2.3 Esecuzione del programma che dice:

Nella macchina astratta, tutte le espressioni sono valutate come specificato dalla semantica. Un’implementazione reale non deve valutare parte di un’espressione se può dedurre che il suo valore non è usato e che non vengono prodotti effetti collaterali necessari (compresi quelli causati dal chiamare una funzione o dall’accedere a un object volatile).

Vedi anche: Ottimizzazione del codice C ++: piegatura costante

Questo è un compito facile da ottimizzare per un compilatore ottimizzante. Cancellerà tutte le variabili e sostituirà il result con 15 .

La piegatura costante in forma SSA è praticamente l’ottimizzazione più semplice che ci sia.

L’esempio che hai fornito è facile da ottimizzare per un compilatore. L’utilizzo di variabili locali per memorizzare i valori di cache estratti da strutture e array globali può effettivamente accelerare l’esecuzione del codice. Se ad esempio stai recuperando qualcosa da una struttura complessa all’interno di un ciclo for in cui il compilatore non può ottimizzare e sai che il valore non sta cambiando, le variabili locali possono risparmiare un po ‘di tempo.

È ansible utilizzare GCC (anche altri compilatori) per generare il codice di assembly intermedio e vedere cosa sta effettivamente facendo il compilatore.

Si discute su come triggersre gli elenchi di assiemi qui: Utilizzare GCC per produrre assembly leggibili?

Può essere istruttivo esaminare il codice generato e vedere cosa sta effettivamente facendo un compilatore.

Mentre ogni sorta di differenze banali al codice possono perturbare il comportamento del compilatore in modi che migliorano o peggiorano leggermente le prestazioni, in linea di principio non dovrebbe fare alcuna differenza di prestazioni se si usano variabili temporanee come questa purché il significato del programma non sia cambiato. Un buon compilatore dovrebbe generare lo stesso codice, o comparabile, in entrambi i casi, a meno che non si stia costruendo intenzionalmente con l’ottimizzazione per ottenere il codice della macchina il più vicino ansible alla fonte (ad esempio per scopi di debug).

Stai soffrendo lo stesso problema che faccio quando cerco di imparare cosa fa un compilatore – fai un programma banale per dimostrare il problema ed esamina l’output di assemblaggio del compilatore, solo per capire che il compilatore ha ottimizzato tutto hai provato a farla sparire. È ansible trovare anche un’operazione piuttosto complessa in main () ridotta essenzialmente:

 push "%i" push 42 call printf ret 

La tua domanda originale non è “cosa succede con int i = 5; int j = 10... ?” ma “le variabili temporanee generalmente comportano una penalità in fase di esecuzione?”

La risposta è probabilmente no. Ma dovresti esaminare l’output dell’assembly per il tuo codice particolare, non banale. Se la tua CPU ha molti registri, come un ARM, allora i e j sono molto probabilmente in registri, proprio come se quei registri stessero memorizzando direttamente il valore di ritorno di una funzione. Per esempio:

 int i = func1(); int j = func2(); int result = i + j; 

è quasi certamente lo stesso codice macchina di:

 int result = func1() + func2(); 

Ti suggerisco di usare variabili temporanee se rendono il codice più facile da capire e da mantenere, e se stai davvero cercando di stringere un ciclo, ti accorgerai comunque dell’output di assemblaggio per capire come ottimizzare tanto le prestazioni quanto ansible. Ma non sacrificare la leggibilità e la manutenibilità per pochi nanosecondi, se ciò non è necessario.