Tratando de entender la opción gcc -fomit-frame-pointer

80

Le pedí a Google que me diera el significado de la gccopción -fomit-frame-pointer, que me redirige a la siguiente declaración.

-fomit-frame-pointer

No guarde el puntero del marco en un registro para funciones que no lo necesitan. Esto evita las instrucciones para guardar, configurar y restaurar punteros de marco; también hace que un registro adicional esté disponible en muchas funciones. También hace que la depuración sea imposible en algunas máquinas.

Según mi conocimiento de cada función, se creará un registro de activación en la pila de la memoria del proceso para mantener todas las variables locales y algo más de información. Espero que este puntero de marco signifique la dirección del registro de activación de una función.

En este caso, ¿cuáles son el tipo de funciones para las que no es necesario mantener el puntero del marco en un registro? Si obtengo esta información, intentaré diseñar la nueva función en base a eso (si es posible) porque si el puntero del marco no se mantiene en los registros, algunas instrucciones se omitirán en binario. Esto realmente mejorará notablemente el rendimiento en una aplicación donde hay muchas funciones.

rashok
fuente
5
Tener que depurar solo un volcado de código que se compiló con esta opción será suficiente para que elimine esta opción de sus archivos MAKE. Por cierto, no elimina ninguna instrucción, solo le da al optimizador un registro más para trabajar con el almacenamiento.
Hans Passant
1
@HansPassant En realidad, es bastante útil para versiones de lanzamiento. Tener dos objetivos en un Makefile, Releasey en Debugrealidad es muy útil, tome esta opción como ejemplo.
Kotauskas
3
@VladislavToncharov Supongo que nunca has necesitado depurar un volcado de Releasememoria de un cliente que ejecuta tu compilación.
Andreas Magnusson

Respuestas:

60

La mayoría de las funciones más pequeñas no necesitan un puntero de marco; las funciones más grandes PUEDEN necesitarlo.

Realmente se trata de qué tan bien se las arregla el compilador para rastrear cómo se usa la pila y dónde están las cosas en la pila (variables locales, argumentos pasados ​​a la función actual y argumentos en preparación para una función a punto de ser llamada). No creo que sea fácil caracterizar las funciones que necesitan o no necesitan un puntero de marco (técnicamente, NINGUNA función TIENE que tener un puntero de marco; es más un caso de "si el compilador considera necesario reducir la complejidad de otro código ").

No creo que debas "intentar hacer que las funciones no tengan un puntero de marco" como parte de tu estrategia de codificación; como dije, las funciones simples no las necesitan, así que úsalas -fomit-frame-pointery obtendrás un registro más disponible. para el asignador de registros y guarde 1-3 instrucciones sobre la entrada / salida a las funciones. Si su función necesita un puntero de marco, es porque el compilador decide que es una mejor opción que no usar un puntero de marco. No es un objetivo tener funciones sin un puntero de marco, es un objetivo tener un código que funcione correctamente y rápidamente.

Tenga en cuenta que "no tener un puntero de marco" debería ofrecer un mejor rendimiento, pero no es una fórmula mágica que ofrezca enormes mejoras, especialmente en x86-64, que ya tiene 16 registros para empezar. En x86 de 32 bits, dado que solo tiene 8 registros, uno de los cuales es el puntero de pila, y tomar otro como puntero de trama significa que se toma el 25% del espacio de registro. Cambiar eso al 12,5% es una gran mejora. Por supuesto, compilar para 64 bits también será de gran ayuda.

Mats Petersson
fuente
24
Normalmente, el compilador puede realizar un seguimiento de la profundidad de la pila por sí solo y no necesita un puntero de marco. La excepción es si la función usa allocaque mueve el puntero de la pila en una cantidad variable. La omisión del puntero de fotograma dificulta significativamente la depuración. Las variables locales son más difíciles de localizar y las trazas de la pila son mucho más difíciles de reconstruir sin un puntero de marco para ayudar. Además, acceder a los parámetros puede resultar más caro, ya que están lejos de la parte superior de la pila y pueden requerir modos de direccionamiento más costosos.
Raymond Chen
3
Sí, entonces, asumiendo que no estamos usando alloca[¿quién lo hace? - Estoy 99% seguro de que nunca he escrito código que use alloca] o variable size local arrays[que es una forma moderna de alloca], entonces el compilador PUEDE decidir que usar frame-pointer es una mejor opción, porque los compiladores están escritos para no seguir ciegamente el opciones dadas, pero le dan las mejores opciones.
Mats Petersson
6
@MatsPetersson VLA son diferentes de alloca: se desechan tan pronto como abandona el ámbito en el que están declarados, mientras que el allocaespacio solo se libera cuando abandona la función. Esto hace que VLA sea mucho más fácil de seguir que alloca, creo.
Jens Gustedt
35
Quizás valga la pena mencionar que gcc tiene activado -fomit-frame-pointerde forma predeterminada para x86-64.
zwol
5
@JensGustedt, el problema no es cuando se desechan, el problema es que su tamaño (como alloca'espacio ed) es desconocido en el momento de la compilación . Por lo general, el compilador usará el puntero del marco para obtener la dirección de las variables locales, si el tamaño del marco de la pila no cambia, puede ubicarlas en un desplazamiento fijo del puntero de la pila.
vonbrand
15

Se trata del registro BP / EBP / RBP en plataformas Intel. Este registro tiene como valor predeterminado el segmento de pila (no necesita un prefijo especial para acceder al segmento de pila).

El EBP es la mejor opción de registro para acceder a estructuras de datos, variables y espacio de trabajo asignado dinámicamente dentro de la pila. EBP se usa a menudo para acceder a elementos en la pila en relación con un punto fijo en la pila en lugar de en relación con el TOS actual. Por lo general, identifica la dirección base del marco de pila actual establecido para el procedimiento actual. Cuando se utiliza EBP como registro base en un cálculo de compensación, la compensación se calcula automáticamente en el segmento de pila actual (es decir, el segmento seleccionado actualmente por SS). Debido a que SS no tiene que especificarse explícitamente, la codificación de instrucciones en tales casos es más eficiente. EBP también se puede utilizar para indexar en segmentos direccionables a través de otros registros de segmento.

(fuente: http://css.csail.mit.edu/6.858/2017/readings/i386/s02_03.htm )

Dado que en la mayoría de las plataformas de 32 bits, el segmento de datos y el segmento de la pila son iguales, esta asociación de EBP / RBP con la pila ya no es un problema. Lo mismo ocurre con las plataformas de 64 bits: la arquitectura x86-64, introducida por AMD en 2003, ha eliminado en gran medida el soporte para la segmentación en el modo de 64 bits: cuatro de los registros de segmento: CS, SS, DS y ES están forzados a 0 Estas circunstancias de las plataformas x86 de 32 y 64 bits significan esencialmente que el registro EBP / RBP se puede utilizar, sin ningún prefijo, en las instrucciones del procesador que acceden a la memoria.

Entonces, la opción del compilador sobre la que escribiste permite que BP / EBP / RBP se use para otros medios, por ejemplo, para mantener una variable local.

Por "Esto evita las instrucciones para guardar, configurar y restaurar punteros de cuadro" se entiende evitar el siguiente código en la entrada de cada función:

o la enterinstrucción, que fue muy útil en los procesadores Intel 80286 y 80386.

Además, antes del retorno de la función, se usa el siguiente código:

o la leaveinstrucción.

Las herramientas de depuración pueden escanear los datos de la pila y utilizar estos datos de registro EBP insertados mientras se localizan call sites, es decir, para mostrar los nombres de la función y los argumentos en el orden en que se han llamado jerárquicamente.

Los programadores pueden tener preguntas sobre los marcos de pila no en un término amplio (que es una entidad única en la pila que sirve solo una llamada de función y mantiene la dirección de retorno, argumentos y variables locales) pero en un sentido estricto - cuando el término stack framesse menciona en el contexto de las opciones del compilador. Desde la perspectiva del compilador, un marco de pila es solo el código de entrada y salida de la rutina , que empuja un ancla a la pila, que también se puede usar para depurar y manejar excepciones. Las herramientas de depuración pueden escanear los datos de la pila y utilizar estos anclajes para realizar un seguimiento, mientras se ubican call sitesen la pila, es decir, para mostrar los nombres de la función en el orden en que se han llamado jerárquicamente.

Por eso es muy importante que un programador comprenda qué es un marco de pila en términos de opciones del compilador, porque el compilador puede controlar si generar este código o no.

En algunos casos, el compilador puede omitir el marco de la pila (código de entrada y salida para la rutina) y se accederá directamente a las variables a través del puntero de la pila (SP / ESP / RSP) en lugar del puntero base conveniente (BP / ESP / RSP). Las condiciones para que un compilador omita los marcos de pila para algunas funciones pueden ser diferentes, por ejemplo: (1) la función es una función hoja (es decir, una entidad final que no llama a otras funciones); (2) no se utilizan excepciones; (3) no se llaman rutinas con parámetros salientes en la pila; (4) la función no tiene parámetros.

Omitir marcos de pila (código de entrada y salida para la rutina) puede hacer que el código sea más pequeño y más rápido, pero también puede afectar negativamente la capacidad de los depuradores para rastrear los datos en la pila y mostrarlos al programador. Estas son las opciones del compilador que determinan bajo qué condiciones debe satisfacer una función para que el compilador le otorgue el código de entrada y salida del marco de pila. Por ejemplo, un compilador puede tener opciones para agregar dicho código de entrada y salida a las funciones en los siguientes casos: (a) siempre, (b) nunca, (c) cuando sea necesario (especificando las condiciones).

Volviendo de las generalidades a las particularidades: si usa la -fomit-frame-pointeropción del compilador GCC, puede ganar tanto en el código de entrada como en el de salida de la rutina, y en tener un registro adicional (a menos que ya esté activado de forma predeterminada, ya sea por sí mismo o implícitamente por otros opciones, en este caso, ya se está beneficiando de la ganancia de usar el registro EBP / RBP y no se obtendrá ninguna ganancia adicional especificando explícitamente esta opción si ya está activada implícitamente). Sin embargo, tenga en cuenta que en los modos de 16 y 32 bits, el registro BP no tiene la capacidad de acceder a partes de 8 bits como lo ha hecho AX (AL y AH).

Dado que esta opción, además de permitir que el compilador use EBP como un registro de propósito general en las optimizaciones, también evita la generación de códigos de entrada y salida para el marco de la pila, lo que complica la depuración, es por eso que la documentación de GCC establece explícitamente (inusualmente enfatizado con negrita estilo) que habilitar esta opción hace que la depuración sea imposible en algunas máquinas

También tenga en cuenta que otras opciones del compilador, relacionadas con la depuración u optimización, pueden activar -fomit-frame-pointero desactivar implícitamente la opción.

No encontré ninguna información oficial en gcc.gnu.org sobre cómo afectan otras opciones -fomit-frame-pointer en las plataformas x86 , https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html solo establece lo siguiente:

-O también activa -fomit-frame-pointer en máquinas donde hacerlo no interfiere con la depuración.

Por lo tanto, no está claro de la documentación en sí si -fomit-frame-pointerse activará si solo compila con una sola -Oopción en la plataforma x86. Puede probarse empíricamente, pero en este caso, los desarrolladores de GCC no se comprometen a no cambiar el comportamiento de esta opción en el futuro sin previo aviso.

Sin embargo, Peter Cordes ha señalado en los comentarios que existe una diferencia en la configuración predeterminada -fomit-frame-pointerentre las plataformas x86-16 y las plataformas x86-32 / 64.

Esta opción - -fomit-frame-pointer- también es relevante para Intel C ++ Compiler 15.0 , no solo para GCC:

Para el compilador Intel, esta opción tiene un alias /Oy.

Esto es lo que Intel escribió al respecto:

Estas opciones determinan si EBP se utiliza como un registro de propósito general en las optimizaciones. Las opciones -fomit-frame-pointer y / Oy permiten este uso. Las opciones -fno-omit-frame-pointer y / Oy- no lo permiten.

Algunos depuradores esperan que EBP se use como un puntero de marco de pila y no pueden producir un seguimiento de pila a menos que sea así. Las opciones -fno-omit-frame-pointer y / Oy- dirigen al compilador a generar código que mantiene y usa EBP como un puntero de marco de pila para todas las funciones, de modo que un depurador aún pueda producir un seguimiento de pila sin hacer lo siguiente:

Para -fno-omit-frame-pointer: desactivación de optimizaciones con -O0 For / Oy-: desactivación de optimizaciones / O1, / O2 u / O3 La opción -fno-omit-frame-pointer se establece cuando especifica la opción - O0 o la opción -g. La opción -fomit-frame-pointer se establece cuando especifica la opción -O1, -O2 u -O3.

La opción / Oy se establece cuando especifica la opción / O1, / O2 o / O3. Option / Oy- se establece cuando especifica la opción / Od.

El uso de la opción -fno-omit-frame-pointer o / Oy- reduce el número de registros de propósito general disponibles en 1 y puede resultar en un código ligeramente menos eficiente.

NOTA Para sistemas Linux *: actualmente existe un problema con el manejo de excepciones de GCC 3.2. Por lo tanto, el compilador Intel ignora esta opción cuando se instala GCC 3.2 para C ++ y el manejo de excepciones está activado (el valor predeterminado).

Tenga en cuenta que la cita anterior solo es relevante para el compilador Intel C ++ 15, no para GCC.

Maxim Masiutin
fuente
1
El código de 16 bits, y BP por defecto a SS en lugar de DS, no es realmente relevante para gcc. gcc -m16existe, pero ese es un caso especial extraño que básicamente hace código de 32 bits que se ejecuta en modo de 16 bits usando prefijos por todas partes. También tenga en cuenta que -fomit-frame-pointerse ha habilitado de forma predeterminada durante años en x86 -m32, y más tiempo que en x86-64 ( -m64).
Peter Cordes
@PeterCordes: gracias, he actualizado las ediciones de acuerdo con los problemas que ha planteado.
Maxim Masiutin