c++ compiler-specific

En C++, ¿por qué algunos compiladores se niegan a poner objetos en un registro que consisten de solo un doble?



compiler-specific (2)

En la sección 20 del C ++ efectivo de Scott Meyer, afirma:

algunos compiladores se niegan a poner objetos en un registro que consistan de solo un doble

Al pasar tipos incorporados por valor, los compiladores colocarán los datos en los registros y rápidamente enviarán ints / doubles / floats / etc. a lo largo. Sin embargo, no todos los compiladores tratarán los objetos pequeños con la misma gracia. Puedo entender fácilmente por qué los compiladores tratarían los Objetos de manera diferente: pasar un Objeto por valor puede ser mucho más trabajo que copiar miembros de datos entre la vtable y todos los constructores.

Pero aún. Esto parece ser un problema fácil de resolver para los compiladores modernos : "Esta clase es pequeña, quizás pueda tratarla de manera diferente". La declaración de Meyer parecía implicar que los compiladores harían esta optimización para los objetos que consisten solo en un int (o char o short ).

¿Alguien puede dar más información sobre por qué esta optimización a veces no ocurre?


Aquí hay un ejemplo que muestra que el ruido de LLVM con el nivel de optimización O3 trata a una clase con un solo miembro doble de datos como si fuera un doble:

$ cat main.cpp #include <stdio.h> class MyDouble { public: double d; MyDouble(double _d):d(_d){} }; void foo(MyDouble d) { printf("%lg/n",d.d); } int main(int argc, char **argv) { if (argc>5) { double x=(double)argc; MyDouble d(x); foo(d); } return 0; }

Cuando lo compilo y veo el archivo de código de bits generado, veo que foo se comporta como si operara en un parámetro de entrada de tipo double :

$ clang++ -O3 -c -emit-llvm main.cpp $ llvm-dis main.bc

Aquí está la parte relevante:

; Function Attrs: nounwind uwtable define void @_Z3foo8MyDouble(double %d.coerce) #0 { entry: %call = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([5 x i8]* @.str, i64 0, i64 0), double %d.coerce) ret void }

Vea cómo foo declara que su parámetro de entrada es double , y lo mueve para imprimir `` como está ". Ahora compilemos exactamente el mismo código con O0 :

$ clang++ -O0 -c -emit-llvm main.cpp $ llvm-dis main.bc

Cuando miramos la parte relevante, vemos que clang usa una instrucción getelementptr para acceder a su primer (y único) miembro de datos d :

; Function Attrs: uwtable define void @_Z3foo8MyDouble(double %d.coerce) #0 { entry: %d = alloca %class.MyDouble, align 8 %coerce.dive = getelementptr %class.MyDouble* %d, i32 0, i32 0 store double %d.coerce, double* %coerce.dive, align 1 %d1 = getelementptr inbounds %class.MyDouble* %d, i32 0, i32 0 %0 = load double* %d1, align 8 %call = call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([5 x i8]* @.str, i32 0, i32 0), double %0) ret void }


Encontré este documento en línea en " Convenciones de llamada para diferentes compiladores y sistemas operativos de C ++ " (actualizado el 2018-04-25) que tiene una tabla que muestra "Métodos para pasar objetos de estructura, clase y unión".

En la tabla puede ver que si un objeto contiene long double , la copia de todo el objeto se transfiere a la pila para todos los compiladores que se muestran aquí.

También del mismo recurso (con énfasis añadido):

Hay varios métodos diferentes para transferir un parámetro a una función si el parámetro es una estructura, una clase o un objeto de unión. Siempre se hace una copia del objeto, y esta copia se transfiere a la función llamada en los registros, en la pila o mediante un puntero, como se especifica en la tabla 6. Los símbolos en la tabla especifican qué método usar. S tiene prioridad sobre I y R. PI y PS tienen prioridad sobre todos los demás métodos de paso.

Como se indica en la tabla 6, un objeto no puede transferirse a los registros si es demasiado grande o demasiado complejo. Por ejemplo, un objeto que tiene un constructor de copia no puede transferirse a los registros porque el constructor de copia necesita una dirección del objeto. El constructor de copia es llamado por el llamante, no por el llamante.

Los objetos pasados ​​en la pila están alineados por el tamaño de palabra de la pila, incluso si se desea una mayor alineación. Los objetos pasados ​​por los punteros no están alineados por ninguno de los compiladores estudiados, incluso si la alineación se solicita explícitamente. El ABI de Windows de 64 bits requiere que los objetos pasados ​​por los punteros se alineen en 16.

Una matriz no se trata como un objeto sino como un puntero, y no se realiza ninguna copia de la matriz, excepto si la matriz está envuelta en una estructura, clase o unión.

Los compiladores de 64 bits para Linux difieren de ABI (versión 0.97) en los siguientes aspectos: los objetos con herencia, las funciones de miembro o los constructores se pueden pasar a los registros. Los objetos con constructor de copia, destructor o virtual son pasados ​​por punteros en lugar de en la pila.

Los compiladores de Intel para Windows son compatibles con Microsoft. Los compiladores Intel para Linux son compatibles con Gnu.