Laboratoare:Profiling

= Introducere =

Un profiler este un utilitar de analiză a performanței care ajută programatorul să determine punctele critice (bottleneck) ale unui program. Acest lucru se realizează prin investigarea comportamentului acestuia, evaluarea consumului de memorie și relaţia dintre modulele acestuia.

=Tehnici de profiling=

Tehnica de instrumentare
Profiler-ele bazate pe această tehnică necesită de obicei modificări în codul programului: se inserează secțiuni de cod la începutul și sfârșitul funcției ce se dorește analizată. De asemenea, se rețin și funcțiile apelate. Astfel, se poate estima timpul total al apelului în sine cât și al apelurilor de subfuncții. Dezavantajul major al acestor profilere este legat de modificarea codului: în funcții de dimensiune scăzuta și des apelate, acest overhead poate duce la o interpretare greșită e rezultatelor.

Tehnica de eșantionare (sampling)
Profiler-ele bazate pe sampling nu fac schimbări in codul programului, ci verifica periodic procesorul cu scopul de a determina ce funcţie (instrucţiune) se executa la momentul respectiv. Apoi estimează frecventa si timpul de execuţie al unei anumite funcţii într-o perioada de timp.

Suporturi pentru profiler
În Linux (și nu numai), suportul pentru profilere este disponibil la nivel de:
 * biblioteca C (GNU libc), prin informații de timp de viață al alocărilor de memorie
 * compilator. Modificarea codului în tehnica de instrumentare se poate realiza ușor în procesul de compilare, compilatorul fiind cel ce inserează secțiunile de cod necesare.
 * nucleu al sistemului de operare, prin punerea la dispoziție de apeluri de sistem
 * hardware: multe procesoare sunt dotate cu contoare de temporizare (Time Stamp Counter - TSC) sau contoare de performanță care numără evenimente ca cicluri de procesor sau TLB misses.

=Linux=

perfcounters
TODO: vezi http://lwn.net/Articles/338576/bigpage

Perfcounters added to the mainline poate inlocuim oprofile?

gprof
gprof este utilitarul de profiling folosit în combinație cu gcc. Pentru folosirea gprof trebuie compilat programul cu suport de profiling prin folosirea opțiunii -pg.

Astfel, pentru fișierul de mai jos:

După compilare:

Se observă că timpii obtinuti nu sunt  specificați în unități măsurabile. Pentru aceasta se va executa codul din funcția main de un număr dat de ori:

Informația obținută este următoarea:

Oprofile
Oprofile este un sistem de profiling disponibil ca modul pentru kernel-ul de Linux (și integrat în variantele mai noi ale acestuia), capabil să analizeze atât kernel-ul, cât și aplicațiile utilizator. Folosește tehnica de sampling și se bazează pe informațiile oferite de contoarele din CPU.

Avantaje: Dezavantaje:
 * overhead redus
 * profiling al întregului sistem (inclusiv secţiuni "delicate" din kernel)
 * prezentarea efectelor la nivel de hardware
 * necesita drepturi de utilizator privilegiat (root)
 * nu poate fi folosit pentru cod compilat dinamic sau interpretat (Java, Python, etc)

Arhitectura și modul de funcționare
Procesoarele au niște countere speciale pe care le decrementează de fiecare dată când are loc un eveniment de un anumit tip (cache miss, tlb miss, branch miss-predictions, etc.). Când un astfel de counter se ajunge la valoarea zero, se trimite o întrerupere NMI care este interceptată de modulul de kernel al oprofile. Modulul de kernel resetează counterul la o valoare fixată de utilizator și salvează într-un buffer din kernel date despre locația care a generat întreruperea (proces/kernel, id-ul threadului, instrucținea curentă care a generat decrementarea counterului, etc.).

Oprofile e format din 3 componente majore:
 * modul de kernel - un driver special care este notificat de la fiecare întrerupere NMI generată de procesor
 * daemon - intermediar între modulul kernel și toolurile userspace. Preia date din kernel și le scrie (după procesare) în /var/lib/oprofile/samples/.
 * utilitare user space:
 * opcontrol - permite configurarea evenimentelor monitorizate și a frecvenței cu care sunt eșantionate, a proceselor pentru care se face monitorizarea, etc.
 * opreport - sumar al monitorizării
 * opannotate</tt> - adnotează pe surse sau pe codul dezasamblat al programului numărul de evenimente care au fost eșantionate la fiecare instrucțiune.

Configurare
Primul pas este verificarea existenței modulului oprofile: Apoi este necesară încărcarea efectivă:

Înainte de a începe colectarea datelor de profiling, trebuie configurat și pornit daemonul oprofiled</tt>. Pentru a afla informații despre kernel, oprofile</tt> are nevoie de calea către imaginea vmlinux (necompresată) a kernelului activ: Dacă nu se dorește analizarea kernelului, ci doar a unor aplicații, se folosește: Pentru a afla tipurile de evenimente disponibile si detalii despre acestea (inclusiv valoarea minima a contorului asociat) se foloseste: opcontrol</tt> poate monitoriza maximum două tipuri de evenimente în același timp (deoarece procesoarele nu au mai multe countere de performanță care pot fi folosite simultan). Pentru a alege evenimentele dorite se folosește opțiunea --event</tt>. Sintaxa acesteia este: Unde:
 * TIP_EVENIMENT - reprezintă numărul evenimentului care se dorește monitorizat
 * COUNT - reprezintă numărul de evenimente hardware de acest tip după care procesorul emite o întrerupere NMI.
 * UNIT-MASK - o valoare numerică ce poate modifica comportamentul pentru counterul curent. Dacă nu este specificat se folosește o valoarea implicită (poate fi determinată cu ophelp</tt>).
 * KERNEL-SPACE-COUNTING - poate lua două valori, 0/1, și specifică dacă se activează sau nu counterul atunci când se rulează cod kernel. Implicit are valoarea 1.
 * USER-SPACE-COUNTING - paote lua două valori, 0/1, și specifică dacă se activează sau nu counterul atunci când se rulează cod user. Implicit are valoarea 1.

De exemplu: pe procesoare Core 2 duo, counterul DTLB_MISSES poate fi folosit cu oricare combinație a următoarelor valori:
 * 0x01: ANY      Memory accesses that missed the DTLB.
 * 0x02: MISS_LD  DTLB misses due to load operations.
 * 0x04: L0_MISS_LD L0 DTLB misses due to load operations.
 * 0x08: MISS_ST  TLB misses due to store operations.

Pentru a vedea care sunt parametrii cu care este configurat în acest moment oprofile se poate folosi

Pornire
După configurare, trebuie pornit daemonul:

Colectarea și analizarea datelor
Pentru a nu fi influențați de eventuale date rămase de la o rulare anterioară, trebuiesc curațate datele salvate:

Odată pornit daemonul, oprofile</tt> va colecta date despre toate executabilele ce rulează pe acea mașină (sau dacă a fost configurat cu --image</tt> va colecta date doar pentru executabilele din calea specificată). Acum trebuie executat și binarul pe care dorim să îl măsurăm. Deoarece oprofile</tt> încearcă să-și minimizeze impactul asupra performanțelor sistemului, acesta va amâna pe cât posibil scrierea datelor din bufferele interne pe disk. Dacă toolurile userspace opreport</tt> sau opannotate</tt> nu găsesc informații de profiling pe disk vor raporta o eroare: Putem să forțăm scrierea pe disc a bufferelor curente cu:

Exemplu de rulare
Avem următorul program care calculează numărul de numere divizibile cu 2 și cu 3 din intervalul 0..230 (exercițiu pur teoretic :). Dacă rulăm programul obținem:

Dorim să îmbunătățim performanțele programului și îl paralelizăm.

O variantă ar fi să incrementăm variabilele div2</tt> și div3</tt> din două threaduri diferite protejând accesul la variabilele div2</tt> și div3</tt> cu un mutex. Această implementare va fi mult mai ineficientă decât cea prezentată mai sus, majoritatea timpului de rulare fiind petrecut în rutinele de preluare și eliberare a mutexului.

O paralelizare mai bună se poate implementa folosind variabile separate pentru fiecare thread și integrarea rezultatelor parțiale la final (folosind o paradigma de programare de tipul map-reduce). Programul paralelizat este mai complex, performanțele lui fiind: Pentru programul paralelizat Intuitiv timpul de procesor ar fi trebuit să crească, dar timpul de rulare ar fi trebuit să scadă. Vom investiga problema folosind oprofile.
 * timpul total de rulare este cu 350% peste cel al variantei neparalelizate.
 * timpul de procesor este cu 700% peste cel al variantei neparalelizate, folosind 2 core-uri în același timp

Partea computațională (for-ul) folosește puține date și ar trebui să încapă în cache-ul L1. Vom verifica dacă se fac accese în cache-ul L2. Din rezultatele monitorizării se observă că se fac un număr mare de accese în cache-ul L2 când se accesează datele per-cpu, deși datele în loop încap în cache-ul L1. Procesorul pe care a fost rulat exemplul anterior deține câte un cache L1 pentru fiecare core și un cache L2 partajat. Dacă mai multe core-uri modifică date care corespund unei aceleiași linii din cache-ul L2, de fiecare dată când unul din core-uri scrie în acea linie, linia va fi invalidată în cache-ul L1 al celorlalte core-uri. Astfel celelalte threaduri vor trebui să recitească din L2 toată linia curentă, cache-ul L1 ne mai ajutând în acest caz, toate acesele făcându-se la o viteză mai mică chiar decât cea a cache-ului L2.

Dacă se modifică structura de date specifică fiecărui thread, se poate elimina problema de "false sharing". În acest caz a avut loc doar un singur eșantion de citire în L2 (în realitate au avut loc între 7500 și 2*7500-1 evenimente de acest tip).
 * timpul total de rulare este 58% din cel al variantei neparalelizate
 * timpul de procesor este 112% din cel al variantei neparalelizate.

= Windows =

Kernrate si KrView
Kernrate este un echivalent al oprofile pentru Windows. 

XPerf


= Exerciții =

Prezentare
Pentru a urmări mai ușor noțiunile expuse la începutul laboratorului folosiți această prezentare (pdf) (odp).

Exerciții de laborator
<ul> <li>Folosiți arhiva de resurse a laboratorului.</li> <li>Puteți folosi fișierele tags pentru o parcurgere rapidă a surselor folosind vim (fișierul tags) sau Emacs (fișierul TAGS).</li>

</ul>


 * 1) (2 puncte) Intrați în directorul 01-bubble-sort/</tt>.
 * 2) * Analizați conținutul fișierului <tt>bubble-sort.c</tt>.
 * 3) * Completați funcția <tt>bubble_sort</tt> cu o implementare de Bubble sort.
 * 4) * Comparați, folosind <tt>gprof</tt>, rezultatele obținute în momentul utilizării funcției <tt>bubble_sort</tt> implementate în C și funcției <tt>bubble_sort</tt> implementate în assembly.
 * 5) * Folosiți _apoi_ flag-ul <tt>-O2</tt> pentru a compila sursa folosind suportul de optimizări ale compilatorului și comparați rezultatele de profiling obținute cu cele anterioare.
 * 6) * Care sunt părțile din cod peste care se pierde mult timp?
 * 7) * Hints:
 * 8) ** Folosiți funcția swap pentru interschimbarea a două valori
 * 9) ** Folosiți opțiunea <tt>-lb</tt> a <tt>gprof</tt> pentru informații sumare despre timpul petrecut în diversele zone de cod.


 * 1) (2 puncte) Intrați în directorul <tt>02-major/</tt>.
 * 2) * Scrieți 2 programe prin care să determinați, cu ajutorul <tt>oprofile</tt>, dacă limbajul C este column-major sau row-major (puteți citi mai multe aici).
 * 3) * Hints:
 * 4) ** Un program va completa o matrice iterând pe linii, celălalt pe coloane.
 * 5) ** Folosiți evenimentul <tt>BSQ_CACHE_REFERENCE</tt> cu masca <tt>0x07</tt>.
 * 6) ** Folosiți <tt>opcontrol --image=program-name</tt> pentru a urmări doar evenimentele care au loc când se rulează cod din programul <tt>program-name</tt>.
 * 7) ** Folosiți <tt>opreport</tt> pentru a afișa informații de debug detaliate
 * 8) ** Nu uitați să ștergeți mesajele de log de la precedenta rulare a <tt>oprofile</tt>


 * 1) (2 puncte) Intrați în directorul <tt>03-find-char/</tt>.
 * 2) * Analizați conținutul fișierului <tt>find-char.c</tt>.
 * 3) * Compilați fișierul <tt>find-char.c</tt> și rulați executabilul obținut.
 * 4) * Identificați, folosind <tt>gprof</tt>, care este funcția care ocupă cel mai mult timp de procesor și poate îmbunătățiți performanțele programului.


 * 1) (2,5 puncte) Intrați în directorul <tt>04-tlb/</tt>.
 * 2) * Analizați conținutul fișierelor <tt>tlbp.c</tt>, respectiv <tt>tlbt.c</tt>. Ce se realizează în cadrul celor două programe?
 * 3) * Compilați cele două programe.
 * 4) * Contorizați timpul de execuție folosind <tt>time</tt>. Care program se rulează mai repede?
 * 5) * Folosiți <tt>oprofile</tt> pentru a determina în rularea cărui program se obțin cele mai multe TLB miss-uri.
 * 6) * Hints:
 * 7) ** Folosiți <tt>ophelp</tt> pentru a determina evenimentul ce poate fi folosit pentru detectarea evenimentelor de tip TLB miss.
 * 8) ** Folosiți <tt>opcontrol --image=program-name</tt> pentru a urmări doar evenimentele care au loc când se rulează cod din programul <tt>program-name</tt>.


 * 1) (2,5 puncte) Intrați în directorul <tt>05-hash/</tt>.
 * 2) * În directorul <tt>05-hash/</tt> se găsește o implementare a unui algoritm pentru tabele distribuite scrisă de Alexandru pentru tema de la CPL.
 * 3) * Deși dimensiunea tabelului este dublă față de numărul de cuvinte, programul are o comportare ineficientă. De ce? Reparați greșeala. (Google is your best friend).

= Soluții =

= Resurse utile =


 * Intel® 64 and IA-32 Architectures Optimization Reference Manual Appendix B
 * Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3B Appendix A
 * False sharing -
 * [[Media:kernrate.pdf | Documentatie kernrate]]
 * gprof manual
 * gcov manual