2013-01-18 10 views
7

sto sviluppando ottimizzazioni per i miei calcoli 3D e ora ho:hanno diverse ottimizzazioni (pianura, SSE, AVX) nello stesso eseguibile con C/C++

  • una versione "plain" utilizzando lo standard C librerie di lingua,
  • un SSE versione ottimizzata che compila utilizzando un preprocessore #define USE_SSE,
  • un AVX versione ottimizzata che compila utilizzando un preprocessore #define USE_AVX

E 'possibile passare tra le 3 versioni senza dover compilare diversi eseguibili (es. avendo diversi file di libreria e caricando il "giusto" uno dinamicamente, non so se le funzioni inline sono "giuste" per quello)? Considererei anche le prestazioni nell'avere questo tipo di switch nel software.

+1

Nessuna menzione della piattaforma? Alcune piattaforme rifiuteranno di eseguire codice usando avx anche se sapete che quelle istruzioni non verranno mai chiamate. Alcune piattaforme hanno ifunc per selezionare tra diverse implementazioni in fase di esecuzione. Alcune piattaforme cercano librerie condivise in percorsi che dipendono dalle capacità. –

risposta

5

Un modo è quello di implementare tre librerie conformi alla stessa interfaccia. Con le librerie dinamiche, puoi semplicemente scambiare il file della libreria e l'eseguibile userà qualsiasi cosa trovi. Ad esempio, su Windows, si potrebbe compilare tre DLL:

  • PlainImpl.dll
  • SSEImpl.dll
  • AVXImpl.dll

E poi fare il collegamento eseguibile contro Impl.dll. Ora è sufficiente inserire una delle tre DLL specifiche nella stessa directory di .exe, rinominarla in Impl.dll e utilizzerà tale versione. Lo stesso principio dovrebbe essere fondamentalmente applicabile su un sistema operativo simile a UNIX.

Il passo successivo sarebbe quello di caricare le librerie di programmazione, che è probabilmente il più flessibile, ma è operativo specifico e richiede un po 'più di lavoro (come l'apertura della biblioteca, ottenendo puntatori a funzione etc.)

Edit : Ma ovviamente, è possibile implementare la funzione tre volte e selezionarne una in fase di esecuzione, in base a qualche parametro/configurazione del file di configurazione ecc., Come indicato nelle altre risposte.

0

Ovviamente è possibile.

Il modo migliore per farlo è disporre di funzioni che eseguono il lavoro completo e selezionarle tra loro in fase di runtime. Questo potrebbe funzionare ma non è ottimale:

typedef enum 
{ 
    calc_type_invalid = 0, 
    calc_type_plain, 
    calc_type_sse, 
    calc_type_avx, 
    calc_type_max // not a valid value 
} calc_type; 

void do_my_calculation(float const *input, float *output, size_t len, calc_type ct) 
{ 
    float f; 
    size_t i; 

    for (i = 0; i < len; ++i) 
    { 
     switch (ct) 
     { 
      case calc_type_plain: 
       // plain calculation here 
       break; 
      case calc_type_sse: 
       // SSE calculation here 
       break; 
      case calc_type_avx: 
       // AVX calculation here 
       break; 
      default: 
       fprintf(stderr, "internal error, unexpected calc_type %d", ct); 
       exit(1); 
       break 
     } 
    } 
} 

Su ogni passo del ciclo, il codice è in esecuzione un'istruzione switch, che è solo in testa. Un compilatore davvero intelligente potrebbe teoricamente risolverlo per te, ma è meglio aggiustarlo da solo.

Invece, scrivere tre funzioni separate, una per plain, una per SSE e una per AVX. Quindi decidere in fase di esecuzione quale eseguire.

Per i punti bonus, in una build di "debug", eseguire il calcolo sia con l'SSE che con il piano e asserire che i risultati sono abbastanza vicini da dare sicurezza. Scrivi la versione semplice, non per la velocità, ma per la correttezza; quindi utilizza i risultati per verificare che le tue versioni intelligenti ottimizzate ottengano la risposta corretta.

Il leggendario John Carmack raccomanda quest'ultimo approccio; lo chiama "implementazioni parallele". Leggi his essay a riguardo.

Quindi ti consiglio di scrivere prima la versione semplice. Quindi, torna indietro e inizia a riscrivere le parti dell'applicazione utilizzando l'accelerazione SSE o AVX e assicurati che le versioni accelerate forniscano le risposte corrette. (E a volte, la versione semplice potrebbe avere un bug che la versione accelerata non ha. Avere due versioni e confrontarle aiuta a far emergere bug in entrambe le versioni.)

+2

Se stai pensando all'ottimizzazione, dubito che tu voglia fare questi controlli all'interno del ciclo ... –

+0

Sì, preferirai posizionare il ciclo all'interno delle funzioni chiamate per ogni ramo 'switch'. –

+1

O, meglio ancora, avere una classe di interfaccia che viene estesa e implementata usando le 3 ottimizzazioni ... lo switch polimorfico. –

6

Ci sono diverse soluzioni per questo.

Uno è basato su C++, dove si creano più classi, in genere si implementa una classe di interfaccia e si utilizza una funzione di fabbrica per fornire un oggetto della classe corretta.

ad es.

class Matrix 
{ 
    virtual void Multiply(Matrix &result, Matrix& a, Matrix &b) = 0; 
    ... 
}; 

class MatrixPlain : public Matrix 
{ 
    void Multiply(Matrix &result, Matrix& a, Matrix &b); 

}; 


void MatrixPlain::Multiply(...) 
{ 
    ... implementation goes here... 
} 

class MatrixSSE: public Matrix 
{ 
    void Multiply(Matrix &result, Matrix& a, Matrix &b); 
} 

void MatrixSSE::Multiply(...) 
{ 
    ... implementation goes here... 
} 

... same thing for AVX... 

Matrix* factory() 
{ 
    switch(type_of_math) 
    { 
     case PlainMath: 
      return new MatrixPlain; 

     case SSEMath: 
      return new MatrixSSE; 

     case AVXMath: 
      return new MatrixAVX; 

     default: 
      cerr << "Error, unknown type of math..." << endl; 
      return NULL; 
    } 
} 

Oppure, come suggerito sopra, è possibile utilizzare le librerie condivise che dispongono di un'interfaccia comune e caricare dinamicamente la libreria che è giusto.

Ovviamente, se si implementa la classe di base Matrix come classe "normale", è possibile eseguire il perfezionamento graduale e implementare solo le parti effettivamente trovate utili e fare affidamento sulla classe di base per implementare le funzioni in cui la prestazione non è t altamente analitico.

Modifica: Parli di inline e penso che tu stia osservando il livello sbagliato di funzione, se così fosse. Vuoi funzioni abbastanza grandi che facciano qualcosa su un bel po 'di dati. In caso contrario, tutti i tuoi sforzi saranno spesi per preparare i dati nel formato corretto, quindi fare alcune istruzioni di calcolo e quindi riportare i dati in memoria.

Vorrei anche considerare come si memorizzano i dati. Stai memorizzando set di array con X, Y, Z, W o stai memorizzando molti X, molti Y, molti Z e molti W in array separati [presumendo che stiamo facendo calcoli 3D]? A seconda di come funziona il tuo calcolo, potresti scoprire che fare l'uno o l'altro modo ti darà il miglior beneficio.

Ho fatto un bel po 'di SSE e 3DNow! ottimizzazioni qualche anno fa, e il "trucco" è spesso più relativo al modo in cui si memorizzano i dati in modo da poter facilmente afferrare un "fascio" del giusto tipo di dati in un colpo solo. Se i dati sono archiviati nel modo sbagliato, si sprecherà molto del "tempo di swizzling dei dati" (spostando i dati da un modo di memorizzazione a un altro).

+0

+1 per il perfezionamento graduale –

+0

Il problema con questo approccio è che non è possibile compilare e ottimizzare le diverse funzioni per le diverse architetture. Se tutto è compilato con say '-march = i7' anche la versione C funzionerà solo su un i7, se si compila con' -march = i686' verrà eseguito su ogni macchina costruita negli ultimi 15 anni, ma alcuni intrinseci (come SSE/AVX) non sarà disponibile e l'ottimizzatore utilizzerà solo un sottoinsieme delle istruzioni disponibili nella versione SSE/AVX. – hirschhornsalz

+0

Quindi creare il codice in file di origine separati.Anche se trovo che se si vuole davvero fare uso delle istruzioni SSE/AVX in un modo veramente buono, è necessario utilizzare l'assemblatore in linea. Il compilatore di solito non fa un buon lavoro a "essere intelligente". –

Problemi correlati