Domanda GCC può essere forzato a generare costruttori efficienti per oggetti allineati alla memoria?


Sto ottimizzando un costruttore chiamato in uno dei loop più interni della nostra app. La classe in questione è larga circa 100 byte, consiste in un gruppo di intS, floatS, bools, e banali strutture, e dovrebbe essere banalmente copiabile (ha un costruttore di default non banale, ma non un distruttore o funzioni virtuali). È costruito abbastanza spesso che ogni nanosecondo del tempo trascorso in questo ctor funziona a circa $ 6.000 di hardware server aggiuntivo che dobbiamo acquistare.

Tuttavia, trovo che GCC non sta emettendo codice molto efficiente per questo costruttore (anche con -O3 -march etc set). L'implementazione di GCC del costruttore, compilando i valori predefiniti tramite un elenco di inizializzatori, richiede circa 34ns per l'esecuzione. Se invece di questo costruttore predefinito utilizzo una funzione scritta a mano che scrive direttamente nello spazio di memoria dell'oggetto con una varietà di intrinseche SIMD e matematica dei puntatori, la costruzione richiede circa 8ns.

Posso ottenere GCC per emettere un costruttore efficiente per tali oggetti quando io __attribute__ essere allineati alla memoria sui confini del SIMD? O devo ricorrere alle tecniche della vecchia scuola come scrivere i miei inizializzatori di memoria in assembly?

Questo oggetto viene sempre costruito come locale nello stack, quindi non si applica nessun overhead nuovo / malloc.

Contesto:

Questa classe viene utilizzata costruendola sullo stack come variabile locale, scrivendo selettivamente alcuni campi con valori non predefiniti e quindi passandoli (per riferimento) a una funzione, che passa il suo riferimento a un altro e così via.

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);
}

Il costruttore ottimale qui funzionerebbe copiando da un'istanza "modello" statica nell'istanza appena costruita, idealmente usando gli operatori SIMD per lavorare 16 byte alla volta. Invece GCC fa esattamente la cosa sbagliata per OversimplifiedExample () - una serie di movimenti immediati per riempire la struttura byte per 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   
}

Ho provato a incoraggiare GCC a costruire un singolo 'modello predefinito' di questo oggetto, e quindi a copiarlo in massa nel costruttore predefinito, facendo un po 'di trucco con un costruttore nascosto' fittizio 'che ha reso l'esempio di base e poi avendo il predefinito basta 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) );
}

Ma questo ha generato anche Più lentamente codice rispetto all'impostazione predefinita di base con l'elenco di inizializzazione, a causa di alcune copie ridondanti dello stack.

Alla fine ho fatto ricorso alla scrittura di una funzione libera inline per fare il *this = DefaultExemplar passo, usando le caratteristiche intrinseche del compilatore e le ipotesi sull'allineamento della memoria da emettere pipeline  MOVDQA Opcode SSE2 che copiano la struct in modo efficiente. Questo mi ha dato lo spettacolo di cui ho bisogno, ma è icky. Pensavo che i miei giorni di scrittura degli inizializzatori in assembly fossero dietro di me, e preferirei che l'ottimizzatore di GCC emettesse il codice giusto in primo luogo.

C'è un modo in cui posso ottenere GCC per generare codice ottimale per il mio costruttore, alcune impostazioni del compilatore o aggiuntive __attribute__ Mi sono perso?

Questo è GCC 4.4 in esecuzione su Ubuntu. I flag del compilatore includono -m32 -march=core2 -O3 -fno-strict-aliasing -fPIC(tra gli altri). La portabilità è non una considerazione, e sono assolutamente disposto a sacrificare la conformità agli standard per le prestazioni qui.

I tempi sono stati eseguiti leggendo direttamente il contatore del tempo con rdtsc, per esempio misurando un ciclo di N OversimplifiedExample () chiama tra i campioni con la dovuta attenzione alla risoluzione del timer e alla cache e alla significatività statistica e così via.

Ho anche ottimizzato questo, riducendo il numero di siti di chiamata il più possibile, naturalmente, ma mi piacerebbe comunque sapere come ottenere in generale i migliori operatori da GCC.


26
2018-01-17 12:35


origine


risposte:


Ecco come lo farei. Non dichiarare alcun costruttore; invece, dichiara un Frobozz fisso che contiene valori predefiniti:

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;
  } ;

Quindi in OversimplifiedExample:

Frobozz params (DefaultFrobozz) ;

Con gcc -O3 (versione 4.5.2), l'inizializzazione di params riduce a:

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

che è tanto buono quanto in un ambiente a 32 bit.

Avvertimento: Ho provato questo con la versione g ++ a 64 bit 4.7.0 20110827 (sperimentale), e ha generato una sequenza esplicita di copie a 64 bit invece di uno spostamento di blocco. Il processore non consente rep movsq, ma mi aspetterei rep movsl per essere più veloce di una sequenza di carichi e negozi a 64 bit. Forse no. (Ma il -Os passa - ottimizza per lo spazio - usa a rep movsl istruzioni.) Comunque, prova questo e facci sapere cosa succede.

Modificato per aggiungere: Mi sbagliavo sul fatto che il processore non lo permettesse rep movsq. La documentazione di Intel dice che "Le istruzioni MOVS, MOVSB, MOVSW e MOVSD possono essere precedute dal prefisso REP", ma sembra che questo sia solo un problema tecnico della documentazione. In ogni caso, se lo faccio Frobozz abbastanza grande, quindi il compilatore a 64 bit genera rep movsq Istruzioni; quindi probabilmente sa cosa sta facendo.


8
2018-01-17 13:05