Computerarchitectuur 2016 Inleveropdracht 3: Exploiting Memory

advertisement
Computerarchitectuur 2016
Inleveropdracht 3:
Exploiting Memory Hierarchies in Modern Systems
Gesuggereerde Deadline: zondag 27 november 2016
Zoals we in het hoorcollege uitgebreid hebben besproken spelen geheugenhiërarchieën een cruciale
rol in hedendaagse computersystemen. In deze opdracht zullen we gaan kijken hoe we onze kennis
hiervan kunnen gebruiken om het gedrag van programma’s te begrijpen en te optimaliseren. Tevens
is er de mogelijkheid een programma te schrijven dat de geheugenhiërarchie van een systeem
analyseert.
Bij de analyse van het gedrag van de programma’s zullen we gebruik maken van “hardware
performance counters”. Deze tellers houden allerlei gebeurtenissen in de processor bij, zoals het
aantal cache misses, verkeerd voorspelde branches, enzovoort. Op Linux-systemen kunnen we met
het programma perf de hardware performance counters uitlezen1 . We kunnen dit bijvoorbeeld
doen voor het programma ls:
$ perf stat
ls -1 /usr/bin > /dev/null
Performance counter stats for ’ls -1 /usr/bin’:
10.624315
1
0
448
16,994,789
<not supported>
<not supported>
30,483,289
4,958,601
91,145
task-clock
context-switches
CPU-migrations
page-faults
cycles
stalled-cycles-frontend
stalled-cycles-backend
instructions
branches
branch-misses
#
#
#
#
#
#
#
#
0.968
0.000
0.000
0.042
1.600
CPUs utilized
M/sec
M/sec
M/sec
GHz
1.79 insns per cycle
466.722 M/sec
1.84% of all branches
0.010975084 seconds time elapsed
We krijgen allerlei statistieken te zien zoals het aantal uitgevoerde instructies en het aantal clock
cycles dat is gebruikt. Met de optie -e kunnen we andere “events” opgeven, -r geeft aan hoe vaak
het experiment moet worden herhaald:
$ perf stat -e instructions:u,cycles:u,cache-misses:u,cache-references:u -r 5 \
ls -1 /usr/bin > /dev/null
Performance counter stats for ’ls -1 /usr/bin’ (5 runs):
25,515,700
13,013,545
1,644
21,742
instructions:u
cycles:u
cache-misses:u
cache-references:u
#
#
#
1.96 insns per cycle
0.000 GHz
7.559 % of all cache refs
0.010833973 seconds time elapsed
(
(
(
(
+- 0.00% )
+- 0.40% )
+- 24.59% )
+- 2.48% )
( +-
0.47% )
De toevoeging :u duidt aan dat alleen events binnen user-space moeten worden geteld, events
binnen de kernel tijdens het uitvoeren van dit programma tellen niet mee. Nog drie punten van
aandacht:
1 perf
staat alleen geı̈nstalleerd in zalen 302, 303, 305 en 306
1
• Alle gerapporteerde getallen zijn schattingen en geen exacte aantallen. Het exact tellen van
events brengt zeer veel overhead met zich mee; in feite zou de machine dan alleen maar bezig
zijn met het verwerken van de counters en niet met het uitvoeren van het programma zelf.
• Meet niet te veel events tegelijkertijd. De hardware kan maar een vast aantal events tegelijkertijd meten. Als er meer events worden opgegeven, dan moet de tijd tussen de events
worden verdeeld en dit komt de nauwkeurigheid niet ten goede.
• Voor de nauwkeurigheid van de metingen helpt het vaak om het programma toe te wijzen
aan een vaste core. Dit kan als volgt: taskset -c 1 ./mijnprogramma. En in combinatie
met perf stat: perf stat -e cycles:u taskset -c 1 ./mijnprogramma.
Verder is het zeer belangrijk om in de gaten te houden wat er precies wordt geteld bij de verschillende events. Voor “cache-misses” wordt het aantal LLC (Last-Level Cache of Longest-Latency
Cache) misses geteld. “cache-references” telt het aantal keer dat de last-level cache is benaderd.
Het programma perf kent een lijst ingebouwde events die kunnen worden bekeken met het commando perf list. Niet alle events staan echter in deze lijst. Wanneer je een event wilt meten
dat niet in de lijst staat, maar wel wordt ondersteund door de processor, dan moet je de exacte
code van het event opgeven. Ter inspiratie:
r0151
L1D.REPLACEMENT
r01d1
MEM LOAD UOPS RETIRED.L1 HIT
r08d1
r02d1
r10d1
r04d1
r20d1
MEM
MEM
MEM
MEM
MEM
LOAD
LOAD
LOAD
LOAD
LOAD
UOPS
UOPS
UOPS
UOPS
UOPS
RETIRED.L1 MISS
RETIRED.L2 HIT
RETIRED.L2 MISS
RETIRED.LLC HIT
RETIRED.LLC MISS
Aantal cache-lines dat in de L1 data cache is
ingelezen.
Het aantal “retired” operaties dat resulteerde in een L1 hit. Retired houdt in dat
de operatie de reorder buffer (ROB) verlaat
en dus ook moest worden uitgevoerd. Speculatief uitgevoerde instructies die uiteindelijk niet moesten worden uitgevoerd, worden
nooit “retired”.
Op dezelfde manier het aantal L1 misses.
Op dezelfde manier het aantal L2 hits.
Op dezelfde manier het aantal L2 misses.
Op dezelfde manier het aantal LLC (L3) hits.
Op dezelfde manier het aantal LLC (L3) misses.
Een dergelijke code wordt gewoon opgegeven bij het -e argument, bijvoorbeeld -e r02d1:u. De
liefhebber kan een volledige lijst van events terugvinden in Hoofdstuk 19 van Volume 3 (System
Programming Guide) van de “Intel 64 and IA-32 Architectures Software Developer’s Manual”
Tenslotte is het belangrijk om in de gaten te houden op welke processor je aan het experimenteren bent. Verschillende processoren gebruiken verschillende codes voor de performance counters
en hebben verschillende cache eigenschappen. Met het programma lscpu kunnen een aantal eigenschappen van de processor worden weergegeven. De Dell-machines in zaal 302 bevatten een
Intel Core i7-3770 (dit kan worden opgezocht met cat /proc/cpuinfo) en dit is een chip van de
“Ivy Bridge” generatie, ofwel een derde-generatie Intel Core processor. Deze processor beschikt
over 32K L1 data cache per core, 256K L2 cache per core en 8M gedeelde L3 cache.
Opgaven
Zorg dat je in het ca2016 environment zit, zodat je gebruik kan maken van een recente versie van
de gcc compiler:
2
source /vol/share/groups/liacs/scratch/ca2016/ca2016.bashrc
1. Als eerste bekijken we een implementatie van matrixvermenigvuldiging. Zie matrixmul.c in
de materialen. Gebruik perf om het gedrag van het programma in kaart te brengen. Hoeveel
cache misses vinden er plaats? Wat gebeurt er wanneer N (zien bovenaan het bestand) wordt
veranderd? Wat is de CPI van het programma? Schrijf in je verslag een korte discussie over
je bevindingen. Vermeld ook het processor-type waarop de analyse is uitgevoerd!
2. Maak een kopie van de source code en pas “Loop Blocking” toe op de matrixvermenigvuldigingsroutine. Kies een geschikte “block size” om mee te beginnen (in ieder geval een macht van
2!). Denk bij het kiezen van de initiële “block size” aan de grootte van de L1 cache en wat
voor data daar moet worden opgeslagen.
Voer vervolgens experimenten uit met de geblokte versie van matrixvermenigvuldiging. Is
de performance beter (CPI)? Is het aantal cache misses afgenomen? Is de executietijd afgenomen? Wat gebeurt er voor verschillende waarden van de “block size”?
Plaats de code van je geblokte matrixvermenigvuldiging in je verslag. En schrijf weer een
aantal paragrafen over je bevindingen.
3. Het bestand matrixvecadd.c bevat een code (add vec) die elke kolom van een matrix met
2 vermenigvuldigt en daar een kolomvector b bij optelt. Benchmark het programma. Bekijk
de source code van add vec. Wat zou je aan deze routine kunnen veranderen om de performance te verbeteren? Pas deze verandering toe en analyseer of deze affect heeft.
Plaats in het verslag de aangepaste routine en een discussie over het effect van de aanpassingen.
Met behulp van SIMD-instructies is het mogelijk om de performance van de routine verder te
verbeteren. De processor in de Dell-machine ondersteunt AVX SIMD instructies. Door gebruik
te maken van “intrinsics” kunnen we deze instructies in ons C-programma gebruiken zonder assembly code te schrijven. Er moeten speciale datatypen worden gebruikt die vectoren voorstellen.
Bijvoorbeeld:
/* Maak een 256-bit vector met 8 floats en geef elk element de waarde 19
*/
float mul = 19.;
__m256 mul_vector = _mm256_broadcast_ss (&mul);
/* Laad 8 floating-point waarden uit het geheugen in een vector , startende bij
* het gegeven adres (pointer).
*/
float array[1024];
__m256 tmp1 = _mm256_load_ps (&array[0]);
De suffix “ss” staat voor “scalar single-precision” en “ps” voor “packed single-precision”. “packed”
duidt aan dat het argument meerdere waarden bevat (en dus een vector is). Handig bij het
programmeren is de Intel Intrinsics Guide: https://software.intel.com/sites/landingpage/
IntrinsicsGuide/. Je kunt op elke intrinsic klikken voor informatie en documentatie. Gebruik
AVX instructies en geen AVX2 instructies (deze laatste worden door deze hardware niet ondersteund).
4. Schrijf nu een SIMD-variant van je geoptimaliseerde add vec routine. Ga uit van N = 1024.
Benchmark de code en probeer met behulp van SIMD een hogere performance te behalen
3
dan de niet-SIMD code.
In je verslag plaats je de SIMD code en een korte discussie. Kijk bijvoorbeeld naar het
aantal instructies en de CPI.
5. Voor de liefhebbers: schrijf een programma dat het aantal caches en de grootte van de
caches in een systeem kan bepalen. Dit wordt gedaan door data te verwerken in steeds
grotere arrays. Voor de verschillende arrays wordt de executietijd bepaald. Dit alles leidt
tot een plot waarin de verschillende caches zichtbaar zijn, de plot heeft de vorm van een
“trap”.
Stuur het geschreven programma mee en schrijf voor het verslag een korte discussie aan
de hand van de geproduceerde plot. Komt de plot min of meer overeen met de caching
statistieken zoals gerapporteerd door lscpu?
Een aantal hints en tips:
• Je hebt een goede timer nodig om de executietijd van de test-loop te meten. Bijvoorbeeld:
#include <time.h>
struct timespec start, end, elapsed;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID , &start);
/* ... */
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
timespec_subtract(&elapsed, &end, &start);
Voor een implementatie van timespec subtract zie https://www.gnu.org/software/
libc/manual/html_node/Elapsed-Time.html.
• Het experiment moet worden uitgevoerd voor arrays van oplopende grootte. Begin
bijvoorbeeld met 256 bytes en vermenigvuldig de grootte steeds met 2. Ga door tot ten
minste 64 * 1024 * 1024 bytes.
• Een experiment bestaat uit de volgende stappen: (1) alloceer een array van de gewenste
grootte, (2) initialiseer de array, (3) roep een functie aan die de cache “trasht” (effectief
leegmaakt), (4) lees de array en herhaal dit 1000 keer, (5) geheugen vrijgeven.
• Cache trashing kan worden gedaan door een array te maken die ruim groter is dan de
L3 cache en deze bijvoorbeeld 5 maal volledig te beschrijven.
• Het lezen van de array moet gebeuren met een niet-regulier toegangspatroon. Als
dit niet wordt gedaan, worden de caches niet zichtbaar omdat de prefetcher het toegangspatroon herkent en alle data al klaar kan zetten. Je moet dus zorgen voor een
toegangspatroon dat de prefetcher niet kan herkennen.
Wat je kunt doen, is de array zo initialiseren dat elk array element wijst naar het
volgende te lezen element. En het laatste element wijst weer naar index 0. Dus bijvoorbeeld a[0] = 3, a[3] = 9, a[9] = 11, a[11] = 0. Zorg dat de sprongen die
worden gemaakt niet regulier zijn! Gebruik bijvoorbeeld 8 verschillende increments
(strides) die je na elkaar gebruikt, zoals hierboven: +3, +6, +2, enz.
Het experiment bewandelt dan deze ketting (next = a[next]) totdat je 0 weer tegenkomt.
Inleveren
Er mag worden gewerkt in duo’s. Het volgende moet worden ingeleverd:
4
• Het verslag, in tekst- of PDF-formaat.
Zorg ervoor dat alle bestanden die worden ingeleverd (source code, verslag, enz.) zijn voorzien van
naam en studentnummer! Plaats alle bestanden om in te leveren in een aparte directory (bijv.,
opdracht3) en maak een “gzipped tar” bestand (het is prima als source code en verslag in dezelfde
tar.gz terechtkomen):
tar -czvf opdracht3-sXXXXXXX-sYYYYYYY.tar.gz opdracht3/
Vul op de plek van XXXXXXX en YYYYYYY de bijbehorende studentnummers in. De inzendingen
kunnen worden verzonden per e-mail naar ca2016 (at) handin.liacs.nl met als onderwerp
“CA Opdracht 3”. Vermeld in de e-mail ook namen en studentnummers.
5
Download