Zentrum für Informationsdienste und Hochleistungsrechnen (ZIH

)

Optimierungen: Compiler- oder Handarbeit?

Zellescher Weg 12 Willers-Bau A[105] Tel. +49 351 - 463 - 32442

Olaf Krzikalla (olaf.krzikalla@tu-dresden.de)

Microoptimierung
Register ausnullen:
mov and sub xor $0,%eax $0,%eax %eax,%eax %eax,%eax

Welche Variante ist die beste?

Microoptimierung
Register ausnullen:
b8 83 29 31 00 00 00 00 e0 00 c0 c0 mov and sub xor $0,%eax $0,%eax %eax,%eax %eax,%eax

– kurzer Maschinencode

– xor und sub aber mit falscher Abhängigkeit von eax
– CPU-interner Shortcut für xor, aber nicht für sub!  Compiler wissen Bescheid!

Pattern matching
Sprünge verhindern:
float r = MIN(x[0], x[1]); // oder std::min

MSVC 2010 /O2:

00E910B1 00E910B3 00E910B5 00E910B8 00E910BA 00E910BC 00E910BE

fcom fnstsw test jp fstp jmp fstp

st(1) ax ah,5 main+0BEh (0E910BEh) st(1) main+0C0h (0E910C0h) st(0)

Pattern matching
Sprünge verhindern:
float r = MIN(x[0], x[1]);

clang 3.2 -O3:

movss movss LBB0_4: minss

-44(%ebp), %xmm0 -40(%ebp), %xmm1
# %for.end %xmm1, %xmm0

Pattern matching
Sprünge verhindern:
float r = MIN(x[0], x[1]);

icc 12 –O3:

002110B4 002110BD 002110C4

movss mov minss

xmm0,dword ptr [x] dword ptr [esp],offset string "%f" xmm0,dword ptr [esp+84h]

Pattern matching
Sprünge verhindern:
float r = std::min(x[0], x[1]);

icc 12 –O3:

002110B4 002110BD 002110C4

movss mov minss

xmm0,dword ptr [x] dword ptr [esp],offset string "%f" xmm0,dword ptr [esp+84h]

Pattern matching
Sprünge verhindern:
float r = MIN(x[0], MIN(x[1], x[2]));

icc 12 –O3:

003510AE 003510B4 003510BD 003510C6 003510CD 003510D6

movss movss movss mov minss minss

dword ptr [esp+eax*4+7Ch],xmm0 xmm0,dword ptr [esp+84h] xmm1,dword ptr [x] dword ptr [esp],offset string "%f" xmm0,dword ptr [esp+88h] xmm1,xmm0

Pattern matching
Sprünge verhindern:
float r = MIN(x[0], MIN(x[1], x[2]));

clang 3.2 -O3:

movss movss movss LBB0_4: minss minss

-36(%ebp), %xmm0 -44(%ebp), %xmm1 -40(%ebp), %xmm2
# %for.end %xmm0, %xmm2 %xmm2, %xmm1

Pattern matching
Sprünge verhindern:
float r = x[0] < x[1] ? x[2] : x[3];

icc 12 –O3:

00C810AE 00C810B4 00C810BD 00C810C5 00C810C7 00C810D0 00C810D2 00C810DB

movss movss comiss jbe movss jmp movss cvtss2sd

dword ptr [esp+eax*4+7Ch],xmm0 xmm0,dword ptr [esp+84h] xmm0,dword ptr [x] main+0D2h (0C810D2h) xmm0,dword ptr [esp+88h] main+0DBh (0C810DBh) xmm0,dword ptr [esp+8Ch] xmm0,xmm0

Pattern matching
Sprünge verhindern:
float r = x[0] < x[1] ? x[2] : x[3];

clang 3.2 -O3:

movss -40(%ebp), %xmm0 ucomiss -44(%ebp), %xmm0 jbe LBB0_5 LBB0_4: movss -36(%ebp), %xmm0 jmp LBB0_6 LBB0_5: movss -32(%ebp), %xmm0 LBB0_6: cvtss2sd %xmm0, %xmm0

# %cond.true

# %cond.false # %cond.end

Pattern matching
Sprünge verhindern:
float r = x[0] < x[1] ? x[2] : x[3];

MSVC 2010 /O2: 00F910A9 00F910AC 00F910AD 00F910B0 00F910B1 00F910B3 00F910B5 00F910B8 00F910BA 00F910BD 00F910BF 00F910C2 fld pop fld pop fcompp fnstsw test jne fld jmp fld fstp dword ptr [ebp-2Ch] edi dword ptr [ebp-28h] ebx ax ah,41h main+0BFh dword ptr main+0C2h dword ptr dword ptr

(0F910BFh) [ebp-24h] (0F910C2h) [ebp-20h] [ebp-30h]

Pattern matching
Blending nur für Vektoren:
float r = _mm_cvtss_f32( _mm_blendv_ps(_mm_set_ss(x[3]), _mm_set_ss(x[2]), _mm_cmplt_ss(_mm_set_ss(x[0]), _mm_set_ss(x[1]))));

icc 12 –O3, MSVC 2010 /O2: 003910AE 003910B4 003910BD 003910C6 003910CF 003910D6 003910E0 003910E5 movss movss movss movss mov cmpltss blendvps cvtss2sd dword ptr [esp+eax*4+7Ch],xmm0 xmm0,dword ptr [x] xmm2,dword ptr [esp+8Ch] xmm1,dword ptr [esp+88h] dword ptr [esp],offset string "%f" xmm0,dword ptr [esp+84h] xmm2,xmm1,xmm0 xmm2,xmm2

Vorsicht: Branch-Misprediction < Cache Miss!

Pattern matching
Blending nur für Vektoren:
float r[100]; #pragma simd for (int i = 0; i < 100; ++i) r[i] = x[i] < x[i+1] ? x[i+2] : x[i+3];

icc 12 –O3: 00241088 00241090 00241093 00241099 0024109C 002410A0 002410A3 002410A9 002410AF 002410B4 movaps movaps palignr movaps cmpltps movaps palignr palignr blendvps movaps xmm2,xmmword ptr x[eax*4] xmm1,xmm3 xmm1,xmm2,4 xmm0,xmm2 xmm0,xmm1 xmm4,xmm3 xmm4,xmm2,0Ch xmm3,xmm2,8 xmm4,xmm3,xmm0 xmmword ptr r[eax*4],xmm4

Funktionen
eingebaute Instruktionen:
float r = sqrt(x[1]);

icc 12 –O3: 003C1077 sqrtss xmm0,dword ptr [esp+84h]

Funktionen
eingebaute Instruktionen:
float r = sqrt(x[1]);

clang 3.2 -O3: movss cvtss2sd movsd calll fstps cvtss2sd -404(%ebp), %xmm0 %xmm0, %xmm0 %xmm0, (%esp) _sqrt -412(%ebp) -412(%ebp), %xmm0

 clang kennt _mm_sqrt_ss nicht

Funktionen
arithmetische Bitmanipulationen:
float r = std::abs(x[1]);

icc 12 –O3: 001B1080 andps xmm0,xmmword ptr [___xi_z+30h]

Funktionen
arithmetische Bitmanipulationen:
#define ABS(X) ((X) < (0) ? (-(X)) : (X))

float r = ABS(x[1]);

icc 12 –O3: 001B1080 andps xmm0,xmmword ptr [___xi_z+30h]

Funktionen
arithmetische Bitmanipulationen:
#define ABS(X) ((X) < (0) ? (-(X)) : (X))

float r = ABS(x[1]);

clang 3.2 -O3: movss -404(%ebp), %xmm0 xorps %xmm1, %xmm1 ucomiss %xmm0, %xmm1 jb LBB0_4 # BB#3: xorps LCPI0_0, %xmm0 LBB0_4:

# %cond.true
# %cond.end

Funktionen
arithmetische Bitmanipulationen:
float r = fabsf(x[1]); // oder std::abs

clang 3.2 -O3: movss -404(%ebp), %xmm0 cvtss2sd %xmm0, %xmm0 andpd LCPI0_0, %xmm0 cvtsd2ss %xmm0, %xmm0

Mikrooptimierung - Zusammenfassung
MSVC 2010 generell schlecht icc kennt alle SSE-Tricks

clang irgendwo dazwischen
generell: Funktionen statt Makros
– einige Patterns von clang nicht erkannt

weiterführende Literatur: www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf

Schleifenoptimierungen
fission: Unterteilung in mehrere kleine Schleifen fusion: Zusammenfassung aufeinanderfolgender Schleifen

interchange: innere mit äußerer Schleife vertauschen
tiling: Iterieren über Blöcke (Cache-Optimierung) unrolling: (teilweises) Ausrollen unswitching: Entfernen loop-invarianter Bedingungen (vectorization: dazu später mehr) ...

Schleifenoptimierungen
unswitching: Entfernen loop-invarianter Bedingungen
for (int i if (mode x[i] = else x[i] = = 0; i < 100; ++i) == 1) sqrt(x[i]); exp(x[i]);

clang 3.2 -O3: cmpb jne
LBB0_1: ... movsd calll ... cmpl jne jmp LBB0_2: ... movsd calll ... cmpl jne

$1, (%eax) LBB0_2
# %for.inc.us %xmm0, (%esp) _sqrt $100, %esi LBB0_1 LBB0_3 # %for.inc %xmm0, (%esp) _exp $100, %esi LBB0_2

Schleifenoptimierungen
unswitching: Entfernen loop-invarianter Bedingungen
for (int i if (mode x[i] = else x[i] = = 0; i < 100; ++i) == 1) sqrt(x[i]); exp(x[i]);

icc 12 -O3: 000A1068 000A106B
000A106D ... 000A1073 000A107B ... 000A108B 000A108E ... 000A10D1 000A10D3 000A10D5 000A10DD ... 000A110C 000A110F xor movaps call

cmp je
edx,edx

byte ptr [ecx],1 main+0D1h (0A10D1h)

xmm0,xmmword ptr x[edi*4] __svml_expf4 (0A1F60h)

cmp jb
xor xor movaps rsqrtps cmp jb

edi,64h main+73h (0A1073h)

icc vektorisiert automatisch!

edx,edx eax,eax xmm2,xmmword ptr [esp+edx*4+80h] xmm0,xmm2 edx,64h main+0D5h (0A10D5h)

Schleifenoptimierungen
loop tiling: for (int i = 0; i < 500; ++i) for (int j = 0; j < 500; ++j) for (int k = 0; k < 500; ++k) z[i][j] += A[i][k] * B[k][j];

Schleifenoptimierungen
tiling-Test:
for (int i = 0; i < 500; ++i) for (int j = 0; j < 500; ++j) for (int k = 0; k < 500; ++k) z[i][j] += x[i][k] * y[k][j];

icc 12 -O3:
00F511C2 call

Danke, sehr lustig! _MATMUL_MKL (0F52050h)

Schleifenoptimierungen
tiling-Test:
for (int i = 0; i < 500; ++i) for (int j = 0; j < 500; ++j) for (int k = 0; k < 500; ++k) z[i][j] -= x[i][k] * y[k][j];

icc 12 -O3: vektorisiert, aber kein Tiling!

Intel empfiehlt Handarbeit:
software.intel.com/en-us/articles/how-to-use-loop-blocking-to-optimize-memory-use-on-32-bit-intel-architecture

Schleifenoptimierungen
tiling-Test:
for (int i = 0; i < 500; ++i) for (int j = 0; j < 500; ++j) for (int k = 0; k < 500; ++k) z[i][j] += x[i][k] * y[k][j];

clang 3.2 -O3:
LBB0_3: LBB0_4: xorl imull movss leal movl movss mulss addss addl addl jne movss incl cmpl jne addl incl cmpl jne %ecx, %ecx $2000, %esi, %edx # imm = _z(%edx,%ecx,4), %xmm0 _z(%edx,%ecx,4), %edx $-1000000, %edi # imm = (%ebx), %xmm1 _y+1000000(%edi,%ecx,4), %xmm1 %xmm1, %xmm0 $4, %ebx $2000, %edi # imm = LBB0_5 %xmm0, (%edx) %ecx $500, %ecx # imm = LBB0_4 $2000, %eax # imm = %esi $500, %esi # imm = LBB0_3 0x7D0

0xFFFFFFFFFFF0BDC0

movl

%eax, %ebx

LBB0_5:

0x7D0

0x1F4 0x7D0 0x1F4

Parallelisierung

MPI OpenMP

• many-core

immer Handarbeit
• multi-core

SIMD

• single core

meist Handarbeit

Vektorisierung
automatisch per Compiler:
– Abhängigkeitsanalyse oft unzureichend

 Metainformationen (z.B. pragmas) notwendig

Vektorrechner und Compiler seit den 70ern
– die Werkzeuge sind aber im vorigen Jahrhundert geblieben

Vektorisierung wurde mit SSE populär

weiterführende Literatur: Maleki, S. et. al.: An Evaluation of Vectorizing Compilers, PACT 2011

Scout: Introduction
Scout: A Source-to-Source Transformator for SIMD-Optimizations

C as input and output source code

focus on pragma-annotated loop vectorization
– loop body simplification by function inlining and loop unswitching – unroll or vectorize inner loops

replace expressions by their vectorized intrinsic counterparts
– even complex expressions like a+b*c (fmadd) or a<b?a:b (min) – and function calls like sqrt

User-Interface: GUI and command-line version Open source software tool: http://scout.zih.tu-dresden.de/
– used in the production to accelerate code without much effort – a means to investigate new vectorization techniques based on the clang parser

Olaf Krzikalla

Scout: Impression

vectorized loop

residual loop

Olaf Krzikalla

Scout: Measurements
first CFD computation kernel computes flow around air planes unstructured grid  indirect indexing

arrays-of-structure data layout
partial vectorization
– enforcing compiler auto-vectorization by pragmas lead to incorrect results

double precision, SSE platform  two vector lanes

Olaf Krzikalla

Scout: Measurements
Compiler: Intel 11.1, Windows 7, Intel Core 2 Duo, 2.4 MHz
int d [100]; #pragma scout loop vectorize for (i = 0; i < 100; ++i) { int j = d[i]; // first computations with // a[j], b[j] aso. } #pragma scout loop vectorize for (i = 0; i < 100; ++i) { int j = d[i]; // second computations with // a[j], b[j] aso. }

speedup relation Grid 1 Grid 2

original to vectorized 1.070 1.075

ideally a value near 2.0  unsatisfying result

Olaf Krzikalla

Scout: Measurements
lot of consecutive loops traverses over the same data structures:
int d [100]; #pragma scout loop vectorize for (i = 0; i < 100; ++i) { int j = d[i]; // first computations with // a[j], b[j] aso. } #pragma scout loop vectorize for (i = 0; i < 100; ++i) { int j = d[i]; // second computations with // a[j], b[j] aso. } int d [100]; #pragma scout loop vectorize for (i = 0; i < 100; ++i) { int j = d[i]; // first computations with // a[j], b[j] aso. // second computations with // a[j], b[j] aso.

handcrafted

}

speedup relation Grid 1 Grid 2

original to vectorized 1.070 1.075

merged to merged+vect. 1.391 1.381

original to merged+vect. 1.489 1.484

Olaf Krzikalla

Scout: Measurements
second CFD computation kernel computes interior flows of jet turbines structured grid  direct indexing

arrays-of-structure data layout
divided in three sub-kernels vectorization of complete loops

Olaf Krzikalla

Scout: Measurements
a CFD computation kernel computing interior flows of jet turbines
three-dimensional grid  (problem size)3 cells to compute single precision, target architecture: SSE  4 vector lanes

Olaf Krzikalla

Scout: Measurements
a CFD computation kernel computing interior flows of jet turbines single precision, target architecture: AVX  8 vector lanes

unsatisfying speedup due to memory bandwith and array-of-struct data layout

Olaf Krzikalla

Speicherlayout
Ausrichtung von Daten
struct A { int a; bool b; float c; }; – vom Compiler vorgenommen
Bytes 0 Variable

1
2 3

a

4
5 6 7 8 9

b
pad . .

c
10 11

Speicherlayout
Ausrichtung von Daten
struct A { int a; bool b; float c; void thread1() { while (!b) { doWork(); } } void thread2() { for (...) { c = /*...*/ } b = true; // terminate thread 1 } };
Bytes 0 Variable

1
2 3

a

4
5 6 7 8 9

b
pad . .

c
10 11

Speicherlayout
Ausrichtung von Daten
struct A { int a; bool b; float c; void thread1() { while (!b) { doWork(); } } void thread2() { for (...) { c = /*...*/ } b = true; // terminate thread 1 } };
Bytes 0 Variable

1
2 3

a

4

b
pad . .

64 Byte CacheLine!

5 6 7 8 9

c
10 11

Speicherlayout
Ausrichtung von Daten
struct A { int a; bool b; char padding[64-sizeof(b)-sizeof(int)]; float c; void thread1() { while (!b) { doWork(); } } void thread2() { for (...) { c = /*...*/ } b = true; // terminate thread 1 } }; – Verhinderung von false-sharing obliegt dem Programmierer!
Bytes 0 Variable

1
2 3

a

4
5 6 ... 64 65

b
pad . .

c
66 67

Fazit
Mikrooptimierungen vom Compiler befriedigend ausgeführt automatische Schleifenoptimierungen beschränken sich auf bekannte und einfache Patterns
– viele halbautomatische Schleifenoptimierer, für llvm z.B. Noise: http://llvm.org/devmtg/2013-04/karrenberg-poster.pdf

Beschleunigung durch Vektorisierung hängt von vielen Faktoren ab
– größtes Hindernis Speicherbus
 Cache-Optimierungen (z.B. tiling) notwendig  Handarbeit

Faktoren auf höherer Ebene vom Compiler gar nicht mehr greifbar

Optimierung ist und bleibt größtenteils Handarbeit!

Olaf Krzikalla

Diskussion

FRAGEN?
http://scout.zih.tu-dresden.de/