visual-studio-2010 performance stackframe

visual studio 2010 - ¿La omisión de los punteros de marco realmente tiene un efecto positivo en el rendimiento y un efecto negativo en la capacidad de depuración?



visual-studio-2010 performance (1)

Respuesta corta: omitiendo el puntero del cuadro,

Debe usar el puntero de la pila para acceder a las variables y argumentos locales. Al compilador no le importa, pero si está programando en ensamblador, esto hace su vida un poco más difícil. Mucho más difícil si no usas macros.

Guarda cuatro bytes (arquitectura de 32 bits) de espacio de pila por llamada de función. A menos que estés usando una recursión profunda, esto no es una victoria.

Guarda una memoria grabada en una memoria caché (la pila) y (teóricamente) guarda algunas marcas de reloj en la entrada / salida de la función, pero puede aumentar el tamaño del código. A menos que su función esté haciendo muy poco muy a menudo (en cuyo caso debe estar en línea), esto no debería ser notable.

Usted libera un registro de propósito general. Si el compilador puede utilizar el registro, producirá un código que es sustancialmente más pequeño y potencialmente más rápido. Pero, si la mayor parte del tiempo de la CPU se gasta en hablar con la memoria principal (o incluso con el disco duro), omitir el puntero del marco no lo salvará de eso.

El depurador perderá una forma fácil de generar el seguimiento de la pila. Es posible que el depurador aún pueda generar el seguimiento de la pila desde una fuente diferente (como un archivo PDB ).

Respuesta larga:

La función típica de entrada y salida es:

PUSH SP ;push the frame pointer MOV FP,SP ;store the stack pointer in the frame pointer SUB SP,xx ;allocate space for local variables et al. ... LEAVE ;restore the stack pointer and pop the old frame pointer RET ;return from the function

Una entrada y una salida sin un puntero de pila podría ser:

SUB SP,xx ;allocate space for local variables et al. ... ADD SP,xx ;de-allocate space for local variables et al. RET ;return from the function.

Guardará dos instrucciones, pero también duplicará un valor literal para que el código no se acorte (todo lo contrario), pero podría haber guardado algunos ciclos de reloj (o no, si se produce una falta de caché en la memoria caché de instrucciones) . Aunque ahorraste algo de espacio en la pila.

Usted libera un registro de propósito general. Esto tiene solo beneficios.

En regcall / fastcall, este es un registro adicional donde puede almacenar argumentos a su función. Por lo tanto, si su función toma siete (en x86; más en la mayoría de las otras arquitecturas) o más argumentos (incluido this ), el séptimo argumento aún encaja en un registro. Lo mismo, más importante, se aplica también a las variables locales. Las matrices y los objetos grandes no encajan en los registros (pero sí los punteros), pero si su función utiliza siete variables locales diferentes (incluidas las variables temporales necesarias para calcular expresiones complejas), es probable que el compilador pueda producir un código más pequeño . Un código más pequeño significa una huella de caché de instrucciones más baja, lo que significa una menor tasa de fallos y, por lo tanto, incluso menos acceso a la memoria (pero Intel Atom tiene un caché de instrucciones de 32K , lo que significa que su código probablemente se ajuste de todos modos).

La arquitectura x86 presenta los modos de direccionamiento [BX/BP/SI/DI] y [BX/BP + SI/DI] . Esto hace que el registro BP sea un lugar extremadamente útil para un índice de matriz escalado, especialmente si el puntero de matriz reside en los registros SI o DI. Dos registros de desplazamiento son mejores que uno.

Utilizar un registro evita el acceso a la memoria, pero si vale la pena almacenar una variable en un registro, es probable que sobreviva igual de bien en un caché L1 (especialmente porque va a estar en la pila). Aún existe el costo de moverse hacia / desde la memoria caché, pero como las CPU modernas realizan una gran optimización de movimiento y paralelización, es posible que un acceso L1 sea tan rápido como un acceso de registro. Por lo tanto, el beneficio de la velocidad de no mover los datos todavía está presente, pero no es tan enorme. Puedo imaginar fácilmente que la CPU evita la memoria caché de datos por completo, al menos en lo que respecta a la lectura (y la escritura en la memoria caché se puede hacer en paralelo).

Un registro que se utiliza es un registro que necesita preservación. No vale la pena almacenar mucho en los registros si va a empujarlo a la pila de todos modos antes de volver a utilizarlo. En las convenciones de llamada preservar por el que llama (como la anterior), esto significa que los registros como almacenamiento persistente no son tan útiles en una función que llama mucho a otras funciones.

También tenga en cuenta que x86 tiene un espacio de registro separado para los registros de punto flotante, lo que significa que los flotadores no pueden utilizar el registro de BP sin instrucciones adicionales de movimiento de datos. Solo lo hacen los enteros y los punteros de memoria.

Lo que se pierde al omitir los punteros de marco es la depuración. Esta respuesta muestra por qué:

Si el código falla, todo lo que debe hacer el depurador para generar el seguimiento de la pila es:

PUSH FP ; log the current frame pointer as well $1: CALL log_FP ; log the frame pointer currently on stack LEAVE ; pop the frame pointer to get the next one CMP [FP+4],0 JNZ $1 ; until the stack cannot be popped (the return address is some specific value)

Si el código falla sin un puntero de marco, es posible que el depurador no tenga forma de generar el seguimiento de la pila porque puede que no sepa (es decir, necesita ubicar el punto de entrada / salida de la función) cuánto se debe restar del puntero de la pila. Si el depurador no sabe que el puntero del marco no se está utilizando, incluso podría fallar.

Como se aconsejó hace mucho tiempo, siempre compilo mis ejecutables de versión sin punteros de marco (que es el valor predeterminado si compila con / Ox).

Sin embargo, ahora leí en el documento http://research.microsoft.com/apps/pubs/default.aspx?id=81176 , que los punteros de cuadro no tienen mucho efecto en el rendimiento. Por lo tanto, optimizarlo completamente (usando / Ox) u optimizarlo completamente con punteros de cuadro (usando / Ox / Oy-) realmente no hace una diferencia en el rendimiento.

Microsoft parece indicar que agregar punteros a cuadros (/ Oy-) facilita la depuración, pero ¿es realmente así?

Hice algunos experimentos y noté que:

  • en una simple prueba ejecutable (compilada usando / Ox / Ob0) la omisión de los punteros de trama aumenta el rendimiento (con aproximadamente el 10%). Pero este ejecutable de prueba solo realiza algunas llamadas de función, nada más.
  • en mi propia aplicación, la adición / eliminación de los punteros de cuadro no parece tener un gran efecto. Añadir punteros a cuadros parece hacer que la aplicación sea un 5% más rápida, pero podría estar dentro del margen de error.

¿Cuál es el consejo general con respecto a los punteros de cuadro?

  • ¿deberían omitirse (/ Ox) en una versión ejecutable porque realmente tienen un efecto positivo en el rendimiento?
  • ¿deberían agregarse (/ Ox / Oy-) en una versión ejecutable porque mejoran la debug-ablity (al depurar con un archivo de volcado)?

Utilizando Visual Studio 2010.