c++ - ¿Se puede obligar a GCC a generar constructores eficientes para objetos alineados con la memoria?
performance x86 (1)
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 estructuras int
s, float
s, bool
s y trivial, y debe ser trivialmente copiable (tiene un constructor predeterminado no trivial, pero no tiene funciones destructoras o virtuales). Se construye con la frecuencia suficiente para que cada nanosegundo de tiempo empleado en este ctor resulte en alrededor de $ 6,000 de hardware de servidor adicional que necesitamos comprar.
Sin embargo, encuentro que GCC no está emitiendo un código muy eficiente para este constructor (incluso con el conjunto -O3 -march
etc.). La implementación de GCC del constructor, que completa los valores predeterminados a través de una lista de inicializadores, requiere aproximadamente 34 ns 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ática de puntero, la construcción toma aproximadamente 8 ns.
¿Puedo hacer que GCC emita un constructor eficiente para tales objetos cuando los " __attribute__
" para que estén alineados con la memoria en los límites de 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 / malloc.
Contexto:
Esta clase se usa al construirla en la pila como una variable local, escribir de forma selectiva algunos campos con valores no predeterminados y luego pasarla (por referencia) a una función, que pasa su referencia a otra 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 óptimo aquí trabajaría copiando desde una instancia de "plantilla" estática a la instancia recién construida, idealmente utilizando operadores SIMD para trabajar 16 bytes a la vez. En su lugar, GCC hace exactamente lo que es incorrecto para OversimplifiedExample (): una serie de operaciones móviles inmediatas para completar la estructura byte por 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
}
Traté de alentar a GCC a construir una ''plantilla predeterminada'' única de este objeto, y luego copiarla de forma masiva en el constructor predeterminado, haciendo un poco de artimañas con un constructor ''ficticio'' oculto que hizo la base ejemplar y luego tener la por defecto simplemente cópialo:
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 generó un código aún más lento que el predeterminado básico con la lista de inicializadores, debido a algunas copias redundantes de la pila.
Finalmente, recurrí a escribir una función libre en línea para realizar el paso *this = DefaultExemplar
, usando intrínsecos del compilador y suposiciones sobre la alineación de la memoria para emitir MOVDQA SSE2 MOVDQA que copian la estructura de manera eficiente. Esto me dio el rendimiento que necesito, pero es icky. Pensé que mis días de escribir inicializadores en ensamblaje habían quedado atrás, y preferiría que el optimizador de GCC emitiera el código correcto en primer lugar.
¿Hay alguna forma en que pueda hacer que GCC genere un código óptimo para mi constructor, alguna configuración del compilador o un __attribute__
adicional que he echado de menos?
Esto es GCC 4.4 corriendo en Ubuntu. Los indicadores del compilador incluyen -m32 -march=core2 -O3 -fno-strict-aliasing -fPIC
(entre otros). La portabilidad no es una consideración, y estoy completamente dispuesto a sacrificar el cumplimiento de los estándares por el desempeño aquí.
Los tiempos se realizaron leyendo directamente el contador de marca de tiempo con rdtsc
, por ejemplo , midiendo un bucle de N OversimplifiedExample () entre las muestras con la debida atención a la resolución del temporizador y la memoria caché y la significación estadística, etc.
También optimicé esto al reducir la cantidad de sitios de llamadas lo más posible, por supuesto, pero todavía me gustaría saber cómo mejorar en general los proveedores de GCC.
Así es como lo haría. No declarar ningún constructor; en su lugar, declare un Frobozz fijo que contenga valores predeterminados:
const Frobozz DefaultFrobozz =
{
0, 1, -1, 0, // int na,nb,nc,nd;
false, true, false, // bool ba,bb,bc;
''a'', ''b'', ''c'', // char ca,cb,cc;
-1, 1.0 // float fa,fb;
} ;
Luego, en OversimplifiedExample
:
Frobozz params (DefaultFrobozz) ;
Con gcc -O3
(versión 4.5.2), la inicialización de params
reduce a:
leal -72(%ebp), %edi
movl $_DefaultFrobozz, %esi
movl $16, %ecx
rep movsl
que es tan bueno como se obtiene en un entorno de 32 bits.
Advertencia: Probé esto con la versión g ++ de 64 bits 4.7.0 20110827 (experimental), y generó una secuencia explícita de copias de 64 bits en lugar de un movimiento de bloque. El procesador no permite la rep movsq
, pero esperaría que la rep movsl
sea más rápida que una secuencia de cargas y almacenes de 64 bits. Talvez no. (Pero el interruptor -Os
- optimizar para el espacio - usa una instrucción de rep movsl
.) De todos modos, intente esto y háganos saber qué sucede.
Editado para agregar: Estaba equivocado acerca de que el procesador no permitiera que rep movsq
. La documentación de Intel dice "Las instrucciones MOVS, MOVSB, MOVSW y MOVSD pueden ir precedidas por el prefijo REP", pero parece que esto es solo una falla de documentación. En cualquier caso, si hago que Frobozz
sea Frobozz
suficientemente grande, entonces el compilador de 64 bits genera instrucciones de rep movsq
; así que probablemente sepa lo que está haciendo.