2009-08-27 21 views
7

Probé este código en Visual C++ 2008 y muestra que A y B no tienen la misma dirección.C++ stack y scope

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

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

Pero ya que A ya no existe cuando se define B, me parece que el mismo nivel de la pila podría ser reutilizado ...

No entiendo por qué el compilador no hace parece hacer lo que parece una optimización muy simple (que podría importar en el contexto de variables más grandes y funciones recursivas, por ejemplo). Y no parece que reutilizarlo sea más pesado en la CPU ni en la memoria. ¿Alguien tiene una explicación para esto?

Creo que la respuesta está en la línea de "porque es mucho más complejo de lo que parece", pero sinceramente no tengo ni idea.

edit: Algunas precisiones con respecto a las respuestas y los comentarios a continuación.

El problema con este código es que cada vez que se llama a esta función, la pila crece "un número entero demasiado". Por supuesto, esto no es problema en el ejemplo, pero considera grandes variables y llamadas recursivas y tienes un desbordamiento de pila que podría evitarse fácilmente.

Lo que sugiero es una optimización de la memoria, pero no veo cómo podría dañar el rendimiento.

Y por cierto, esto ocurre en las versiones , todas las optimizaciones estarán activadas.

+0

¿Está compilando una versión de lanzamiento o una versión de depuración? – Michael

+1

lo que está sugiriendo es una optimización de ** espacio **, pero no necesariamente una optimización de velocidad. –

+1

Si todos los locales son demasiado grandes para caber en una línea de caché, esto se convierte en una optimización de velocidad ya que no tiene una falta de caché. – Michael

Respuesta

8

Reutilizar el espacio de pila para locales como esta es una optimización muy común. De hecho, en una compilación optimizada, si no tomó la dirección de los locales, el compilador ni siquiera podría asignar espacio de pila y la variable solo viviría en un registro.

Es posible que no vea esta optimización por varias razones.

En primer lugar, si las optimizaciones están desactivadas (como una compilación de depuración) el compilador no hará ninguna de ellas para facilitar la depuración; puede ver el valor de A incluso si ya no se usa en la función.

Si está compilando optimizaciones, mi suposición sería que está tomando la dirección del local y pasándola a otra función, el compilador no quiere reutilizar la tienda ya que no está claro qué está haciendo esa función con la dirección

También se puede imaginar un compilador que no usaría esta optimización a menos que el espacio de pila utilizado por una función exceda algún umbral. No conozco ningún compilador que haga esto, ya que reutilizar el espacio de las variables locales que ya no se usan no tiene costo y podría aplicarse en todos los ámbitos.

Si el crecimiento de la pila es una preocupación grave para su aplicación, es decir, en algunos escenarios está llegando a desbordamientos de pila, no debe confiar en la optimización del espacio de la pila del compilador. Debería considerar mover grandes almacenamientos intermedios en la pila al montón y trabajar para eliminar la recursión muy profunda. Por ejemplo, en los subprocesos de Windows tiene una pila de 1 MB de forma predeterminada.Si le preocupa desbordar eso porque está asignando 1k de memoria en cada marco de pila y yendo 1000 llamadas recursivas de profundidad, la solución no es tratar de persuadir al compilador para que ahorre espacio de cada marco de pila.

+0

No creo que el compilador pueda poner A y B en registros en el ejemplo anterior, porque & A y & B son necesarios. – AraK

+0

@Arak - Correcto, por eso dije "si no tomaste la dirección". – Michael

+0

@Michael, lo siento, no vi eso :) – AraK

1

Es probable que el compilador ponga ambos en el mismo marco de pila. Entonces, aunque A no es accesible fuera de su alcance, el compilador puede vincularlo a un lugar en la memoria siempre que eso no corrompa la semántica del código. En resumen, ambos se ponen en la pila al mismo tiempo que ejecutas tu principal.

+2

También conocido como: No intente ser más inteligente que su compilador.(Sin ofender) – ebo

+0

Sé que el compilador tiene permiso para hacerlo, pero todavía me pregunto por qué lo hace :) – Drealmer

1

A se asigna en la pila después de B. B se declara después de A en el código (que no está permitido por C90 por cierto), pero todavía está en el alcance superior de la función principal y existe de el comienzo de main hasta el final. Por lo tanto, B se empuja cuando se inicia principal, A se empuja cuando se ingresa el alcance interno y se hace estallar cuando se deja, y luego se sobresale B cuando se deja la función principal.

+1

El compilador puede asignar A y B en cualquier orden en la pila, siempre que los constructores/destructores se ejecuten en el orden y la ubicación correctos. –

+0

Ni siquiera necesitan estar en la pila. Si la dirección no se tomó en el ejemplo anterior, el compilador podría simplemente dejarlos en registros y la función podría tomar cero espacio de pila. – Michael

3

¿Por qué no echa un vistazo al montaje?

He cambiado el código ligeramente para que int A = 1; y int B = 2; para que sea un poco más fácil de descifrar.

De g ++ con la configuración predeterminada:

.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: 

En última instancia parece que el compilador no se molestó en ponerlos en la misma dirección. No hubo una sofisticada optimización de anticipación involucrada. O bien no estaba tratando de optimizar, o decidió que no había ningún beneficio.

Se asigna el aviso A y luego se imprime. Entonces B se asigna e imprime, al igual que en la fuente original. Por supuesto, si usa configuraciones de compilador diferentes, esto podría verse completamente diferente.

2

Por mi conocimiento del espacio para B está reservada a la entrada al principal, y no en la línea

int B; 

Si se rompe en el depurador antes de esa línea, que son, sin embargo, capaz de obtener la dirección de B. El stackpointer tampoco cambia después de esta línea. Lo único que sucede en esta línea es que se llama al constructor de B.

+0

No hay nada en el estándar C++ que lo prohíba o lo permita. Entonces, es bastante posible que su compilador de hecho asigne la memoria para B inmediatamente. Otros compiladores no. – MSalters

-1

El compilador realmente no tiene otra opción en este caso. No puede asumir ningún comportamiento particular de printf(). Como resultado, debe suponerse que printf() podría esperar &A, siempre que A sí mismo exista. Por lo tanto, A sí mismo está vivo en todo el ámbito donde está definido.

+2

-1: * 'A' mismo está activo en todo el ámbito donde está definido *. Es exactamente por eso que está en una construcción '{...}', por lo que no se define afuera, en el momento en que el compilador encuentra 'B'. Entonces el compilador ** tiene ** una opción. –

1

Una gran parte de mi trabajo es luchar contra los compiladores, y debo decir que no siempre hacen lo que nosotros, los humanos, esperamos que hagan. Incluso cuando haya programado el compilador, aún puede sorprenderse por los resultados, la matriz de entrada es imposible de predecir al 100%.

La parte de optimización del compilador es muy compleja y, como se menciona en otras respuestas, lo que observó podría deberse a una respuesta voluntaria a un ajuste, pero podría ser resultado de la influencia del código circundante o incluso la ausencia de esta optimización en la lógica.

En cualquier caso, como dice Micheal, no debe confiar en el compilador para evitar desbordamientos de pila, ya que puede estar presionando el problema más tarde, cuando se usa mantenimiento de código normal o un conjunto diferente de entrada, y se bloqueará mucho más en la tubería, tal vez en manos del usuario.