2009-08-27 15 views
7

Ho provato questo codice su Visual C++ 2008 e mostra che A e B non hanno lo stesso indirizzo.Stack e scope C++

int main() 
{ 
    { 
     int A; 
     printf("%p\n", &A); 
    } 

    int B; 
    printf("%p\n", &B); 
} 

Ma poiché A non esiste più quando B viene definito, mi sembra che la stessa posizione dello stack potrebbe essere riutilizzato ...

Non capisco il motivo per cui il compilatore non lo fa sembra fare quello che sembra un'ottimizzazione molto semplice (che potrebbe importare nel contesto di variabili più grandi e funzioni ricorsive per esempio). E non sembra che riutilizzarlo sarebbe più pesante sulla CPU o sulla memoria. Qualcuno ha una spiegazione per questo?

Credo che la risposta sia sulla falsariga di "perché è molto più complessa di quanto sembri", ma onestamente non ne ho idea.

modifica: alcune precisioni relative alle risposte e ai commenti riportati di seguito.

Il problema con questo codice è che ogni volta che viene chiamata questa funzione, lo stack aumenta "un numero intero troppo". Ovviamente questo non è un problema nell'esempio, ma si consideri grandi variabili e chiamate ricorsive e si ha un overflow dello stack che potrebbe essere facilmente evitato.

Quello che suggerisco è un ottimizzazione della memoria, ma non vedo come danneggerebbe le prestazioni.

E a proposito, questo accade in versione, tutte le ottimizzazioni su.

+0

Stai compilando una build di rilascio o una build di debug? – Michael

+1

quello che stai suggerendo è un ** spazio ** di ottimizzazione, ma non necessariamente un ottimizzazione della velocità. –

+1

Se tutti i locali sono troppo grandi per adattarsi a una linea della cache, questo diventa un ottimizzazione della velocità poiché non si dispone di una mancanza di cache. – Michael

risposta

8

Riutilizzare lo spazio di stack per i locali come questo è un'ottimizzazione molto comune. In effetti, su una build ottimizzata, se non si prendeva l'indirizzo della gente del posto, il compilatore non poteva nemmeno allocare lo spazio di stack e la variabile vivrebbe solo in un registro.

Potresti non vedere questa ottimizzazione per diversi motivi.

Innanzitutto, se le ottimizzazioni sono disattivate (come una build di debug), il compilatore non eseguirà nessuna di queste operazioni per rendere più facile il debug - è possibile visualizzare il valore di A anche se non è più utilizzato nella funzione.

Se si stanno eseguendo le ottimizzazioni, la mia ipotesi sarebbe che, poiché si sta prendendo l'indirizzo del locale e lo si passa ad un'altra funzione, il compilatore non vuole riutilizzare l'archivio poiché non è chiaro cosa stia facendo quella funzione con l'indirizzo.

Si può anche immaginare un compilatore che non utilizzi questa ottimizzazione a meno che lo spazio di stack utilizzato da una funzione non superi una determinata soglia. Non conosco alcun compilatore che faccia questo, dal momento che riutilizzare lo spazio delle variabili locali che non sono più utilizzate ha un costo zero e potrebbe essere applicato su tutta la linea.

Se la crescita dello stack è un problema serio per l'applicazione, ad esempio, in alcuni scenari si stanno superando gli overflow dello stack, non si dovrebbe fare affidamento sull'ottimizzazione dello stack dello spazio da parte del compilatore. Si dovrebbe prendere in considerazione lo spostamento di buffer di grandi dimensioni nello stack nell'heap e lavorare per eliminare la ricorsione molto profonda. Ad esempio, sui thread di Windows è disponibile uno stack di 1 MB per impostazione predefinita.Se sei preoccupato di traboccare perché stai allocando 1k di memoria su ogni frame dello stack e 1000 chiamate ricorsive in profondità, la correzione non è cercare di convincere il compilatore a risparmiare spazio su ogni frame dello stack.

+0

Non penso che il compilatore possa inserire A e B nei registri nell'esempio sopra, perché & A e & B sono necessari. – AraK

+0

@Arak - Giusto, ecco perché ho detto "se non hai preso l'indirizzo". – Michael

+0

@ Michael, scusa non l'ho visto :) – AraK

1

È probabile che il compilatore stia mettendo entrambi nello sullo stesso stack frame. Quindi, anche se A non è accessibile al di fuori del suo ambito, il compilatore è libero di collegarlo a un posto in memoria a patto che ciò non corrompa la semantica del codice. In breve, entrambi vengono messi in pila nello stesso momento in cui esegui il main.

+2

Anche conosciuto come: Non cercare di essere più intelligente del tuo compilatore.(Senza offesa) – ebo

+0

So che il compilatore è autorizzato a farlo, ma mi chiedo ancora perché lo fa :) – Drealmer

1

A viene allocato sullo stack dopo B. B viene dichiarata dopo A nel codice (che non è consentito dalla C90 tra l'altro), ma è ancora nella portata all'inizio della funzione principale ed esiste quindi dalla l'inizio del main fino alla fine Quindi B viene premuto all'avvio principale, A viene premuto quando viene inserito lo scope interno e scoppiato quando viene lasciato, e quindi B viene fatto scattare quando viene lasciata la funzione principale.

+1

Il compilatore è libero di allocare A e B in qualsiasi ordine nello stack fintanto che i costruttori/distruttori vengono eseguiti nell'ordine e nella posizione corretti. –

+0

Non hanno nemmeno bisogno di essere in pila. Se l'indirizzo non è stato preso nell'esempio precedente, il compilatore potrebbe semplicemente lasciarli in registri e la funzione potrebbe richiedere zero stack space. – Michael

3

Perché non controllare l'assemblaggio?

Ho modificato leggermente il codice in modo che int A = 1; e int B = 2; per renderlo leggermente più facile da decifrare.

Da g ++ con le impostazioni predefinite:

.globl main 
    .type main, @function 
main: 
.LFB2: 
    leal 4(%esp), %ecx 
.LCFI0: 
    andl $-16, %esp 
    pushl -4(%ecx) 
.LCFI1: 
    pushl %ebp 
.LCFI2: 
    movl %esp, %ebp 
.LCFI3: 
    pushl %ecx 
.LCFI4: 
    subl $36, %esp 
.LCFI5: 
    movl $1, -8(%ebp) 
    leal -8(%ebp), %eax 
    movl %eax, 4(%esp) 
    movl $.LC0, (%esp) 
    call printf 
    movl $2, -12(%ebp) 
    leal -12(%ebp), %eax 
    movl %eax, 4(%esp) 
    movl $.LC0, (%esp) 
    call printf 
    movl $0, %eax 
    addl $36, %esp 
    popl %ecx 
    popl %ebp 
    leal -4(%ecx), %esp 
    ret 
.LFE2: 

In definitiva sembra che il compilatore semplicemente non si è preoccupato di metterli nello stesso indirizzo. Non c'era nessuna ottimizzazione dell'aspetto in testa. O non stava cercando di ottimizzare, o ha deciso che non c'era alcun beneficio.

L'avviso A è assegnato e quindi stampato. Quindi B viene assegnato e stampato, proprio come nella fonte originale. Ovviamente, se si utilizzano impostazioni del compilatore diverse, ciò potrebbe apparire completamente diverso.

2

Con la mia conoscenza lo spazio per la B è riservato al momento dell'entrata al principale, e non alla linea

int B; 

Se si interrompe nel debugger prima di quella linea, si è tuttavia in grado di ottenere l'indirizzo di B. Anche lo stackpointer non cambia dopo questa riga. L'unica cosa che accade su questa linea è che viene chiamato il costruttore di B.

+0

Non c'è nulla nello standard C++ che vieta o consente questo. Quindi, è abbastanza probabile che il compilatore in effetti allochi immediatamente la memoria per B. Altri compilatori no. – MSalters

-1

In questo caso il compilatore non ha davvero alcuna scelta. Non può assumere alcun comportamento particolare di printf(). Di conseguenza, è necessario supporre che printf() possa rimanere sospeso a &A finché A stesso esiste. Pertanto, A stesso è vivo nell'intero ambito in cui è definito.

+2

-1: * 'A' stesso è in diretta nell'intero ambito in cui è definito *. Questo è esattamente il motivo per cui è in un costrutto '{...}', quindi non è definito al di fuori, al momento in cui il compilatore incontra 'B'. Quindi il compilatore ** ha ** una scelta. –

1

Gran parte del mio lavoro sta combattendo i compilatori e devo dire che non sempre fanno ciò che noi umani ci aspettiamo che facciano. Anche quando hai programmato il compilatore, puoi rimanere sorpreso dai risultati, la matrice di input è impossibile da prevedere al 100%.

La parte di ottimizzazione del compilatore è molto complessa e, come menzionato in altre risposte, ciò che è stato osservato potrebbe essere dovuto a una risposta volontaria a un'impostazione, ma potrebbe essere solo il risultato dell'influenza del codice circostante o anche l'assenza di questa ottimizzazione nella logica.

In ogni caso, come dice Micheal, non si dovrebbe fare affidamento sul compilatore per evitare gli overflow dello stack, perché si potrebbe semplicemente spingere il problema in un secondo momento, quando viene utilizzata la normale manutenzione del codice o un diverso set di input, e si romperà molto più in là, forse nelle mani dell'utente.