SIMD Programmierung mit GCC: Vector Extensions

Einführung

In meinem letzten Artikel habe ich die ersten Schritte auf dem Weg zur SIMD-Programmierung mit GCC gezeigt. Diesmal soll es speziell um die Vector Extensions des GCC [1] gehen. Diese erlauben es, Vektoren von Typen zu definieren und die wichtigsten arithmetischen Operatoren direkt mit diesen zu verwenden. Das ist erstens wesentlich bequemer als selbst SIMD-Instruktionen im eigenen Code zu benutzen und zweitens abstrahiert der Compiler vom tatsächlich verwendeten Befehlssatz, was den Code portierbarer macht und ohne Änderungen am Code neuere SIMD-Erweiterungen benutzt, sobald der Compiler sie unterstützt. Im Vergleich zur Auto-Vektorisierung erfordern die Vector Extensions zwar mehr Programmierarbeit, sind allerdings auch wesentlich flexibler einzusetzen.

Vektortypen definieren

Um die Vektorerweiterungen zu benutzen, reicht es, wie im letzten Artikel beschrieben, die jeweilige Befehlssatzerweiterung zu aktivieren. Danach können alle Features verwendet werden. Die wichtigste Neuerung besteht in einem neuen Typ-Attribut, vector_size. Dieses erlaubt es, die Größe des Vektors zu definieren. Zusammen mit einem typedef lassen sich so ganz bequem neue Vektortypen definieren.

typedef float vec4f __attribute__((vector_size(4*sizeof(float))));

Das definiert einen neuen Vektortyp aus vier float-Komponenten. __attribute__ ist die GCC-Schreibweise für Attribut-Spezifikationen, vector_size ist das Attribut, das die Gesamtgröße des Vektors in Byte festlegt.

Mit diesem neuen Typ lassen sich jetzt Vektoren der entsprechenden Größe definieren und wie Arrays initialisieren:

vec4f vector_a = {1.0f, 2.0f, 3.0f, 4.0f};

Im übrigen muss die Komponentenzahl eine Zweierpotenz sein, also muss im Zweifelsfall aufgerundet werden, was allerdings kaum Nachteile bringt. Statt eines 3D-Vektors einen 4D-Vektor zu benutzen verbraucht zwar ein paar Byte mehr pro Vektor, das Laden der Vektoren aus dem Speicher (bzw. aus dem Cache) dauert allerdings genauso lange, weil bei System mit DDR-Speicher ohnehin immer 64 Bit gleichzeitig übertragen werden und die Vektoren müssen bei SSE ohnehin immer auf 16 Byte aligned werden (bei AVX sogar auf 32 Byte). Der größere Platzbedarf (und eventuelle vermehrte Cache-Misses durch weniger Vektoren die in den Cache passen) werden außerdem normalerweise durch die Parallelisierung mehr als ausgeglichen.

Elementzugriff

Hier kommt eine unschöne Seite der Vector Extensions ans Tageslicht: der GCC erlaubt es zwar, auf die einzelnen Elemente eines Vektors wie auf die Elemente eines Arrays zuzugreifen, allerdings nur mit hinreichend aktuellen Compiler-Versionen. In C++ funktioniert das ganze leider erst ab GCC 4.8, in C immerhin schon ab GCC 4.6:

vector_a[1] = 42.0f;
float x = vector_a[0];

Ansonsten müssen wir hier also ein wenig in die Trickkiste greifen, wenn wir auf die einzelnen Elemente des Vektors zugreifen wollen.

Die Vektoren sind im Speicher genauso abgelegt, wie ein Array der entsprechenden Größe, so dass man also schnell auf die Idee kommen könnte, den Vektor einfach auf einen Float-Pointer zu casten und diesen zu indizieren. Das würde sogar funktionieren, wenn es da nicht ein kleines Problem gäbe: Um alle Optimierungen im generierten Code vorzunehmen, nimmt der C++-Compiler an, dass es keine zwei Namen mit unterschiedlichen Typen gibt, die auf die gleichen Speicheradressen verweisen. Auf diese Weise kann der Compiler unter anderem bestimmte Speicherzugriffe wegoptimieren und viel mehr Daten direkt in den Prozessorregistern vorhalten (was grade bei der Vielzahl der SSE-Register einen enormen Vorteil bringen kann). Mehr Informationen zu diesem Problem gibt’s unter [2].

Den Vektor zu casten kommt also nicht in Frage; außerdem wäre dafür ein C-Style-Cast oder ein reinterpret_cast notwendig, was beides nicht besonders schön ist. Die Alternative, die auch von der Manpage zum GCC [3] empfohlen wird, ist es, eine Union aus Vector-Typ und float-Array anzulegen:

union vec4fu {
    vec4f vec;
    float data[4];
};

Auf diese Weise weiß der Compiler, dass vec und data unterschiedliche Namen mit unterschiedlichen Typen für den selben Speicher sind und kann feststellen, wann und wo er sicher optimieren kann. Der Nachteil ist, dass jetzt überall unions benutzt werden müssen. Aber das Problem löst sich von alleine, wenn die aktuellen GCC Versionen breiter verfügbar sind, in Ubuntu 13.04 ist zumindest der GCC 4.8 schon mal in den offiziellen Paketquellen verfügbar, Debian hat ihn zumindest schonmal im experimental Repository.

Arithmetik

Die Vector Extensions erlauben die üblichen arithmetische Operationen direkt mit Vektoren zu benutzen, wobei sie dann elementweise durchgeführt werden. Die erlaubten Operationen sind +, -, *, /, unäres Minus, ^, |, &, ~, %, <> [1]:

vec4f vector_c = vector_a * vector_b;
vec4f vector_d = vector_a + vector_b;
// etc.

Außerdem erlaubt der Compiler, dass einer der Operanden ein skalarer Wert, der dann zu einem Vektor erweitert wird, bei dem alle Elemente den Wert dieses Skalars haben:

vec4f vector_b = 3 * vector_a;

Zu guter Letzt können einige eingebaute Funktionen des Compilers mit diesen Vektortypen benutzt werden, mehr Informationen finden sich wie immer im GCC Handbuch

Ausblick

Nachdem jetzt alle Möglichkeiten der abstrakten und CPU-übergreifenden SIMD Programmierung abgehandelt wurden, wird der nächste Artikel sich speziell mit der SSE-Programmierung mittels intrinsischer Compiler-Funktionen beschäftigen, die direkten Zugriff auf den kompletten SSE-Befehlssatz zulassen, uns aber im Gegensatz zu Inline-Assembler die Verwaltung der Register abnehmen und von sämtlichen Optimierungen des Compilers profitieren können.

Während mit den Vector Extensions prinzipiell noch jeder Vektorbefehlssatz aller möglichen CPU-Architekturen ohne Mehraufwand und portierbar genutzt werden kann, wird es ab jetzt nur noch SSE und AVX geben. Zumindest werde ich mich nur mit diesen Erweiterungen beschäftigen, auch wenn es für viele andere Erweiterungen eigene intrinsische Funktionen im Compiler gibt.

Referenzen

[1] http://gcc.gnu.org/onlinedocs/gcc-4.8.0/gcc/Vector-Extensions.html
[2] http://en.wikipedia.org/wiki/Type_punning
[3] http://linux.die.net/man/1/gcc

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: