Estoy optimizando un constructor que se llama en uno de los bucles más internos de nuestra aplicación. La clase en cuestión tiene aproximadamente 100 bytes de ancho, consiste en un grupo de int
s, float
s, bool
sy estructuras triviales, y debe poder copiarse trivialmente (tiene un constructor no trivial predeterminado, pero no destructor ni funciones virtuales). Se construye con la suficiente frecuencia para que cada nanosegundo de tiempo empleado en esta herramienta resulte en alrededor de $ 6,000 de hardware de servidor adicional que necesitamos comprar.¿Se puede obligar a GCC a generar constructores eficientes para objetos alineados con memoria?
Sin embargo, me parece que GCC no emite un código muy eficiente para este constructor (incluso con el conjunto -O3 -march
etc.). La implementación del constructor de GCC, que completa los valores predeterminados a través de una lista de inicializadores, requiere aproximadamente 34ns para ejecutarse. Si en lugar de este constructor predeterminado utilizo una función escrita a mano que escribe directamente en el espacio de memoria del objeto con una variedad de intrínsecos SIMD y matemáticas de puntero, la construcción requiere aproximadamente 8ns.
¿Puedo hacer que GCC emita un constructor eficiente para tales objetos cuando I __attribute__
estén alineados con la memoria en los límites SIMD? ¿O debo recurrir a técnicas de la vieja escuela como escribir mis propios inicializadores de memoria en ensamblaje?
Este objeto solo se construye como local en la pila, por lo que no se aplica ninguna sobrecarga nueva o malloc.
Contexto:
Esta clase se utiliza construyéndolo en la pila como una variable local, escribiendo de forma selectiva algunos campos con valores no predeterminados, y luego pasándolo (referencia) a una función, que pasa su referencia a otro y así sucesivamente.
struct Trivial {
float x,y,z;
Trivial() : x(0), y(0), z(0) {};
};
struct Frobozz
{
int na,nb,nc,nd;
bool ba,bb,bc;
char ca,cb,cc;
float fa,fb;
Trivial va, vb; // in the real class there's several different kinds of these
// and so on
Frobozz() : na(0), nb(1), nc(-1), nd(0),
ba(false), bb(true), bc(false),
ca('a'), cb('b'), cc('c'),
fa(-1), fb(1.0) // etc
{}
} __attribute__((aligned(16)));
// a pointer to a func that takes the struct by reference
typedef int (*FrobozzSink_t)(Frobozz&);
// example of how a function might construct one of the param objects and send it
// to a sink. Imagine this is one of thousands of event sources:
int OversimplifiedExample(int a, float b)
{
Frobozz params;
params.na = a; params.fb = b; // other fields use their default values
FrobozzSink_t funcptr = AssumeAConstantTimeOperationHere();
return (*funcptr)(params);
}
El constructor óptima aquí funcionaría mediante la copia de una instancia estática "plantilla" en el ejemplo recién construido, idealmente usando operadores SIMD a trabajar 16 bytes a la vez. En cambio, GCC hace exactamente lo incorrecto para OversimplifiedExample() — una serie de movimientos inmediatos para completar la estructura byte a byte.
// from objdump -dS
int OversimplifiedExample(int a, float b)
{
a42:55 push %ebp
a43:89 e5 mov %esp,%ebp
a45:53 push %ebx
a46:e8 00 00 00 00 call a4b <_Z21OversimplifiedExampleif+0xb>
a4b:5b pop %ebx
a4c:81 c3 03 00 00 00 add $0x3,%ebx
a52:83 ec 54 sub $0x54,%esp
// calling the 'Trivial()' constructors which move zero, word by word...
a55:89 45 e0 mov %eax,-0x20(%ebp)
a58:89 45 e4 mov %eax,-0x1c(%ebp)
a5b:89 45 e8 mov %eax,-0x18(%ebp)
a5e:89 45 ec mov %eax,-0x14(%ebp)
a61:89 45 f0 mov %eax,-0x10(%ebp)
a64:89 45 f4 mov %eax,-0xc(%ebp)
// filling out na/nb/nc/nd..
a67:c7 45 c4 01 00 00 00 movl $0x1,-0x3c(%ebp)
a71:c7 45 c8 ff ff ff ff movl $0xffffffff,-0x38(%ebp)
a78:89 45 c0 mov %eax,-0x40(%ebp)
a7b:c7 45 cc 00 00 00 00 movl $0x0,-0x34(%ebp)
a82:8b 45 0c mov 0xc(%ebp),%eax
// doing the bools and chars by moving one immediate byte at a time!
a85:c6 45 d0 00 movb $0x0,-0x30(%ebp)
a89:c6 45 d1 01 movb $0x1,-0x2f(%ebp)
a8d:c6 45 d2 00 movb $0x0,-0x2e(%ebp)
a91:c6 45 d3 61 movb $0x61,-0x2d(%ebp)
a95:c6 45 d4 62 movb $0x62,-0x2c(%ebp)
a99:c6 45 d5 63 movb $0x63,-0x2b(%ebp)
// now the floats...
a9d:c7 45 d8 00 00 80 bf movl $0xbf800000,-0x28(%ebp)
aa4:89 45 dc mov %eax,-0x24(%ebp)
// FrobozzSink_t funcptr = GetFrobozz();
aa7:e8 fc ff ff ff call aa8 <_Z21OversimplifiedExampleif+0x68>
// return (*funcptr)(params);
aac:8d 55 c0 lea -0x40(%ebp),%edx
aaf:89 14 24 mov %edx,(%esp)
ab2:ff d0 call *%eax
ab4:83 c4 54 add $0x54,%esp
ab7:5b pop %ebx
ab8:c9 leave
ab9:c3 ret
}
Me trataron de animar a GCC para construir un solo 'plantilla por defecto' de este objeto, y después de copia masiva en el constructor por defecto, haciendo un poco de engaño con un constructor oculto 'ficticia' que hizo teniendo la base ejemplar y el valor por defecto simplemente copiarlo:
struct Frobozz
{
int na,nb,nc,nd;
bool ba,bb,bc;
char ca,cb,cc;
float fa,fb;
Trivial va, vb;
inline Frobozz();
private:
// and so on
inline Frobozz(int dummy) : na(0), /* etc etc */ {}
} __attribute__((aligned(16)));
Frobozz::Frobozz()
{
const static Frobozz DefaultExemplar(69105);
// analogous to copy-on-write idiom
*this = DefaultExemplar;
// or:
// memcpy(this, &DefaultExemplar, sizeof(Frobozz));
}
Pero esto genera aún más lenta código de la predeterminada básico con lista de inicialización, debido a alguna copia pila redundante.
Finalmente recurrió a la escritura de una función inline libre para hacer el paso *this = DefaultExemplar
, utilizando las características intrínsecas del compilador y suposiciones sobre la alineación de memoria para emitir pipelinedMOVDQA códigos de operación SSE2 que copian la estructura de manera eficiente. Esto me dio el rendimiento que necesito, pero es asqueroso. Pensé que mis días de escribir inicializadores en ensamblaje habían quedado atrás, y realmente preferiría que el optimizador de GCC emitiera el código correcto en primer lugar.
¿Hay alguna manera en que pueda hacer que GCC genere código óptimo para mi constructor, alguna configuración de compilador o __attribute__
adicional que me he perdido?
Esto es GCC 4.4 ejecutándose en Ubuntu.Los indicadores del compilador incluyen -m32 -march=core2 -O3 -fno-strict-aliasing -fPIC
(entre otros). La portabilidad es no una consideración, y estoy totalmente dispuesto a sacrificar el cumplimiento de las normas para el rendimiento aquí.
Timings se realizaron mediante la lectura directamente el contador de sello de tiempo con rdtsc
, por ejemplo medición de un bucle de N OversimplifiedExample() llama entre las muestras con la debida atención a la resolución del temporizador y la memoria caché y la significación estadística y así sucesivamente.
También he optimizado mediante la reducción del número de sitios de llamadas tanto como sea posible, por supuesto, pero todavía me gustaría saber cómo conseguir generalmente mejor ctors de GCC.
¿Has probado un GCC más reciente, como 4.6.2 (o la última instantánea del lanzamiento de 4.7)? –
¿Se puede omitir la definición del constructor y escribirla completamente a mano en asm? Arriesgado y difícil de mantener, pero por 34 * $ 6000 se pagaría por sí mismo Sospecho – Flexo
¿También ha intentado agregar algunas de las diversas banderas '-msse'? Creo que son necesarios para SSE en ciertos casos. También sugiero que simplemente obtenga un gcc reciente y explore su página de manual, pensando si cada opción puede mejorar su situación y luego pruébelo. – PlasmaHH