2013-01-18 3 views
7

Ich bin Optimierungen für meine 3D-Berechnungen zu entwickeln, und ich habe jetzt:haben verschiedene Optimierungen (plain, SSE, AVX) in der gleichen ausführbaren Datei mit C/C++

  • eine "plain" Version der Standard-C unter Verwendung von Sprachbibliotheken,
  • eine SSE optimierte Version, die #define USE_SSE,
  • eine AVX optimierte Version, die kompiliert mit einem Prä-Prozessor #define USE_AVX
mit einem Prä-Prozessor kompiliert

Ist es möglich, zwischen den 3 Versionen zu wechseln, ohne verschiedene ausführbare Dateien kompilieren zu müssen (z. verschiedene Bibliotheksdateien haben und die "richtige" dynamisch laden, weiß nicht ob inline Funktionen "richtig" dafür sind)? Ich würde auch Leistungen in dieser Art von Schalter in der Software berücksichtigen.

+1

Keine Erwähnung der Plattform? Einige Plattformen werden es ablehnen, Code mit avx auszuführen, selbst wenn Sie wissen, dass diese Anweisungen niemals aufgerufen werden. Einige Plattformen haben zur Laufzeit die Auswahl zwischen mehreren Implementierungen. Einige Plattformen suchen nach freigegebenen Bibliotheken in Pfaden, die von den Funktionen abhängen. –

Antwort

5

Eine Möglichkeit besteht darin, drei Bibliotheken zu implementieren, die der gleichen Schnittstelle entsprechen. Mit dynamischen Bibliotheken können Sie einfach die Bibliotheksdatei austauschen, und die ausführbare Datei verwendet alles, was sie findet. Zum Beispiel unter Windows, könnten Sie drei DLLs kompilieren:

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

Und dann den ausführbaren Link gegen Impl.dll machen. Jetzt legen Sie einfach eine der drei spezifischen DLLs in das gleiche Verzeichnis wie die .exe, benennen Sie sie in Impl.dll um, und sie wird diese Version verwenden. Das gleiche Prinzip sollte grundsätzlich auf ein UNIX-ähnliches Betriebssystem anwendbar sein.

Der nächste Schritt wäre die Bibliotheken programmatisch zu laden, was wahrscheinlich ist das flexibelste, aber es ist betriebssystemspezifische und erfordert etwas mehr Arbeit (wie die Bibliothek zu öffnen, zu erhalten Funktionszeiger etc.)

bearbeiten : Aber natürlich könnten Sie die Funktion einfach dreimal implementieren und zur Laufzeit einen auswählen, abhängig von einigen Parametern/Konfigurationseinstellungen etc., wie in den anderen Antworten beschrieben.

0

Natürlich ist es möglich.

Der beste Weg, es zu tun, ist, Funktionen zu haben, die die komplette Arbeit erledigen, und wählen Sie zwischen ihnen zur Laufzeit aus. Das würde funktionieren, aber nicht optimal:

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

bei jedem Durchlauf durch die Schleife, wird der Code eine switch Anweisung ausführen, die nur Overhead ist. Ein wirklich cleverer Compiler könnte es theoretisch für Sie reparieren, aber besser, es selbst zu reparieren.

Schreiben Sie stattdessen drei separate Funktionen, eine für Plain, eine für SSE und eine für AVX. Entscheide dann zur Laufzeit, welche man ausführen soll.

Für Bonuspunkte, in einem "Debug" -Build, führen Sie die Berechnung mit der SSE und der Ebene, und bestätigen Sie, dass die Ergebnisse nah genug sind, um Vertrauen zu geben. Schreibe die einfache Version, nicht für die Geschwindigkeit, aber für die Richtigkeit; Verwenden Sie dann die Ergebnisse, um zu überprüfen, ob Ihre cleveren optimierten Versionen die richtige Antwort erhalten.

Der legendäre John Carmack empfiehlt den letzteren Ansatz; Er nennt es "parallele Implementierungen". Lesen Sie his essay darüber.

Also ich empfehle Ihnen, zuerst die einfache Version zu schreiben. Gehen Sie dann zurück und beginnen Sie, Teile Ihrer Anwendung mithilfe der SSE- oder AVX-Beschleunigung neu zu schreiben, und stellen Sie sicher, dass die beschleunigten Versionen die richtigen Antworten geben. (Und manchmal, die einfache Version könnte einen Fehler haben, den die beschleunigte Version nicht hat. Zwei Versionen zu haben und sie zu vergleichen, hilft Fehler in beiden Versionen ans Licht zu bringen.)

+2

Wenn Sie über Optimierung nachdenken, bezweifle ich, dass Sie solche Überprüfungen innerhalb der Schleife durchführen möchten ... –

+0

Ja, Sie würden lieber die Schleife innerhalb der Funktionen platzieren, die für jeden 'switch'-Zweig aufgerufen werden. –

+1

Oder noch besser, haben eine Interface-Klasse, die erweitert und implementiert mit den 3 Optimierungen ... der polymorphe Schalter. –

6

Es gibt mehrere Lösungen dafür.

Einer basiert auf C++, wo Sie mehrere Klassen erstellen würden - normalerweise implementieren Sie eine Interface-Klasse und verwenden eine Factory-Funktion, um Ihnen ein Objekt der korrekten Klasse zu geben.

z.B.

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

Oder, wie oben vorgeschlagen, können Sie gemeinsam genutzte Bibliotheken verwenden, die eine gemeinsame Schnittstelle haben und dynamisch die Bibliothek laden, die richtige ist.

Natürlich, wenn Sie die Matrix-Basisklasse als Ihre "einfache" Klasse implementieren, können Sie stufenweise verfeinern und nur die Teile implementieren, die Sie tatsächlich finden, und sich auf die Basisklasse verlassen, um die Funktionen zu implementieren, wo Leistung nicht ist. t hochkarätig.

Edit: Sie sprechen über Inline, und ich denke, Sie sind auf der falschen Ebene der Funktion suchen, wenn das der Fall ist. Sie möchten ziemlich große Funktionen, die etwas mit ziemlich vielen Daten machen. Sonst wird Ihre gesamte Anstrengung darauf verwendet, die Daten in das richtige Format zu bringen und dann ein paar Rechenanweisungen zu machen und die Daten dann wieder in den Speicher zu schreiben.

Ich würde auch darüber nachdenken, wie Sie Ihre Daten speichern. Speichern Sie Sets eines Arrays mit X, Y, Z, W oder speichern Sie viele X, viele Y, viele Z und viele W in separaten Arrays (vorausgesetzt, wir machen 3D-Berechnungen)? Abhängig davon, wie Ihre Berechnung funktioniert, können Sie feststellen, dass Sie den einen oder anderen Vorteil haben.

Ich habe ein gutes Stück SSE und 3DNow gemacht! Optimierungen vor einigen Jahren, und der "Trick" ist oft mehr darüber, wie Sie die Daten speichern, damit Sie auf einfache Weise ein "Bündel" der richtigen Art von Daten auf einmal erfassen können. Wenn Sie die Daten falsch gespeichert haben, verschwenden Sie viel Zeit mit dem "Swizzling von Daten" (Daten werden von einer Speichermethode auf eine andere übertragen).

+0

+1 für die schrittweise Verfeinerung –

+0

Das Problem mit diesem Ansatz ist, dass Sie die verschiedenen Funktionen für verschiedene Architekturen nicht kompilieren und optimieren können. Wenn alles mit say '-march = i7' kompiliert wird, läuft selbst die C-Version nur auf einem i7, wenn Sie mit '-march = i686' kompilieren, wird es auf jedem Rechner laufen, der in den letzten 15 Jahren gebaut wurde. wie SSE/AVX) wird nicht verfügbar sein und das Optimierungsprogramm wird nur eine Teilmenge der verfügbaren Anweisungen in der SSE/AVX-Version verwenden. – hirschhornsalz

+0

So erstellen Sie den Code in separaten Quelldateien.Obwohl ich finde, dass, wenn Sie wirklich SSE/AVX Anweisungen auf eine wirklich gute Weise verwenden möchten, Sie Inline-Assembler verwenden müssen. Üblicherweise macht es der Compiler nicht gut, "clever zu sein". –