it-swarm.com.de

Verwenden SSE Anleitung

Ich habe eine in C++ geschriebene Schleife, die für jedes Element eines ganzzahligen Arrays ausgeführt wird. Innerhalb der Schleife maskiere ich einige Bits der Ganzzahl und finde dann die Min- und Max-Werte. Ich habe gehört, dass, wenn ich SSE -Anweisungen für diese Operationen verwende, diese im Vergleich zu einer normalen Schleife, die mit bitweisen AND- und if-else-Bedingungen geschrieben wurde, viel schneller laufen wird. Meine Frage ist, sollte ich diese SSE Anweisungen befolgen? Was passiert auch, wenn mein Code auf einem anderen Prozessor läuft? Funktioniert es noch oder sind diese Anweisungen verarbeitungsspezifisch?

28
Naveen
  1. SSE-Anweisungen sind prozessorspezifisch. Welcher Prozessor welche SSE -Version auf Wikipedia unterstützt, kann nachgeschlagen werden.
  2. Ob SSE Code schneller ist oder nicht, hängt von vielen Faktoren ab: Der erste ist natürlich, ob das Problem speicher- oder CPU-gebunden ist. Wenn der Speicherbus der Engpass ist, wird SSE nicht viel helfen. Versuchen Sie, Ihre ganzzahligen Berechnungen zu vereinfachen. Wenn dies den Code beschleunigt, ist er wahrscheinlich an die CPU gebunden und Sie haben gute Chancen, ihn zu beschleunigen.
  3. Beachten Sie, dass das Schreiben von SIMD-Code viel schwieriger ist als das Schreiben von C++ - Code, und dass der resultierende Code viel schwieriger zu ändern ist. Halten Sie den C++ - Code immer auf dem neuesten Stand. Sie möchten ihn als Kommentar und zur Überprüfung der Richtigkeit Ihres Assembler-Codes.
  4. Überlegen Sie sich, eine Bibliothek wie den IPP zu verwenden, die gängige SIMD-Operationen auf niedriger Ebene implementiert, die für verschiedene Prozessoren optimiert sind.
24
Niki

Mit SIMD, von dem SSE ein Beispiel ist, können Sie dieselbe Operation für mehrere Datenblöcke ausführen. Wenn Sie SSE als direkten Ersatz für Ganzzahloperationen verwenden, haben Sie keinen Vorteil. Sie erhalten nur dann Vorteile, wenn Sie die Operationen für mehrere Datenelemente gleichzeitig ausführen können. Dazu werden einige im Speicher zusammenhängende Datenwerte geladen, die erforderliche Verarbeitung ausgeführt und dann zum nächsten Satz von Werten im Array gesprungen. 

Probleme:

1 Wenn der Codepfad von den verarbeiteten Daten abhängig ist, wird die Implementierung von SIMD wesentlich schwieriger. Zum Beispiel:

a = array [index];
a &= mask;
a >>= shift;
if (a < somevalue)
{
  a += 2;
  array [index] = a;
}
++index;

ist nicht einfach als SIMD zu machen:

a1 = array [index] a2 = array [index+1] a3 = array [index+2] a4 = array [index+3]
a1 &= mask         a2 &= mask           a3 &= mask           a4 &= mask
a1 >>= shift       a2 >>= shift         a3 >>= shift         a4 >>= shift
if (a1<somevalue)  if (a2<somevalue)    if (a3<somevalue)    if (a4<somevalue)
  // help! can't conditionally perform this on each column, all columns must do the same thing
index += 4

2 Wenn die Daten nicht anfällig sind, ist das Laden der Daten in die SIMD-Anweisungen umständlich

3 Der Code ist prozessorspezifisch. SSE ist nur auf IA32 (Intel/AMD) und nicht alle IA32-CPUs unterstützen SSE.

Sie müssen den Algorithmus und die Daten analysieren, um zu sehen, ob es SSE-fähig sein kann und dass Sie wissen müssen, wie SSE funktioniert. Die Website von Intel enthält zahlreiche Dokumente.

15
Skizz

Diese Art von Problem ist ein perfektes Beispiel dafür, wo ein guter Low-Level-Profiler unerlässlich ist. (Etwas wie VTune) Es gibt Ihnen eine viel informativere Vorstellung davon, wo Ihre Hotspots liegen.

Meine Vermutung, was Sie beschreiben, ist, dass es sich bei Ihrem Hotspot wahrscheinlich um Verzweigungsvorhersagen handelt, die sich aus Min/Max-Berechnungen mit if/else ergeben. Daher sollten Sie bei Verwendung von SIMD-Intrinsics die Min/Max-Anweisungen verwenden. Es kann sich jedoch lohnen, stattdessen eine verzweigungslose Min/Max-Berechnung zu verwenden. Dies kann die meisten Gewinne mit weniger Schmerzen erzielen.

Etwas wie das:

inline int 
minimum(int a, int b)
{
  int mask = (a - b) >> 31;
  return ((a & mask) | (b & ~mask));
}
10
Peter Jeffery

Wenn Sie SSE -Anweisungen verwenden, sind Sie offensichtlich auf Prozessoren beschränkt, die diese unterstützen. Das bedeutet x86 (aus dem Pentium 2 oder so) (ich kann mich nicht genau erinnern, wann sie eingeführt wurden (aber es ist lange her)

Etwas neuer ist SSE2 (soweit ich mich erinnern kann, das Ganzzahloperationen anbietet) (Pentium 3? Obwohl die ersten AMD Athlon-Prozessoren sie nicht unterstützten)

In jedem Fall haben Sie zwei Möglichkeiten, diese Anweisungen zu verwenden. Schreiben Sie entweder den gesamten Codeblock in Assembly (wahrscheinlich eine schlechte Idee. Dies macht es dem Compiler praktisch unmöglich, Ihren Code zu optimieren, und es ist sehr schwierig für einen Menschen, einen effizienten Assembler zu schreiben).

Verwenden Sie alternativ die für Ihren Compiler verfügbaren Intrinsics (wenn der Arbeitsspeicher dient, werden sie normalerweise in xmmintrin.h definiert)

Aber auch hier kann sich die Leistung nicht verbessern. SSE Code stellt zusätzliche Anforderungen an die Daten, die es verarbeitet. Dabei ist zu beachten, dass die Daten an 128-Bit-Grenzen ausgerichtet werden müssen. Es sollte auch wenige oder keine Abhängigkeiten zwischen den Werten geben, die in dasselbe Register geladen werden (ein 128-Bit-Register SSE kann 4 Ints aufnehmen. Das Hinzufügen des ersten und des zweiten ist nicht optimal. Aber alle vier Ints müssen addiert werden.) zu den entsprechenden 4 Ints in einem anderen Register ist schnell)

Es kann verlockend sein, eine Bibliothek zu verwenden, die alle Low-Level-SSE-Fummel einschließt, dies kann jedoch auch einen potenziellen Leistungsvorteil ruinieren.

Ich weiß nicht, wie gut die Unterstützung von Ganzzahloperationen durch SSE ist, daher kann dies auch ein Faktor sein, der die Leistung einschränken kann. SSE zielt hauptsächlich darauf ab, Gleitkommaoperationen zu beschleunigen.

6
jalf

Wenn Sie Microsoft Visual C++ verwenden möchten, sollten Sie Folgendes lesen:

http://www.codeproject.com/KB/recipes/sseintro.aspx

4
Migol

Meiner Erfahrung nach kann ich sagen, dass SSE eine einfache (4x und höhere) Beschleunigung über eine einfache c-Version des Codes bringt (kein Inline-ASM, keine Intrinsics verwendet), aber der von Hand optimierte Assembler kann Compiler-Generationen schlagen Assembler, wenn der Compiler nicht herausfinden kann, was der Programmierer beabsichtigt hat (glauben Sie, Compiler decken nicht alle möglichen Code-Kombinationen ab und werden dies niemals tun.) Oh, und der Compiler kann die Daten nicht jedes Mal so anordnen es läuft mit der schnellstmöglichen Geschwindigkeit. Aber Sie benötigen viel Erfahrung für eine Beschleunigung über einen Intel-Compiler (wenn möglich).

3
Quonux

Wir haben einen Bildverarbeitungscode implementiert, der dem von Ihnen beschriebenen ähnlich ist, jedoch in einem Bytearray In SSE. Die Beschleunigung im Vergleich zu C-Code ist beachtlich, je nach dem exakten Algorithmus mehr als einen Faktor 4, selbst in Bezug auf den Intel-Compiler. Wie Sie bereits erwähnt haben, haben Sie jedoch folgende Nachteile:

  • Portabilität. Der Code läuft auf jeder Intel-ähnlichen CPU, also auch auf AMD, aber nicht auf anderen CPUs. Das ist für uns kein Problem, weil wir die Zielhardware steuern. Das Umschalten von Compilern und sogar auf ein 64-Bit-Betriebssystem kann ebenfalls ein Problem sein.

  • Sie haben eine steile Lernkurve, aber ich habe festgestellt, dass es nicht so schwer ist, neue Algorithmen zu schreiben, nachdem Sie die Prinzipien verstanden haben.

  • Wartbarkeit. Die meisten C- oder C++ - Programmierer haben keine Kenntnisse über Assembly/SSE.

Mein Rat an Sie wird es sein, sich nur dafür einzusetzen, wenn Sie wirklich eine Leistungsverbesserung benötigen und in einer Bibliothek wie dem Intel-IPP keine Funktion für Ihr Problem finden können und wenn Sie mit den Portabilitätsproblemen leben können.

3

Schreiben Sie Code, der dem Compiler hilft zu verstehen, was Sie tun. GCC wird SSE-Code wie diesen verstehen und optimieren:

typedef union Vector4f
{
        // Easy constructor, defaulted to black/0 vector
    Vector4f(float a = 0, float b = 0, float c = 0, float d = 1.0f):
        X(a), Y(b), Z(c), W(d) { }

        // Cast operator, for []
    inline operator float* ()
    { 
        return (float*)this;
    }

        // Const ast operator, for const []
    inline operator const float* () const
    { 
        return (const float*)this;
    }

    // ---------------------------------------- //

    inline Vector4f operator += (const Vector4f &v)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += v[i];

        return *this;
    }

    inline Vector4f operator += (float t)
    {
        for(int i=0; i<4; ++i)
            (*this)[i] += t;

        return *this;
    }

        // Vertex / Vector 
        // Lower case xyzw components
    struct {
        float x, y, z;
        float w;
    };

        // Upper case XYZW components
    struct {
        float X, Y, Z;
        float W;
    };
};

Vergessen Sie nicht, -msse -msse2 für Ihre Build-Parameter zu verwenden!

2
LiraNuna

Die SSE-Anweisungen waren ursprünglich nur auf Intel-Chips, aber kürzlich (seit Athlon?) Unterstützt AMD sie ebenfalls. Wenn Sie also gegen den SSE-Befehlssatz codieren, sollten Sie zu den meisten x86-Prozessen portierbar sein.

Allerdings lohnt es sich möglicherweise nicht, SSE zu lernen, wenn Sie den Assembler von x86 bereits kennen. Eine einfachere Möglichkeit wäre, die Compiler-Dokumente zu überprüfen und zu prüfen, ob Optionen verfügbar sind Der Compiler generiert automatisch den SSE - Code für Sie. Einige Compiler vektorisieren Schleifen auf diese Weise sehr gut. (Sie sind wahrscheinlich nicht überrascht zu hören, dass die Intel-Compiler das gut machen :)

2
Mike

Es ist zwar richtig, dass SSE für einige Prozessoren spezifisch ist (SSE ist relativ sicher, SSE2 ist viel weniger erfahrungsgemäß), Sie können jedoch die CPU zur Laufzeit erkennen und den Code abhängig von der Ziel-CPU dynamisch laden .

1

Ich stimme den vorherigen Plakaten zu. Die Vorteile können sehr groß sein, aber um sie zu erhalten, ist möglicherweise viel Arbeit erforderlich. Die Intel-Dokumentation in dieser Anleitung umfasst mehr als 4K-Seiten. Sie können EasySSE (C++ - Wrapper-Bibliothek über Intrinsics + Beispiele) kostenlos von Ocali Inc. testen.

Ich gehe davon aus, dass meine Zugehörigkeit zu diesem EasySSE eindeutig ist. 

1
Ogan Ocali

SIMD-Intrinsics (wie SSE2) können diese Art der Beschleunigung beschleunigen, erfordern jedoch Fachwissen, um sie richtig einzusetzen. Sie sind sehr empfindlich gegenüber Ausrichtung und Pipeline-Latenz. Unvorsichtiger Gebrauch kann die Leistung noch schlimmer machen als ohne sie. Sie erhalten eine wesentlich einfachere und schnellere Beschleunigung, wenn Sie einfach das Cache-Prefetching verwenden, um sicherzustellen, dass sich alle Ihre Ints in L1 befinden, damit Sie sie bearbeiten können.

Wenn Ihre Funktion nicht einen Durchsatz von mehr als 100.000.000 ganzen Zahlen pro Sekunde benötigt, ist SIMD wahrscheinlich nicht die Mühe wert. 

1
Crashworks

Um nur kurz hinzuzufügen, was zuvor gesagt wurde, dass verschiedene SSE-Versionen auf verschiedenen CPUs verfügbar sind: Dies kann durch Anzeigen der jeweiligen Merkmalsflags überprüft werden, die von der CPUID-Anweisung zurückgegeben werden (siehe z. B. Intel-Dokumentation). .

1
PhiS

Werfen Sie einen Blick auf Inline Assembler für C/C++, hier ist ein DDJ-Artikel . Wenn Sie nicht zu 100% sicher sind, dass Ihr Programm auf einer kompatiblen Plattform ausgeführt wird, sollten Sie den Empfehlungen folgen, die viele hier gegeben haben.

1
epatel

Ich empfehle Ihnen nicht, dies selbst zu tun, es sei denn, Sie beherrschen die Montage recht gut. Die Verwendung von SSE erfordert höchstwahrscheinlich eine sorgfältige Neuorganisation Ihrer Daten, wie Skizz darauf hinweist, und der Nutzen ist im besten Fall oft fragwürdig.

Es wäre wahrscheinlich viel besser für Sie, sehr kleine Schleifen zu schreiben, Ihre Daten sehr straff zu organisieren und sich darauf zu verlassen, dass der Compiler dies für Sie erledigt. Sowohl der Intel C-Compiler als auch GCC (seit 4.1) können Ihren Code automatisch vektorisieren und werden wahrscheinlich bessere Ergebnisse erzielen als Sie. (Fügen Sie einfach -tree-vectorize zu Ihren CXXFLAGS hinzu.)

Edit : Eine andere Sache, die ich erwähnen sollte, ist, dass mehrere Compiler Assembly Intrinsics unterstützen, was wahrscheinlich IMO einfacher zu benutzen wäre als die Syntax asm () oder __asm ​​{}.

0
greyfade