2010-12-11 15 views
7
  1. Quando una variabile globale viene utilizzato all'interno di una funzione (C/C++), se sarà prelevato direttamente da registri o dalla pila?C++/C domande livello ASSEMBLAGGIO

  2. Perché i loop di collegamento (per loop) sono considerati più ottimizzati per l'ottimizzazione rispetto ai loop nobound (while loop/do while)?

  3. Perché restituire un valore non è buono come passare il valore per riferimento?

Se possibile, fornire descrizioni del livello di assemblaggio.

+9

(1) né, (2) non lo sono, (3) non è necessario, a causa di NRVO. –

+0

Cosa intendi nel punto 3.? Passare gli argomenti per funzionare in base al riferimento o restituire il valore della funzione con uno degli argomenti della funzione. –

+0

@Pawel: RVO significa Return Value Optimization, che significa costruire il valore sul posto (dove viene restituito) piuttosto che incorrere nel costo di una copia. Quando si attiva questa ottimizzazione, restituire per valore non costa più del passaggio di un riferimento o di un puntatore. –

risposta

1

1) La variabile globale viene allocata staticamente dal linker (può tuttavia essere un offset dalla base del modulo, non necessariamente un indirizzo fisso). Tuttavia, una funzione in genere legge una variabile globale da un indirizzo diretto e una variabile locale da offset + puntatore stack e un campo classe da offset + puntatore alla base dell'oggetto. Il valore di una variabile globale può essere memorizzato nella cache in un registro per le letture successive, a meno che non sia dichiarato "volatile".

2) La sua non è davvero una questione di per/do/while scelta, ma come è facile la sua per calcolare il numero di iterazioni, in modo che il compilatore sarebbe in grado di decidere se srotolare e/o Vectorize e/o parallelizzare il ciclo. Per esempio, ecco il compilatore avrebbe saputo il numero di iterazioni :

for(i=0; i<8; i++) { j = 1 << i; XXX } 

e qui non lo farà:

for(j=1; j<256; j<<=1) { XXX } 

La cicli for forse solo più frequentemente hanno una struttura che è più facile capire per il compilatore.

3) Se la sua un valore di tipo di base (char/short/int, ecc), la sua più lento di restituirlo per riferimento (anche se a volte compilatore può ottimizzare questo). Ma per strutture più grandi un riferimento/puntatore può ridurre la quantità di lavoro per il compilatore, e in realtà può essere più veloce se compilatore non sarà in grado di evitare la creazione di alcuni temporanei copie ecc

Update: Ok , ecco un esempio più specifico:

#include <stdio.h> 

int main(void) { 

    int a,b, i,j,s1,s2; 

    a = 123 + printf(""); // unknown in compile time 
    s1 = 1; 
    // bit reverse loop v1, gets unrolled 
    for(i=0; i<8; i++) { j = 1 << i; s1 += s1 + ((a&j)>0); } 
    s1 -= 256; 

    b = s1 + printf(""); 
    // bit reverse loop v2, not unrolled 
    for(s2=1; s2<256; s2+=s2+(j>0)) { j = b & s2; b -= j; } 
    s2 -= 256; 

    printf("a=%02X s1=%02X s2=%02X\n", a, s1, s2); 
} 

annunci Asm per gcc/intelc sono disponibili qui: http://nishi.dreamhosters.com/u/1.zip

+0

Supponendo che 'XXX' non modifichi' j' in modo complicato, è del tutto possibile che un compilatore ottimizzante sia in grado di determinare il numero di iterazioni per il secondo ciclo –

+0

Un esempio migliore di un ciclo per il quale il numero di iterazioni non è noto (e quindi non può essere srotolato) dovrebbe scorrere su un elenco collegato: 'for (ptr_t p = first; p; p = p-> next) {'. Il compilatore non può conoscere il numero di iterazioni.Nel secondo caso, come sottolinea Ben, l'ottimizzatore può sapere che quel ciclo verrà ripetuto per 7 volte (a meno che' XXX' non modifichi 'j'). +1 per portando il fatto che non è 'per/while' ma quanto il compilatore conosce il ciclo. (Un altro esempio più semplice:' for (int i = 0; i

4

1) Sarà preso da un indirizzo assegnato come parte del carico dell'applicazione. cioè una variabile globale è semplicemente un indirizzo nello spazio degli indirizzi virtuali del processo. Se questo globale è stato utilizzato di recente, il compilatore può essere in grado di memorizzare nella cache un registro in.

2) Non lo fanno.

3) La restituzione di un valore spesso richiede una copia dei dati. Se i dati sono di tipo semplice (come int o float), possono essere restituiti tramite un registro. Se l'oggetto è troppo grande per rientrare in un registro, allora il compilatore deve allocare lo spazio per l'oggetto nello stack e quindi copiare i dati che vengono restituiti in questo spazio allocato. Passare il valore come riferimento è, di solito, implementato passando un puntatore ai dati. Pertanto si restituisce il valore modificando direttamente i dati su quell'indirizzo di memoria. Nessuna copia ha luogo e quindi è più veloce. Si noti, tuttavia, che Return Value Optimisation (RVO) può significare che non vi è alcuna vincita al superamento del valore di ritorno come riferimento. Allo stesso modo, una s sottolineata nei commenti, il nuovo costruttore di mosse del C++ 0x può anche fornire gli stessi bonus di RVO.

Non c'è bisogno di spiegare nessuno di quelli che usano gli esempi di assemblatore, IMO.

+1

Le variabili globali non vengono in genere allocate dal HEAP (a meno che non si intenda qualcosa di diverso da ciò che intendo per HEAP). –

+1

@Charles: beh, è ​​davvero divertente. Ho sempre considerato (e mi è stato insegnato) che tutto ciò che non è lo stack è il mucchio. Su questa base, lo stack verrebbe anche allocato sull'heap (che considererei come tale). Ma in un certo senso hai ragione non è il mucchio da cui derivano le allocazioni globali. È lo spazio di indirizzamento virtuale del processo. – Goz

+2

"3) Restituire un valore richiede una copia dei dati" - Non con un movimento però. –

1

Prima di tutto non è stato specificato una piattaforma di destinazione, braccio, X 86, 6502, ZPU, etc.

1) Quando una variabile globale viene utilizzata all'interno di una funzione (C/C++), se verrà prelevata direttamente dai registri o dallo stack?

Non eri chiaro, quindi un globale può essere passato per valore, per riferimento o non passato e utilizzato direttamente nella funzione.

passato per valore dipende dal codice/compilatore/destinazione che non hai specificato. Quindi il valore o l'indirizzo globale può andare in un registro o in pila a seconda della convenzione di chiamata per quel compilatore/target. Gli elementi trasmessi dal registro a volte hanno un segnaposto sullo stack nel caso in cui la funzione necessiti di più registri di quelli disponibili. Così passato per valore il valore al quale il contenuto globale è inizialmente acceduto sia in un registro o in pila.

passato per riferimento è praticamente lo stesso del valore passato, invece del valore l'indirizzo globale viene passato dal registro o dallo stack a seconda del compilatore/destinazione. Dove questo è diverso è che è possibile accedere al globale direttamente da/alla sua posizione di memoria, ma questa è la natura del passaggio per riferimento.

utilizzato direttamente nella funzione, quindi dipende dal codice/compilatore/destinazione per stabilire se l'accesso globale viene eseguito direttamente dalla sua posizione di memoria fissa o se un registro carica tale posizione di memoria e il valore viene gestito da un registro. La pila non viene utilizzata in questo caso, quindi la risposta è o (non stack) memoria o registro.

2) Perché i loop di collegamento (per cicli) sono considerati più ottimizzati per l'ottimizzazione rispetto ai loop nobound (while loop/do while)?

A seconda del codice, del compilatore e dell'obiettivo, non riesco a pensare a un caso generico in cui uno è migliore dell'altro.

3) Perché restituire un valore non è buono come passare il valore per riferimento?

Guadagno di prestazioni molto molto sottile se non altro. Dipende molto dal codice, dal compilatore e dal target. Ci sono casi in cui per riferimento è leggermente più veloce e casi in cui per valore è leggermente più veloce. Confrontando i due, le differenze hanno a che fare con il numero di volte in cui l'indirizzo oi dati devono essere copiati in/da registri o lo stack sul suo percorso. Nella migliore delle ipotesi puoi salvare alcune istruzioni di mov o load/store.

1

Nel caso generale (essere precisi qui è difficile), i globals vengono recuperati dalla memoria ma non dallo stack (a meno che non siano già memorizzati in un registro), i loop possono essere ottimizzati a seconda delle informazioni che il compilatore ha su ciò che loop fa (può eseguire lo srotolamento del loop?) e nel terzo caso dipende dal codice effettivo.Poiché i primi due sono già stati trattati in altre domande, mi concentrerò sulla terza domanda.

Esiste un'ottimizzazione comune denominata (denominata) Return Value Optimization (N) RVO che il compilatore può eseguire per evitare copie non necessarie.

// RVO     // NRVO    // cannot perform RVO 
type foo() {   type bar() {  type baz() { 
    value a;    type a;    type a,b; 
    // operate on a   // modify a   // pass a and b to other functions 
    return type(a);   return a;   if (random() > x) return a; 
}      }      else return b; 
              } 

Sia foo e bar, il compilatore è in grado di analizzare il codice e determinare che il temporaneo type(a) in foo o il nome locale variabile a in barsono il valore di ritorno della funzione, in modo che possa costruisci quegli oggetti al posto del valore di ritorno (secondo la convenzione di chiamata) ed evita di copiarlo. Contrariamente a quello con baz in cui il compilatore deve creare oggetti a e b prima di sapere effettivamente quale deve essere restituito. In questo caso il compilatore non può ottimizzare nulla, deve eseguire le operazioni e solo alla fine copia o a o al valore restituito.

Ogniqualvolta sulle prestazioni del compilatore (N) RVO o se non é effettivamente impossibile da eseguire, la modifica della firma funzione di ricevere l'oggetto con riferimento non fornirà un vantaggio di prestazione e farà codice sul luogo di chiamata meno leggibile per funzioni che crea nuovi oggetti.

Questo dovrebbe essere usato come una regola generale, ma notando che come sempre, ci sono delle eccezioni, e casi in cui uno o l'altro potrebbe essere leggermente migliore delle prestazioni. Ma per la maggior parte dei casi, e a meno che la misurazione delle prestazioni non si riveli altrimenti, dovresti scrivere il codice il più vicino possibile alla semantica del design. Se una funzione crea un nuovo oggetto, restituiscilo per valore, se una funzione modifica un oggetto, passa per riferimento.

Alcuni casi speciali possono essere una funzione che crea vettori e viene chiamato in un ciclo chiuso, dove avere un singolo vettore che viene passato per riferimento, cancellato nella funzione e quindi riempito ridurrà il numero di allocazioni di memoria (clear() in un vettore non rilascia la memoria, quindi non ha bisogno di riallocarlo nella prossima iterazione).

All'altro estremo, quando le chiamate di funzione sono concatenate e con la combinazione corretta di valore di ritorno e passaggio per valore, è possibile evitare copie aggiuntive non passando i riferimenti in -a riferimento non-const richiede un non- oggetto temporaneo.