15 Stimmen

Nutzung der Vorteile von SSE und anderen CPU-Erweiterungen

In meiner Codebasis gibt es einige Stellen, an denen derselbe Vorgang für einen großen Datensatz sehr oft wiederholt wird. In einigen Fällen dauert es sehr lange, diese zu verarbeiten.

Ich glaube, dass die Verwendung von SSE zur Implementierung dieser Schleifen ihre Leistung erheblich verbessern sollte, insbesondere wenn viele Operationen mit demselben Datensatz durchgeführt werden, so dass nach dem ersten Einlesen der Daten in den Cache keine Cache-Misses mehr auftreten sollten. Ich bin mir jedoch nicht sicher, wie ich das angehen soll.

  • Gibt es eine compiler- und betriebssystemunabhängige Möglichkeit, den Code so zu schreiben, dass er die Vorteile der SSE-Befehle nutzt? Ich mag die VC++ intrinsics, die SSE-Operationen enthalten, aber ich habe keine compilerübergreifenden Lösungen gefunden.

  • Ich muss noch einige CPUs unterstützen, die entweder keine oder nur eingeschränkte SSE-Unterstützung haben (z. B. Intel Celeron). Gibt es eine Möglichkeit zu vermeiden, dass ich verschiedene Versionen des Programms erstellen muss, z. B. mit einer Art "Laufzeit-Linker", der entweder den Basis- oder den SSE-optimierten Code einbindet, je nachdem, auf welcher CPU er beim Start des Prozesses läuft?

  • Wie sieht es mit anderen CPU-Erweiterungen aus? Ein Blick auf die Befehlssätze verschiedener Intel- und AMD-CPUs zeigt, dass es ein paar davon gibt.

7voto

Michael Burr Punkte 320591

Für Ihren zweiten Punkt gibt es mehrere Lösungen, solange Sie die Unterschiede in verschiedene Funktionen aufteilen können:

  • einfache alte C-Funktionszeiger
  • dynamische Verknüpfung (die im Allgemeinen auf C-Funktionszeigern beruht)
  • Wenn Sie C++ verwenden, können verschiedene Klassen, die die Unterstützung für verschiedene Architekturen darstellen, und die Verwendung virtueller Funktionen eine große Hilfe sein.

Da Sie sich auf indirekte Funktionsaufrufe verlassen, müssen die Funktionen, die die verschiedenen Operationen abstrahieren, im Allgemeinen eine etwas höhere Funktionalität darstellen, da sonst die Vorteile der optimierten Anweisung im Aufruf-Overhead verloren gehen (mit anderen Worten: Abstrahieren Sie nicht die einzelnen SSE-Operationen, sondern die Arbeit, die Sie tun).

Hier ein Beispiel für die Verwendung von Funktionszeigern:

typedef int (*scale_func_ptr)( int scalar, int* pData, int count);

int non_sse_scale( int scalar, int* pData, int count)
{
    // do whatever work needs done, without SSE so it'll work on older CPUs

    return 0;
}

int sse_scale( int scalar, in pData, int count)
{
    // equivalent code, but uses SSE

    return 0;
}

// at initialization

scale_func_ptr scale_func = non_sse_scale;

if (useSSE) {
    scale_func = sse_scale;
}

// now, when you want to do the work:

scale_func( 12, theData_ptr, 512);  // this will call the routine that tailored to SSE 
                                    // if the CPU supports it, otherwise calls the non-SSE
                                    // version of the function

6voto

Juraj Punkte 850

Gute Lektüre zu diesem Thema: Beendigung des Befehlssatzkrieges

Kurzer Überblick: Leider ist es nicht möglich, Ihr Problem auf einfache und möglichst kompatible (Intel vs. AMD) Weise zu lösen.

4voto

Nils Pipenbrinck Punkte 80152

Die SSE-Intrinsics funktionieren mit Visual C++, GCC und dem Intel-Compiler. Es ist kein Problem, sie heutzutage zu verwenden.

Beachten Sie, dass Sie immer eine Version Ihres Codes aufbewahren sollten, die SSE nicht verwendet, und diese ständig mit Ihrer SSE-Implementierung abgleichen sollten.

Dies hilft nicht nur bei der Fehlersuche, sondern ist auch nützlich, wenn Sie CPUs oder Architekturen unterstützen wollen, die Ihre benötigten SSE-Versionen nicht unterstützen.

3voto

jalf Punkte 235501

Als Antwort auf Ihren Kommentar:

Solange ich also nicht versuche, Code auszuführen, der nicht unterstützte Anweisungen enthält, ist alles in Ordnung, und ich könnte mit einem Schalter vom Typ "if(see2Supported){...}else{...}" auskommen?

Kommt darauf an. Es ist in Ordnung, wenn SSE-Befehle in der Binärdatei vorhanden sind, solange sie nicht ausgeführt werden. Die CPU hat damit kein Problem.

Wenn Sie jedoch die SSE-Unterstützung im Compiler aktivieren, wird dieser höchstwahrscheinlich eine Reihe "normaler" Anweisungen gegen ihre SSE-Entsprechungen austauschen (z. B. skalare Gleitkommaoperationen), so dass sogar Teile Ihres normalen Nicht-SSE-Codes auf einer CPU, die dies nicht unterstützt, explodieren werden.

Sie müssen also höchstwahrscheinlich eine oder zwei separate Dateien mit aktiviertem SSE kompilieren, die alle Ihre SSE-Routinen enthalten. Dann verknüpfen Sie diese mit dem Rest der Anwendung, die ohne SSE-Unterstützung kompiliert wird.

1voto

gavinb Punkte 18143

Anstatt eine alternative SSE-Implementierung für Ihren skalaren Code von Hand zu programmieren, empfehle ich Ihnen dringend, einen Blick auf OpenCL . Es handelt sich um ein herstellerneutrales, portables, plattformübergreifendes System für rechenintensive Anwendungen (und es ist in hohem Maße "buzzword-konform"!). Sie können Ihren Algorithmus in einer Teilmenge von C99 schreiben, die für vektorisierte Operationen ausgelegt ist, was viel einfacher ist als SSE von Hand zu programmieren. Und das Beste ist, dass OpenCL zur Laufzeit die beste Implementierung generiert, die dann entweder auf der GPU ou auf der CPU. Sie bekommen also im Grunde den SSE-Code für Sie geschrieben.

In meinem Code gibt es einige Stellen, an denen derselbe Vorgang sehr oft für einen großen Datensatz wiederholt wird. In einigen Fällen dauert es sehr lange, diese zu verarbeiten.

Ihre Anwendung klingt nach genau der Art von Problem, für das OpenCL entwickelt wurde. Das Schreiben von alternativen Funktionen in SSE würde die Ausführungsgeschwindigkeit sicherlich verbessern, ist aber mit viel Arbeit verbunden, um sie zu schreiben und zu debuggen.

Gibt es eine compiler- und betriebssystemunabhängige Möglichkeit, den Code so zu schreiben, dass er die Vorteile der SSE-Befehle nutzt? Ich mag die VC++ intrinsics, die SSE-Operationen enthalten, aber ich habe keine compilerübergreifenden Lösungen gefunden.

Ja. Die SSE-Intrinsics wurden von Intel im Wesentlichen standardisiert, so dass die gleichen Funktionen unter Windows, Linux und Mac (insbesondere mit Visual C++ und GNU g++) gleich funktionieren.

Ich muss noch einige CPUs unterstützen, die entweder keine oder nur begrenzte SSE-Unterstützung haben (z. B. Intel Celeron). Gibt es eine Möglichkeit zu vermeiden, dass ich verschiedene Versionen des Programms erstellen muss, z. B. mit einer Art "Laufzeit-Linker", der entweder den Basis- oder den SSE-optimierten Code einbindet, je nachdem, auf welcher CPU er beim Start des Prozesses läuft?

Sie könnten dies tun (z. B. mit dlopen() ), aber es ist eine sehr komplexe Lösung. Viel einfacher wäre es (in C), eine Funktionsschnittstelle zu definieren und die entsprechende Version der optimierten Funktion über einen Funktionszeiger aufzurufen, oder in C++ verschiedene Implementierungsklassen zu verwenden, je nach der festgestellten CPU.

Bei OpenCL ist dies nicht erforderlich, da der Code zur Laufzeit für die jeweilige Architektur generiert wird.

Wie sieht es mit anderen CPU-Erweiterungen aus? Ein Blick auf die Befehlssätze verschiedener Intel- und AMD-CPUs zeigt, dass es ein paar davon gibt.

Innerhalb des SSE-Befehlssatzes gibt es viele verschiedene Varianten. Es kann recht schwierig sein, denselben Algorithmus in verschiedenen Untergruppen von SSE zu kodieren, wenn bestimmte Anweisungen nicht vorhanden sind. Ich schlage vor (zumindest für den Anfang), dass Sie eine minimal unterstützte Stufe wählen, z. B. SSE2, und auf älteren Maschinen auf die skalare Implementierung zurückgreifen.

Dies ist auch eine ideale Situation für Unit-/Regressionstests, die sehr wichtig sind, um sicherzustellen, dass Ihre verschiedenen Implementierungen die gleichen Ergebnisse liefern. Stellen Sie eine Testreihe mit Eingabedaten und bekannt guten Ausgabedaten zusammen und lassen Sie die gleichen Daten durch beide Versionen der Verarbeitungsfunktion laufen. Möglicherweise müssen Sie einen Präzisionstest durchführen (d. h. die Epsilon-Differenz zwischen dem Ergebnis und der richtigen Antwort liegt unter 1e6 zum Beispiel). Dies wird die Fehlersuche erheblich erleichtern, und wenn Sie in Ihr Test-Framework ein hochauflösendes Timing einbauen, können Sie gleichzeitig die Leistungsverbesserungen vergleichen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X