2012-01-17 10 views
26

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.

+0

¿Has probado un GCC más reciente, como 4.6.2 (o la última instantánea del lanzamiento de 4.7)? –

+1

¿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

+1

¿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

Respuesta

8

Así es como lo haría. No declare ningún constructor; En su lugar, declarar una frobozz fijo que contiene 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 se reduce a:

leal -72(%ebp), %edi 
movl $_DefaultFrobozz, %esi 
movl $16, %ecx 
rep movsl 

que es casi tan bueno como se consigue en un entorno de 32 bits.

Advertencia: Intenté esto con la versión de 64 bits g ++ versión 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 rep movsq, pero esperaría que rep movsl fuera más rápido que una secuencia de cargas y almacenes de 64 bits. Talvez no. (Pero el interruptor -Os - optimiza para el espacio - utiliza una instrucción rep movsl.) De todos modos, prueba esto y cuéntanos qué pasa.

Editado para agregar: Estaba equivocado sobre el procesador que no permite 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 un error de documentación. En cualquier caso, si hago Frobozz lo suficientemente grande, entonces el compilador de 64 bits genera rep movsq instrucciones; entonces probablemente sabe lo que está haciendo.

+0

"No declare ningún constructor": puede declarar-privado-sin-definición (o eliminar) el constructor no-arg, para asegurarse de que nadie termine accidentalmente con un objeto no inicializado. Copian el valor predeterminado o usan una lista de inicializadores, pero no pueden simplemente escribir 'Frobozz params;'. Personalmente me sentiría más feliz con el código existente si el constructor predeterminado desapareciera por completo, en lugar de cambiar su comportamiento para hacer algo que no funciona ;-) –

+3

"pero esperaría que el rep movsd fuera más rápido que una secuencia de cargas y tiendas de 64 bits "hay un umbral donde las instrucciones' REP MOVS' generalmente serán más lentas. además, 'REP MOVS' requiere 3 registros explícitos' ECX', 'ESI' y' EDI', lo que puede provocar registros excesivos de mezcla/desbordamiento como apuestos para bloquear copias. – Necrolis

Cuestiones relacionadas