parameter c++ c variadic-functions inline-functions

c++ - parameter - Inclinación de funciones vararg



c++ parameter (4)

Mientras jugaba con la configuración de optimización, noté un fenómeno interesante: las funciones que toman un número variable de argumentos ( ... ) nunca parecían estar en línea. (Obviamente, este comportamiento es específico del compilador, pero lo he probado en un par de sistemas diferentes).

Por ejemplo, compilando el siguiente programa pequeño:

#include <stdarg.h> #include <stdio.h> static inline void test(const char *format, ...) { va_list ap; va_start(ap, format); vprintf(format, ap); va_end(ap); } int main() { test("Hello %s/n", "world"); return 0; }

Aparentemente, siempre aparecerá un símbolo de test (posiblemente alterado) que aparecerá en el ejecutable resultante (probado con Clang y GCC en los modos C y C ++ en MacOS y Linux). Si uno modifica la firma de test() para tomar una cadena simple que se pasa a printf() , la función está -O1 desde -O1 hacia arriba por ambos compiladores, como es de esperar.

Sospecho que esto tiene que ver con la magia vudú utilizada para implementar varargs, pero cómo exactamente esto se hace normalmente es un misterio para mí. ¿Puede alguien aclararme cómo los compiladores suelen implementar las funciones vararg y por qué esto aparentemente impide la integración?


Al menos en x86-64, el paso de var_args es bastante complejo (debido a los argumentos que pasan en los registros). Otras arquitecturas pueden no ser tan complejas, pero rara vez son triviales. En particular, puede ser necesario tener un marco de pila o un puntero de marco para referirse al obtener cada argumento. Este tipo de reglas bien puede impedir que el compilador incorpore la función.

El código para x86-64 incluye empujar todos los argumentos de enteros y 8 registros sse en la pila.

Esta es la función del código original compilado con Clang:

test: # @test subq $200, %rsp testb %al, %al je .LBB1_2 # BB#1: # %entry movaps %xmm0, 48(%rsp) movaps %xmm1, 64(%rsp) movaps %xmm2, 80(%rsp) movaps %xmm3, 96(%rsp) movaps %xmm4, 112(%rsp) movaps %xmm5, 128(%rsp) movaps %xmm6, 144(%rsp) movaps %xmm7, 160(%rsp) .LBB1_2: # %entry movq %r9, 40(%rsp) movq %r8, 32(%rsp) movq %rcx, 24(%rsp) movq %rdx, 16(%rsp) movq %rsi, 8(%rsp) leaq (%rsp), %rax movq %rax, 192(%rsp) leaq 208(%rsp), %rax movq %rax, 184(%rsp) movl $48, 180(%rsp) movl $8, 176(%rsp) movq stdout(%rip), %rdi leaq 176(%rsp), %rdx movl $.L.str, %esi callq vfprintf addq $200, %rsp retq

y desde gcc:

test.constprop.0: .cfi_startproc subq $216, %rsp .cfi_def_cfa_offset 224 testb %al, %al movq %rsi, 40(%rsp) movq %rdx, 48(%rsp) movq %rcx, 56(%rsp) movq %r8, 64(%rsp) movq %r9, 72(%rsp) je .L2 movaps %xmm0, 80(%rsp) movaps %xmm1, 96(%rsp) movaps %xmm2, 112(%rsp) movaps %xmm3, 128(%rsp) movaps %xmm4, 144(%rsp) movaps %xmm5, 160(%rsp) movaps %xmm6, 176(%rsp) movaps %xmm7, 192(%rsp) .L2: leaq 224(%rsp), %rax leaq 8(%rsp), %rdx movl $.LC0, %esi movq stdout(%rip), %rdi movq %rax, 16(%rsp) leaq 32(%rsp), %rax movl $8, 8(%rsp) movl $48, 12(%rsp) movq %rax, 24(%rsp) call vfprintf addq $216, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc

En clang para x86, es mucho más simple:

test: # @test subl $28, %esp leal 36(%esp), %eax movl %eax, 24(%esp) movl stdout, %ecx movl %eax, 8(%esp) movl %ecx, (%esp) movl $.L.str, 4(%esp) calll vfprintf addl $28, %esp retl

No hay nada que realmente impida que cualquiera de los códigos anteriores se incorpore como tal, por lo que parece que es simplemente una decisión de política sobre el escritor del compilador. Por supuesto, para una llamada a algo como printf , no tiene sentido optimizar un par de llamada / retorno para el costo de la expansión del código; después de todo, printf NO es una función corta.

(Una parte decente de mi trabajo durante la mayor parte del año pasado ha sido implementar printf en un entorno OpenCL, así que sé que mucho más de lo que la mayoría de las personas incluso buscará sobre los especificadores de formato y varias otras partes difíciles de printf)

Edición: El compilador OpenCL que usamos en línea LLAMARÁ a las funciones var_args, por lo que es posible implementar tal cosa. No lo hará para las llamadas a printf, porque infla mucho el código, pero por defecto, nuestro compilador lo alinea TODO, todo el tiempo, sin importar lo que sea ... Y funciona, pero encontramos que tener 2-3 copias de printf en el código lo hacen REALMENTE enorme (con todo tipo de inconvenientes, incluida la generación final de código que se demora mucho debido a algunas malas elecciones de algoritmos en el compilador), por lo que tuvimos que agregar código para DETENER el compilador haciendo eso ...


El punto de alineación es que reduce la sobrecarga de llamadas a funciones.

Pero para varargs, hay muy poco que ganar en general.
Considere este código en el cuerpo de esa función:

if (blah) { printf("%d", va_arg(vl, int)); } else { printf("%s", va_arg(vl, char *)); }

¿Cómo se supone que el compilador debe integrarlo? Hacer eso requiere que el compilador empuje todo de la pila en el orden correcto de todos modos , aunque no se esté llamando a ninguna función. Lo único que se optimiza es un par de instrucciones call / ret (y tal vez presionando / haciendo estallar ebp y todo eso). Las manipulaciones de la memoria no se pueden optimizar, y los parámetros no se pueden pasar en los registros. Por lo tanto, es poco probable que gane algo notable al incorporar varargs.


La implementación de los argumentos variables generalmente tiene el siguiente algoritmo: tome la primera dirección de la pila que está después de la cadena de formato, y mientras analiza la cadena de formato de entrada, use el valor en la posición dada como el tipo de datos requerido. Ahora incremente el puntero de análisis de pila con el tamaño del tipo de datos requerido, avance en la cadena de formato y use el valor en la nueva posición como el tipo de datos requerido ... y así sucesivamente.

Algunos valores se convierten automáticamente (es decir, se promueven) a tipos "más grandes" (y esto es más o menos dependiente de la implementación), tales como char o short se promueve a int y float to double .

Ciertamente, no necesita una cadena de formato, pero en este caso necesita saber el tipo de argumentos pasados ​​(como: todas las entradas o todas las dobles, o las primeras 3 pulgadas, luego 3 más.).

Esta es la teoría corta.

Ahora, a la práctica, como lo muestra el comentario de nm anterior, gcc no hace funciones en línea que tengan manejo de argumentos variables. Es posible que haya operaciones bastante complejas en marcha mientras se manejan los argumentos variables, lo que aumentaría el tamaño del código a un tamaño no óptimo, por lo que simplemente no vale la pena incorporar estas funciones.

EDITAR:

Después de hacer una prueba rápida con VS2012, parece que no puedo convencer al compilador para que integre la función con los argumentos variables. Independientemente de la combinación de indicadores en la pestaña "Optimización" del proyecto, siempre hay una llamada a test y siempre hay un método de test . Y de hecho:

http://msdn.microsoft.com/en-us/library/z8y1yy88.aspx

dice que

Incluso con __forceinline, el compilador no puede en línea el código en todas las circunstancias. El compilador no puede alinear una función si: ...

  • La función tiene una lista de argumentos variable.

No espero que alguna vez sea posible alinear una función varargs, excepto en el caso más trivial.

Una función varargs que no tenía argumentos, o que no accedió a ninguno de sus argumentos, o que accedió solo a los argumentos fijos que preceden a los variables, podría ser incorporada reescribiéndola como una función equivalente que no usaba varargs. Este es el caso trivial.

Una función varargs que accede a sus diversos argumentos lo hace ejecutando el código generado por las macros va_start y va_arg , que se basan en los argumentos que se presentan en la memoria de alguna manera. Un compilador que realizó inlining simplemente para eliminar la sobrecarga de una llamada de función aún tendría que crear la estructura de datos para soportar esas macros. Un compilador que intentara eliminar toda la maquinaria de la función llamada tendría que analizar y optimizar también esas macros. Y aún fallaría si la función variadic hiciera una llamada a otra función que pasara va_list como un argumento.

No veo un camino viable para este segundo caso.