SIMD Programmierung mit GCC: Erste Schritte

Einführung

SIMD [1] steht für “Single Instruction, Multiple Data” und bedeutet auch genau das: Der Prozessor führt den selben Befehl gleichzeitig auf mehreren Daten durch. Für x86-CPUs gibt es inzwischen einige verschiedene Befehlssatzerweiterungen, angefangen bei der Multi Media Extension des gleichnamigen Pentium MMX und AMDs 3DNow!, über die verschiedenen Streaming SIMD Extensions [2] (SSE bis SSE4) bis hin zu den Advanced Vector Extensions (AVX und AVX2) der aktuellen und nächsten Generation von Intels Core Prozessoren.

Diese Befehlssatzerweiterungen mit GCC zu benutzen ist an sich nicht kompliziert, es gibt aber ein paar Kleinigkeiten zu beachten.

Extensions einschalten

Der Compiler kann die Instruktionen der verschiedenen Erweiterungen nur benutzen, wenn er weiß, dass die CPU, auf der das fertige Programm laufen soll, den jeweiligen Befehlssatz unterstützt. Normalerweise erzeugt der Compiler nur Code, der auf einem “größten gemeinsamen Nenner” ausgeführt werden kann. Was dieser größte gemeinsame Nenner genau ist, wird beim Übersetzen des GCC festgelegt und hängt vom Distributor ab, der den GCC bereitstellt, ist aber normalerweise sehr weit gefasst. Meistens wird man für 32-Bit gerade mal Pentium-Pro-Code finden (das heißt, überhaupt keine Vektorerweiterungen) und für 64-Bit SSE2.

Um dem Compiler die erweiterten Befehle zu erlauben, gibt es prinzipiell zwei Möglichkeiten. Einerseits können verschiedene Erweiterungen manuell eingeschaltet werden, andererseits kann die CPU, für die Code erzeugt werden soll geändert werden. Beides wird über Kommandozeilen-Optionen des Compilers gemacht. Um einzelne Befehlssätze zu aktivieren, kann eine Kombination der folgenden Flags benutzt werden: -mmmx, -msse, -msse2, -msse3, -mssse3, -msse4, -msse4.1, -msse4.2, -mavx, -mavx2, -msse4a, -m3dnow. Welches Flag welche Erweiterung aktiviert ist wohl offensichtlich.

Der Prozessortyp lässt sich mit -march=XX auswählen, wobei XX beispielsweise corei7, amdfam10 oder ähnliches sein. Die GCC Manpage [3] gibt Auskunft, was der Compiler alles kennt und was die einzelnen Architekturen für Befehlessätze unterstützen. Dadurch aktiviert der Compiler sämtliche Befehlssatzerweiterungen, die dieser Prozessor unterstützt. Danach läuft der Code allerdings auch nur noch auf Rechnern mit einer CPU, die sämtliche benutzten Instruction Sets versteht.

Auto-Vektorisierung

Um die Befehlssatzerweiterungen nach dem Aktivieren auch zu benutzen, gibt es mehrere Möglichkeiten. Die einfachste Methode ist Auto-Vektorisierung. Dabei erkennt der Compiler selbstständig bestimmte Muster im Code, die mit SIMD-Instruktionen parallelisiert werden können.

Hier ein kleines Beispiel:

float a[4] = {1.0f, 2.0f, 3.0f, 4.0f};
float b[4] = {4.0f, 3.0f, 2.0f, 1.0f};
float c[4];
for(int i = 0; i < 4; i++)
    c[i] = a[i] + b[i];

Dieser Code wird ohne Vektorisierung so übersetzt, wie man es sich vorstellt, wobei ich die Initialisierung von xmm0, xmm1 und rax entfernt habe, weil die vom eigentlichen Thema ablenken. Zu Beginn des folgenden Assembler-Ausschnitts enthalten xmm0 und xmm1 jeweils das erste Element von a und b und rax ist 0:

.L3: 
addss xmm0, xmm1
movss DWORD PTR [rsp+32+rax], xmm0
add rax, 4
cmp rax, 16
je .L2
movss xmm0, DWORD PTR [rsp+rax]
movss xmm1, DWORD PTR [rsp+16+rax]
jmp .L3
.L2:

Hier sehen wir die Schleife wie aus dem Lehrbuch in Assembler übersetzt. addss und movss sowie die Register xmm0 und xmm1 zeigen übrigens, dass der Compiler bereits SSE-Befehle benutzt, lediglich die Vektorisierung fehlt noch.

Schaltet wir jetzt die Auto-Vektorisierung ein, indem wir noch die Kommandozeilen-Option -ftree-vectorize oder noch bequemer -O3 benutzen, erkennt der Compiler, dass sich dieser Code mittels SSE-Befehlen parallelisieren lässt. Und erzeugt stattdessen den folgenden Assembler-Code:

movaps xmm0, XMMWORD PTR [rsp]
addps  xmm0, XMMWORD PTR [rsp+16]
movaps XMMWORD PTR [rsp+32], xmm0

Hier wird zu Begin xmm0 mit dem kompletten Array a initialisiert. addps addiert dann elementweise die vier floats von b dazu. (das ps in addps steht für “parallel, single precision”) und das Ergebnis wird in das Zielarray im Speicher geschrieben.

Diese Art, von SSE zu profitieren ist also mit sehr wenig Aufwand verbunden und kann sicher bei viel bestehendem Code, schon durch einfügen weniger Kommandozeilen-Optionen an den Compiler-Aufruf erreicht werden. Außerdem lassen sich viele Schleifen umschreiben, um automatisch vektorisiert zu werden.

Leider lassen sich nicht alle prinzipiell möglichen Arten von Vektor-Operationen automatisch vektorisieren. Was alles möglich ist, lässt sich auf der GCC Projektseite [4] nachlesen.

Ausblick

Im GCC gibt es noch (mindestens) zwei weitere Möglichkeiten, SSE-Befehle zu benutzen. Erstens die GCC Vector Extensions, die arithmetische Operationen auf Vektoren erweitern und Compiler-Intrinsics, die es erlauben, SSE-Befehle wie Funktionen zu benutzen. Diese Themen werde ich in einem späteren Artikel behandeln.

Referenzen

[1] http://de.wikipedia.org/wiki/Flynnsche_Klassifikation
[2] http://de.wikipedia.org/wiki/Streaming_SIMD_Extensions
[3] http://linux.die.net/man/1/gcc
[4] http://gcc.gnu.org/projects/tree-ssa/vectorization.html

Tags: , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: