SSE mit Doppel, nicht wert?

8

Ich habe ein wenig über die Verwendung von SSE-Intrinsics gelesen und habe mein Glück bei der Implementierung der Quaternion-Rotation mit Doubles versucht. Im Folgenden sind die normalen und SSE-Funktionen, die ich schrieb,

%Vor%

Mit SSE

%Vor%

Die Drehung erfolgt grundsätzlich mit der Funktion

Ich führe dann den folgenden Test aus, um zu überprüfen, wie viel Zeit jede Funktion benötigt, um eine Reihe von Rotationen auszuführen,

%Vor%

Ich habe mit gcc 4.6.3 mit -O3 -std = c99-msse3 kompiliert.

Die Zeiten für die normale Funktion, die die Unix time verwendet, waren 18.841s und 21.689s für die SSE one.

Fehle ich etwas, warum ist die SSE-Implementierung 15% langsamer als die normale? In welchen Fällen wäre eine SSE-Implementierung für doppelte Präzision schneller?

BEARBEITEN : Ich habe aus den Kommentaren Ratschläge ausprobiert und verschiedene Dinge versucht,

    Die Option
  • -O1 liefert sehr ähnliche Ergebnisse.
  • Es wurde versucht, restrict für die Funktion cross_p zu verwenden, und es wurde ein __m128d hinzugefügt, um das zweite Kreuzprodukt aufzunehmen. Dies hatte keinen Unterschied in der hergestellten Baugruppe.
  • Die Assembly, die für die normale Funktion erstellt wurde, enthält nach meinem Verständnis nur skalare Anweisungen außer einigen movapd .

Der für die SSE-Funktion generierte Assemblercode ist nur 4 Zeilen kleiner als der normale.

EDIT : Links zu der generierten Assembly hinzugefügt,

quat_rot

quat_rotSSE

    
Grieverheart 19.01.2013, 16:47
quelle

2 Antworten

10

SSE (und SIMD im Allgemeinen) funktioniert wirklich gut, wenn Sie dieselben Operationen für eine große Anzahl von Elementen durchführen, bei denen keine Abhängigkeiten zwischen den Operationen bestehen. Zum Beispiel, wenn Sie ein Array von Double hatten und array[i] = (array[i] * K + L)/M + N; für jedes Element benötigten, dann würde SSE / SIMD helfen.

Wenn Sie bei einer großen Anzahl von Elementen nicht die gleichen Operationen ausführen, hilft SSE nicht. Zum Beispiel, wenn Sie ein Double hatten und foo = (foo * K + L)/M + N; machen mussten, dann wird SSE / SIMD nicht helfen.

Grundsätzlich ist SSE das falsche Werkzeug für den Job. Sie müssen den Job in etwas ändern, in dem SSE das richtige Werkzeug ist. Zum Beispiel, anstatt Multiplizieren eines Vektors mit einer Quaternion; Versuchen Sie, ein Array von 1000 Vektoren mit einer Quaternion zu multiplizieren oder vielleicht ein Array von 1000 Vektoren mit einem Array von 1000 Quaternionen zu multiplizieren.

BEARBEITEN: Alles hier unten hinzugefügt!

Beachten Sie, dass dies in der Regel bedeutet, Datenstrukturen entsprechend anzupassen. Zum Beispiel ist es oft besser, eine Struktur von Arrays zu haben, anstatt ein Array von Strukturen zu haben.

Stellen Sie sich zum besseren Beispiel vor, dass Ihr Code ein Array von Quaternionen verwendet:

%Vor%

Der erste Schritt wäre, sie in eine Quaternion von Arrays zu transformieren und dies zu tun:

%Vor%

Dann, weil 2 benachbarte Doubles in ein einzelnes SSE-Register passen, wollen Sie die Schleife um 2:

abwickeln %Vor%

Nun wollen Sie das in einzelne Operationen aufteilen. Zum Beispiel würden die ersten 2 Zeilen der inneren Schleife werden:

%Vor%

Ordne das jetzt neu an:

%Vor%

Wenn Sie all dies getan haben, überlegen Sie, ob Sie in SSE konvertieren möchten. Die ersten zwei Codezeilen sind ein Ladevorgang (der sowohl a[2][i] als auch a[2][i+1] in ein SSE-Register lädt), gefolgt von einer Multiplikation (und nicht zwei separaten Ladevorgängen und zwei separaten Multiplikationen). Diese 6 Zeilen könnten (Pseudocode) werden:

%Vor%

Jede Pseudocodezeile ist hier eine einzelne SSE-Anweisung / intrinsisch; und macht jeder SSE-Befehl / intrinsic 2 Operationen parallel .

Wenn jeder Befehl zwei Operationen parallel ausführt, dann (theoretisch) könnte er doppelt so schnell sein wie der ursprüngliche Code "eine Operation pro Anweisung".

    
Brendan 19.01.2013, 20:58
quelle
1

Einige Ideen, die vielleicht eine vollständige Optimierung Ihres Codes ermöglichen würden.

  • Ihre Funktionen sollten inline
  • sein
  • Sie sollten restrict specifications zu cross_p hinzufügen, um das zu vermeiden mehrfaches Neuladen von Parametern durch den Compiler
  • Wenn Sie das tun, müssten Sie eine vierte Variable __m128d einführen, die das Ergebnis des zweiten Aufrufs an cross_p erhält.

Dann schauen Sie in den Assembler (gcc Option -S), um zu sehen, was von all dem erzeugt wird.

    
Jens Gustedt 19.01.2013 17:12
quelle