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