Sie sind auf Seite 1von 4

ct.1214.176 179 12.05.

14 11:05 Seite 176

Know-how | Matrixmultiplikation

Andreas Stiller

Matrix
reloaded
Von Matrizen und magischen Compiler-Fhigkeiten
Programme, die mit blichen Matrixroutinen arbeiten, knnen hufig ohne jegliche
nderung des Codes einfach durch die magischen Krfte moderner Compiler um
Grenordnungen schneller laufen. Nicht von solch lppischen Faktoren wie zwei
oder drei ist dabei die Rede, sondern von hundert oder gar tausend.

ie kommt es, dass dieser harmlose C99Code zur Berechnung des Matrixproduktes aus zwei quadratischen Matrizen mit
Code von Compiler A gut tausendmal so
schnell luft wie mit dem von Compiler B?
#define DIM 1024
typedef double mat[DIM][DIM];
mat a,b,c;
void matrixmul_kji () {
for (int i = 0; i < DIM; i++)
for (int j = 0; j < DIM; j++)
c[i][j] = 0.0;
for (int k = 0; k < DIM; k++)
for (int j = 0; j < DIM; j++)
for (int i = 0; i < DIM; i++)
c[i][j] += a[i][k] * b[k][j];

}
//Hauptroutine mit Initialisierung
//der Matrizen und Aufruf
Ja, Sie haben recht gelesen: Faktor 1000 oder
sogar noch ein Stckchen mehr. So ein Unterschied lsst sich nicht so schnell durch
leistungsfhigere Hardware wettmachen.
Auf einem aktuellen Notebook mit HaswellProzessor Core i7- 4750HQ unter Windows
8.1 braucht der Code des ersten Compilers
17 Sekunden (entsprechend 0,13 GFlops),
der des zweiten nur 15 Millisekunden (138
GFlops).
Dabei handelt es sich bei ersterem nicht
etwa um einen veralteten Compiler aus der
DOS-Grabbelkiste (640 KByte htten fr solch

groe Matrizen eh nicht gereicht ), sondern um die aktuelle Version von Microsoft
Visual Studio 2013 (Update 1 beziehungsweise Update 2RC). Der Microsoft-Compiler
muss sich in diesem Matrix-Quizduell allerdings mit dem Intel-C/C++-Compiler 14 aus
dem Composer 2013 messen. Beide drfen
dabei die hchsten Optimierungsstufen fr
die Zielrechner mit Haswell-Kernen (AVX2)
fahren. Okay, der Intel-Compiler beherrscht
ein paar Zaubertricks, sonst wrde dieses
Wunder nicht geschehen.
Obiger Code ist allerdings etwas gruselig,
und zwar gleich aus mehreren Grnden. Er
verwendet global definierte Felder, auf die
matrixmul_kji() zugreift solche Konstruktionen
lassen einem C++-Programmierer kalte
Schauer den Rcken hinunterlaufen. Gruselig
ist aber insbesondere die absichtlich bsartig
gewhlte Reihenfolge der Schleifen mit
k-j-i; in dieser Form haben nmlich die Zugriffe ohne intelligente Umsortierung ein katastrophal schlechtes Cache-Verhalten.
Das lsst sich mit einem Blick auf die innerste Schleife schon erahnen, wenn man
wei, dass C++-Compiler blicherweise
zweidimensionale Felder auf [row*dim+col]
ablegen, was man als row major bezeichnet. Dann liegen die Daten von aufeinanderfolgenden Spalten kontinuierlich im
Speicher, die von aufeinanderfolgenden
Zeilen hpfen hingegen. Die innere
Schleife luft nun sowohl bei Feld c als auch
bei Feld a ber den Zeilenindex i. Dabei ist
zwar der Faktor b[k][j] konstant, die Zugriffe
auf die anderen beiden Matrizenelemente

hpfen jedoch Cache-feindlich quer durch


den Speicher.

Naive Reihenfolge
Die Reihenfolge k-j-i ist also zugegebenermaen herbe konstruiert, blicherweise findet man die natrliche oder naive Schleifenreihenfolge i-j-k vor, so wie es die Multiplikationsformel der Matrizenmultiplikation
auch nahelegt. Die vergleichsweise uninteressante Routine zum Nullsetzen der Ergebnismatrix C vorab schenken wir uns aus
bersichts- und Platzgrnden im Folgenden
so was kann man auch effizient mit memset()
erledigen. Muss man aber nicht, intelligente
Compiler entdecken das ohnehin selbst und
ersetzen die Nullsetzschleife durch einen Einsprung in die Laufzeitbibliothek.
void matrixmul_ijk () { //naiv
// Nullsetzen von c
for (int i = 0; i < DIM; i++)
for (int j =0; j < DIM; j++)
for (int k = 0; k < DIM; k++)
c[i][j] += a[i][k] * b[k][j];
}
Wo man auch hinschaut, landauf, landab,
berall sind solche Routinen mit Reihenfolge
i-j-k zu finden, in unzhligen Programmbeispielen und Tutorials, unter anderem auch in
Entwicklungssystemen von Intel, Nvidia,
AMD, Microsoft und so weiter. In denen geht
es den Firmen ja hufig darum, die Vorzge
ihrer Bibliotheken oder ihrer Rechenbeschleuniger in den Vordergrund zu stellen,

176

ct 2014, Heft 12

Copyright by Heise Zeitschriften Verlag

Persnliches PDF fr Thorsten Grahs move-csc aus 38108 Braunschweig

ct.1214.176 179 12.05.14 11:05 Seite 177

Know-how | Matrixmultiplikation

da schadet es nicht, dass die Haupt-CPU mit


dem Referenzcode mglichst alt aussieht.
Man wei nmlich seit es Caches gibt,
dass auch die i-j-k-Reihenfolge uerst unglcklich ist, da zumindest eine Matrix (hier
b) in der innersten Schleife ber den hpfenden Zeilenindex k angesprochen wird.
Das ist aber nicht nur suboptimal fr die
Cache-Ausnutzung, sondern erschwert auch
die Vektorisierung fr SIMD (SSE oder AVX).
Immerhin, es hpft dabei nur noch eine
Matrix damit ist die Performance dieser
Version schon etwa doppelt so hoch wie die
obiger Worst-Case-Fassung, jedenfalls bei
den Microsoft-Compilern bis hin zu Visual
Studio 2012. Bei den 2013-Compilern ist der
Zugewinn indes deutlich hher, die knnen
schon ein bisschen Magie.
Die weitaus bessere Wahl ist nmlich die
Reihenfolge i-k-j.
void matrixmul_ikj () {
// Nullsetzen von c
for (int i = 0; i < DIM; i++)
for (int k = 0; k < DIM; k++)
for (int j = 0; j < DIM; j++)
c[i][j] += a[i][k] * b[k][j];
}
In der inneren Schleife ber j bleibt a[i][k] netterweise konstant. So was merkt ein Compiler
heutzutage selbst, da braucht ein Programmierer den Faktor nicht mehr explizit vor die
Schleife zu setzen. Wichtig ist vor allem, dass
die Daten der beiden verbleibenden Elemente spaltenweise, also fortlaufend adressiert
werden. Das ist prima, denn dann kann der
Code die geladenen Cachelines gleich samt
Nachbar (adjacent Cacheline) in einem
Rutsch ausnutzen. Die Zugriffe der inneren
Schleife laufen damit im sogenannten Streaming-Modus und das lohnt sich deutlich,
spielt auf neueren Prozessoren mal eben Performancegewinne in der Grenordnung
von Faktor 20, 30 und mehr ein. Unser Haswell-Kern erreicht bei ikj dann schon mal mit
den Microsoft-Compilern knapp ber 5 statt
nur 0,12 GFlops, egal ob man zustzlich das
Architekturflag /arch:AVX gesetzt hat oder nicht.

Pyrrhus-Sieg
Seit Visual Studio 2012 beherrscht nun auch
der Microsoft-Compiler Autovektorisierung.
Der erste Versuch war allerdings noch uerst
rudimentr und schaffte nicht einmal solche
einfachen Konstrukte wie die in obiger Routine matrixmul_ikj(). Visual Studio 2013 ist jedoch
ein gutes Stckchen weiter und vermag immerhin diese Konstrukte fr SSE3 oder AVX
zu vektorisieren. Den Vektorisierungserfolg
kann man sich durch einen Report ber die
Option /Qvec-report:1 anzeigen lassen. Im Visual
Studio gibt es fr den Report bislang noch
keinen eigenen Menpunkt, den Reportwunsch muss man zu Fu unter den zustzlichen Optionen eintragen.
Allein das Ganze stellte sich fr AVX als
Pyrrhus-Sieg heraus, denn der vektorisierte
Code erwies sich nicht nur als nicht schneller,
sondern war erheblich langsamer als der ska-

lare. Dummerweise man mag meinen:


typisch Microsoft ist die Autovektorisierung
standardmig bei /O2 immer eingeschaltet
und ein generelles Abschaltflag wie Intel mit
(/Qvec- hat Microsoft nicht spezifiziert. So kann
man lediglich durch das Pragma #pragma
loop(no_vector) vor allen in Frage kommenden
Schleifen eine misslungene Autovektorisierung unterbinden. Der Verzicht fhrt im vorliegenden Fall immerhin zu 50 Prozent schnellerem Code, sprich zu 6,5 GFlops. Mit SSE3
kommt der Autovektorisierer indes etwas besser zurecht, hier ist der vektorisierte Code mit
5,4 GFlops um gut ein Viertel schneller als die
skalare Fassung mit 3,2 GFlops.
Die Autovektorisierung bei Microsoft
steckt also noch in den Kinderschuhen, aber
die neueren Visual-Studio-Versionen beherrschen dafr was anders, was die 2012er Version noch nicht kannte, nmlich eine Autopermutation. Der Compiler erkennt zumindest in einigen Fllen die unglckliche Reihenfolge von i-j-k und dreht sie in i-k-j um
allerdings eigenmchtig und ohne jeden Report. Es kann also durchaus sein, dass Ihr
alter Code, allein damit neu kompiliert, performancemig geradezu explodiert. Es
muss aber nach unseren zahlreichen Experimenten genau obiges Konstrukt mit globalen Feldern sein, sonst erkennt der Permutator das nicht. Daher der gruselige Code, doch
wer programmiert so?
Die Permutationen funktionieren zudem
nur bei aktivem Autovektorisierer und mit
der naiven Reihenfolge i-j-k sowie j-i-k. Oft
findet man diese Version aber in minimal
vernderter Fassung vor, etwa mit der
zweckmig aussehenden Nullsetzung von c
direkt vor der innersten Schleife:
void matrixmul_x () { //naiv2
for (int i = 0; i < DIM; i++)
for (int j = 0; j < DIM; j++) {
c[i][j] = 0.0;
for (int k = 0; k < DIM; k++)
c[i][j] += a[i][k] * b[k][j];
}
}
Auch hier wieder: Pech gehabt, solche komplizierten Dinge verwirren den Permutator,
er kann die Reihenfolge nicht mehr optimieren und die Performance bleibt massiv hinter
den Mglichkeiten zurck.
Mit Routinen, die auf globale Felder zugreifen wie matrixmul_ikj(), programmieren wie
gesagt nur alte C-Dinosaurier. Zumindest
sollte man Matrizen als Parameter mit statisch festgelegten Typen bergeben knnen:
matrixmulS_ikj( const mat a, const mat b, mat c, int dim)
Statisch festgelegte Typen machen es dem
Compiler einfach, weil er schon zur CompileZeit berblick ber die Dimensionen hat.
Das vereinfacht nicht nur Autovektorisierung
und -parallelisierung, er kann dann auch
hufig schnellere Zugriffe durch Einsatz spezieller Befehle (Scaled Index Base) bewirken.
Den Microsoft-Compiler berfordert die
komplizierte Konstruktion aber bereits. Dass
der Autovektorisierer aufgibt, ist nach dem

ct 2014, Heft 12

oben Genannten allerdings eher von Vorteil,


jedenfalls bei AVX. Aber leider passt auch der
Autopermutator. Die naive ijk-Reihenfolge
sackt dann dramatisch auf 0,27 GFlops ab.
Die bliche Routine fr Matrixmultiplikation von dicht besetzten quadratischen Matrizen wird ohnehin meist anders aussehen,
nmlich dynamisch mit Pointern:
void matrixmulP_ikj(double* a, double * b, double* c,
int dim) {
// Nullsetzen von c
for (int i = 0; i < dim; i++)
for (int k = 0; k < dim; k++)
for (int j = 0; j < dim; j++)
c[i*dim + j] += a[i*dim + k] * b[k*dim + j];
}
Aber das ist zu schwere Kost fr den Microsoft-Compiler, selbst wenn man ihm weitere
Hilfestellung gibt, etwa durch Ersatz der
Variablen dim durch einen festen Wert DIM
und durch den Aufruf:
void matrixmulPR_ikj (
const double* __restrict a,
const double* __restrict b,
double* __restrict c, int dim)

Matrixmultiplikation
Sie ist das Arbeitspferd im High Performance Computing, die Matrixmultiplikation, die in den Mathematik-Bibliotheken
(BLAS) meist unter SGEMM (Single Precision) und DGEMM (Double Precision) zu
finden ist. Auch bei grafischen Berechnungen, beim Rendern, in der Finanzanalyse und in vielen anderen Bereichen
wird sie hufig bentigt. Die klassische
Berechnungsformel fr jedes Element
der Ergebnismatrix sieht so aus:
n

cij = aik bkj


k=1

Weil sie hochoptimiert in allen BLASBibliotheken zu finden ist, braucht man


sich eigentlich keine groen Gedanken
im ihre Implementierung zu machen.
Zudem gehren SGEMM/DGEMM zum
Standard-Lieferumfang der GPUs und
Rechenbeschleuniger, mit Routinen in
OpenCL, CUDA, AMP... wo sie mit hoher
Effizienz in zweistellige Teraflops-Bereiche eindringen.
Aber die klassischen Routinen eignen
sich auch sehr gut, um die Optimierungsfhigkeiten moderner Compiler aufzuzeigen. Werden konstante Werte aus Schleifen herausgezogen, Indizes der Felder
geschickt berechnet, Reihenfolgen von
Schleifen zweckmig getauscht, Autovektorisierung und/oder Autoparallelisierung vorgenommen oder werden die
Routinen gar durch Einsprnge in hochoptimierte Bibliotheken ersetzt?

177

Copyright by Heise Zeitschriften Verlag

Persnliches PDF fr Thorsten Grahs move-csc aus 38108 Braunschweig

ct.1214.176 179 12.05.14 11:05 Seite 178

Know-how | Matrixmultiplikation

Whrend Microsofts Compiler des


VS2013 im Report zumeist Auskunft
gibt, warum er nicht autovektorisieren
konnte, findet man bei Intel viele erfolgreiche Vektorisierungen, oft auch mit
nderung der Schleifenreihenfolge
(Permuted Loop).
Das aus C99 stammende __restrict vor dem
Feldnamen versichert dem Compiler, dass er
kein Alias gibt, sich die Felder also nicht
berschneiden so wie es bei FORTRAN
grundstzlich vorausgesetzt wird. Mit const
verrt man dem Compiler zustzlich, dass
diese beiden Felder in der Routine unverndert bleiben. Aber auch damit klappts nicht.
Moderne Compiler multiplizieren brigens nicht mehr tatschlich i*dim+j, sondern
werten in der Regel die Schleifen aus und inkrementieren die Pointer entsprechend. Alte
handoptimierte Fassungen, die das zu Fu
mit zustzlichen Variablen bewirkten, irritieren die Compiler oft nur, sodass manche magische Optimierung dann nicht greift. Auch
selbstfabriziertes Loop Unrolling ist heutzutage eher kontraproduktiv.
Man sollte aber mangels ausreichender
Permutationsfhigkeit der Microsoft-Compiler von vornherein die richtige Reihenfolge
ikj whlen. Damit ist man ein gutes Stck
weiter, allerdings mit dem Optimierungslatein noch lngst nicht am Ende. Das zeigt
eine Flut von Verffentlichungen zu dem
Thema in den letzten 20 Jahren, von denen
viele von verschiedenen Tricks zur Optimierung der Datenwiederverwendung (data
reuse) in den Caches handeln.
Obige Routinen haben nmlich eine deutlich verbesserungsfhige Data-Reuse-Quote.
Zur deren Optimierung muss man den Job
geschickt in geeignete Blcke oder Kacheln
(Tiling) aufteilen, die dann optimal in die Caches passen.

Add (FMA) verwendet. Das bietet der Haswell-Prozessor gleich in zwei parallelen Pipelines an jedenfalls, wenn man keinen abgespeckten Celeron oder Pentium besitzt. Der
Visual-Studio-2013-Compiler kennt zwar das
Architekturflag /arch:AVX2 aber der Autovektorisierer offenbar noch nicht. Zudem
wird die Option im Visual-Studio-Men erst
ab dem neuen Upgrade 2 angeboten.
Rein theoretisch sind beim Haswell mit
seinen beiden FMA-Pipelines bei voller Vektorisierung und optimalem Data Reuse
16doppeltgenaue Flops pro Takt drin, beim
Grundtakt von 2,8 GHz also 44,8 GFlops pro
Kern. Microsofts Compiler kommt demnach
im besten Fall auf knapp 20 Prozent Effizienz.
Mit viel Aufwand kann man nun die DataReuse-Quote noch etwas steigern.

Kacheln
In der einfachsten Form des Tiling wird nur
die mittlere Schleife gekachelt, indem man
sie auf zwei Schleifen aufteilt. Die erste luft
mit Schrittweite n, die zweite ber die Zwischenwerte. Der beste Wert von n hngt von
der Gre des L1-Cache ab, beim Haswell hat
sich n=16 gut bewhrt, das bringt dann mit
dem Microsoft-Compiler immerhin weitere
40 Prozent auf 8,7 GFlops.
void matrixmul_ikjB(int n, int dim) {
// Nullsetzen von c
for (int k0 = 0; k0 < dim; k0 += n)
for (int i = 0; i < dim; i++)
for (int k = k0; k < min (dim,k0 + n); k++)
#pragma loop (no_vector)
for (int j = 0; j < dim; j++)
c[i][j] += a[i][k] * b[k][j]; }
Umgerechnet auf den effektiven Takt von
2, 8 GHz sind das 3,1 Flops pro Takt. Gar nicht
so schlecht, wenn man bedenkt, dass das die
skalare Performance ist, und der MicrosoftCompiler noch nicht einmal Fused Multiply

Parallel
Man kann das Ganze aber auch noch parallelisieren. Microsofts 2013-Compiler bieten
hierfr als neues Feature Autoparallelisierung via /Qpar an, aber die steckt offenbar
noch tiefer in den Kinderschuhen als die
Autovektorisierung. Jedenfalls funktionierte
sie mit keiner der zahlreich ausprobierten
Matrixmultiplikationsvarianten.
Aber wenn man den Code minimal ndern darf, braucht man sie auch nicht, denn
die Parallelisierung kann man mit einem einzigen richtig platzierten OpenMP-Pragma
bequem selbst veranlassen so man ber
eine kommerzielle Version des Compilers
verfgt und nicht nur ber die kostenlose Expressversion, der Microsoft weder Autoparallelisierung noch OpenMP gegnnt hat.
Der eingesetzte Haswell-Prozessor bietet
vier physische Kerne auf, das msste eigentlich die Performance vervierfachen knnen
aber nur dann, wenn sich die Kerne bei ihren
Zugriffen nicht ins Gehege kommen. Ganz

schafft OpenMP das ohne weitere Manahmen nicht, aber immerhin ist eine Verdreifachung auf 29 Gflops drin.

Intels Zauberknste
Der Trick, den Intel verwendet und der die
eingangs erwhnte gigantische Beschleunigung bewirkt, msste Autobibliothekisierung heien. Diese Zauberei wird mit /Qoptmatmul /Qopenmp aktiviert oder auch bereits
mit /Qpar in den hohen Optimierungsstufen
/O3 oder /fast. Dann versucht der Compiler,
lohnenswerte Matrixmultiplikationen im
Code selbststndig zu erkennen und durch
Einsprnge in die Math Kernel Library (MKL)
zu ersetzen. Die Erkennungslogik ist allerdings uerst sensibel, kleine Variationen
des Codes und sie gibt auf. Aber die hier gezeigten einfachen Routinen, sowohl die mit
den statischen Feldern als auch die mit den
dynamischen Pointern gem matrixmulP_xxx
schafft sie locker das Ergebnis liegt dann
wie eingangs erwhnt bei bis zu 140 GFlops
auf dem Quad-Core mit 2,8 GHz Grundtakt.
Bei Prozessoren mit Hyper-Threading sollte
man die Environment-Variable set KMP_AFFINITY=scatter setzten, sonst schwankt das Resultat erheblich.
Hufig findet man jedoch einen etwas allgemeiner gehaltenen Aufruf, so wie bei
DGEMM der verbreiteten BLAS-Bibliotheken
(Basic Linear Algebra Subprogram) auch. Die
sogenannte leading dimension der Felder
kann man nmlich grer whlen, als man es
fr die jeweilige Matrix braucht. Dadurch lassen sich Alignment-Konflikte in den Caches
vermeiden, was ebenfalls der Performance
zugutekommt. Daher sieht der DGEMM-Aufruf fr jede der drei bergebenen Matrizen
eine leading dimension vor (lda, ldb, ldc), in
unserem vereinfachten Fall reicht eine namens dim0.

178

ct 2014, Heft 12

Copyright by Heise Zeitschriften Verlag

Persnliches PDF fr Thorsten Grahs move-csc aus 38108 Braunschweig

ct.1214.176 179 12.05.14 11:05 Seite 179

Know-how | Matrixmultiplikation

Doppeltgenaue Matrixmultiplikation und Compiler-Knste zwischen 0,12 und 140 GFlops


Adressierung

Reihenfolge

statisch global, dim fix


statisch global, dim fix
statisch global, dim fix
mit typisierten Parametern
mit typisierten Parametern
mit typisierten Parametern
via Pointer, dim=dim0 fix
via Pointer, dim=dim0 fix
via Pointer, dim=dim0 fix
via Pointer, dim=dim0 variable
via Pointer, dim=dim0 variable
via Pointer, dim=dim0 variable
via Pointer, dim, dim0 variable
via Pointer, dim, dim0 variable
via Pointer, dim, dim0 variable
Tiling
Tiling openMP
mkl 1C
mkl 4C pointer beliebig

jki
ijk
ikj
jki
ijk
ikj
jki
ijk
ikj
jki
ijk
ikj
jki
ijk
ikj
ikj
ikj

mit MVS2012 nur 0,26 GFlops

MVS2013 Update 2RC, C++ 18.00.30324


/O2
/O2 /arch:AVX2
(novec)
(auto)
(novec)
0,12
0,12
0,12
3,21
3,21
6,51
3,2
5,4
6,5
0,12
0,12
0,12
0,26
0,26
0,26
3
3
5
0,12
0,12
0,12
0,26
0,26
0,27
3
3
6,4
0,12
0,12
0,12
0,26
0,26
0,27
3
3
6,4
0,12
0,12
0,12
0,26
0,26
0,27
3
3
6,4
3,5
4,5
8,7
8,4
15,3
28,9
37
37
37
140
140
140
dunkel unterlegt: autovektorisiert

void matrixmul_P0_ikj(double* a, double * b,


double* c, int dim, int dim0) {
// Nullsetzen von c
for (int i = 0; i < dim; i++)
for (int k = 0; k < dim; k++)
for (int j = 0; j < dim; j++)
c[i*dim0 + j] += a[i*dim0 + k] * b[k*dim0 + j]; }
Intels Erkennungslogik zeigt sich nun aber
recht sperrig. Sie mag es nur, wenn die leading dimension zur Compile-Zeit feststeht,
sonst gibt sie aus unerklrlichen Grnden
auf, auch wenn man __restrict und const setzt.
Dummerweise geben dann auch Autovektorisierer und -Permutierer auf die Performance fllt ins Bodenlose.
Ohne nderung des Codes etwa durch zustzliche Pragmas kommt man hier nicht weiter, aber dann kann man auch gleich selbst
den MKL-Aufruf ohne jegliche Automagie einsetzen. Dazu ist lediglich im Code #include
"mkl.h" einzubinden und im Visual Studio die
Option Use Intel MKL auf parallel zu stellen.
So luft es dann auch mit beliebig gewhlten
leading dimensions mit voller Performance:
void matrixmulDGEMM(const double *a, const double *b,
double __restrict *c, int dim, int dim0) {
cblas_dgemm(
CblasRowMajor, CblasNoTrans, CblasNoTrans,
dim, dim, dim, // K
1.0, //alpha
a, dim0,
b, dim0,
0.0, //beta
c, dim0
);
}
Der MKL-Aufruf lohnt sich brigens schon
bei kleinen Matrizen bereits ab 8 x 8.
Setzt man die MKL-Option auf Sequential oder entzieht man per set OMP_NUM_
THREADS=1 dem Programm die anderen Kerne,

(auto)
0,12
6,51
5,1
0,12
0,26
5
0,12
0,27
6,4
0,12
0,27
6,4
0,12
0,27
6,4
4,5
15,7
37
140

Intel Composer 2013 C++14.0.2.176


/O3 /Qipo /QxSSE4.2
/O3 /Qipo /QxCORE-AVX2
/Qvec(auto)
/Qvec(auto)
2,5
8,5
8,1
19,8
2,8
8,5
8,1
20,3
5,7
8,5
8,1
20,3
2,5
8,5
8,6
19,5
2,8
8,5
8,6
19,8
5,7
8,5
8,6
20,2
5,9
11,1
8,7
16,8
5,9
11,1
8,7
16,8
5,9
11,1
8,7
16,8
5,9
11,1
6,4
7,5
5,9
11,1
6,4
7,5
5,9
11,1
6,4
7,5
0,12
0,12
0,12
0,12
0,25
0,25
0,23
0,23
5,5
5,5
6,6
6,6
3,2
6,2
3,2
9,5
21,4
38,4
23,7
62
37
37
37
37
140
140
140
140

/Qopt-matmul /QopenMP
110140
110140
110140
110140
110140
110140
110140
110140
110140
110140
110140
110140
0,12
0,23
6,6
9,5
62
37
140

Werte in GFlops unter Windows 8.1 auf Haswell Core i7-4750HQ

so zeigt sich die rohe MKL-Leistung auf nur


einem Kern und die ist bei der gewhlten
Dimension von 1024 x 1024 und mit Haswell-Optimierung (/QxCORE_AVX2) mit 37 GFlops
fast doppelt so hoch wie das beste Resultat
obiger Routinen. Das sind 13,2 Flops pro Takt
bei 2,8 GHz oder 82,5 Prozent Effizienz. Die
blichen Routinen kommen auf bis zu
21GFlops, wenn Intels Autovektorisierer und
Autopermutierer krftig mithelfen und man
mit zur Laufzeit feststehenden Dimensionen
arbeitet. Bei variablem dim sind es aber mit
knapp ber 16 GFlops immer noch erheblich
mehr, als der Microsoft-Compiler im besten
Fall herauszaubern kann.
Man sieht die Wirkung des Autovektorisierers am Report, wenn LOOP WAS VECTORIZED oder PERMUTED LOOP WAS VECTORIZED an den entscheidenden Stellen auftaucht. Fast immer kann der Compiler die
beste Reihenfolge whlen. Hin und wieder
war aber die Vektorisierung unter dem Architekturflag /QxCORE-AVX2 unterschiedlich. Nicht
immer verwendete der Compiler FMA. Dann
wird der Code etwa um 20 Prozent langsamer
als ntig. Es gibt aber noch zahlreiche Pragmas, mit denen man Intels automagische
Fhigkeiten, vor allem bei der Autoparallelisierung, untersttzen kann. Wenn man den
GAP-Adviser einschaltet, bekommt man
zahlreiche Hinweise, wo man etwa #pragma parallel setzen sollte oder wo man ihm per #pragma
min loopcount (n) mitteilen kann, dass die Schleife
hier mindestens n Iterationen aufweisen wird.

Alternativen
Es gibt ein Reihe Alternativen zu den hier
vorgestellten klassischen Routinen. Fr verteiltes Rechnen (etwa via MPI) ist der Cannon-Algorithmus besser geeignet, der aber
lediglich die Indizierung etwas variiert. Mathematisch interessanter sind andere Ideen.

ct 2014, Heft 12

Vor rund 45 Jahren hat der deutsche Mathematiker Volker Strassen gezeigt, dass man
eine 2x2-Multiplikation statt klassisch mit 8
Multiplikationen und 4 Additionen auch mit
7 Multiplikationen und 18 Additionen (spter
dann auch mit nur 15) hinbekommen kann.
Der Algorithmus lsst sich rekursiv auf grere Matrizen anwenden und so ab
2048x2048 braucht man bei ihm sogar insgesamt weniger Gleitkommaoperationen als
bei der normalen Matrixmultiplikation.
Whrend bei jener die sogenannte Komplexitt mit der Dimension kubisch ansteigt,
betrgt der Exponent bei Straen nmlich
nur noch 2,81. Findige Mathematiker haben
das Verfahren im Laufe der Zeit noch deutlich verbessert, quasi einen Wettbewerb zur
Minimierung des Komplexittsfaktors begonnen. Derzeit liegt der Rekord von Franois le Gall bei 2,3728639 (April 2014). Le Gall
hat auerdem aufgezeigt, dass man mit
Quantenalgorithmen noch schneller sein
kann. Aber all diese schnen Algorithmen
sind weitgehend akademisch und, wenn
berhaupt, nur fr sehr groe Matrizen geeignet. Sie lassen sich zudem schwer implementieren, insbesondere vektorisieren und
in FMA-Pipelines packen, fr Caches optimieren und so weiter. Vor allem aber leiden sie
unter einer geringeren numerischen Stabilitt, sodass die Ergebnisse ungenauer sind.
Neben den hier betrachteten normalen
vollbesetzten machen verstrkt auch die
dnnbesetzten Matrizen (sparse matrices)
von sich reden. Ihre Elemente sind berwiegend null, fr sie gibt es dann vllig andere
Herausforderungen, Algorithmen und Optimierungen. Sie bilden die Grundlage des
nchsten groen HPC-Benchmarks, der dem
Linpack zur Seite gestellt werden soll. Mit
ihnen und dem neuen Benchmark beschftigen wir uns in einer der nchsten Ausgaben.
(as) c

179

Copyright by Heise Zeitschriften Verlag

Persnliches PDF fr Thorsten Grahs move-csc aus 38108 Braunschweig

Das könnte Ihnen auch gefallen