Dinamikus program analízis profile-ozás segítségével¶
A kód-profilozó eszközök segítik a program működését és erőforrás-felhasználását elemezni (legyez az az erőforrás CPU használat, sávszélesség, órajelek száma, memória használat). Ha optimalizálni akarjuk a kódunkat az első lépés, hogy adatot gyűjtünk róla. Melyik függvényben tölti a legtöbb időt a program? Mi fogyasztja a legtöbb memóriát? A profilozás eredményeire támaszkodva újraírhatjuk a programunk sokat fogyasztó, csekély hatékonyságú részeit, potenciális bugok detektálásban segíthet.
A legtöbb profilozó dinamikus elemzést alkalmaz, vagyis a futó programon végeznek méréseket. (Ezzel szemben a statikus elemzés a program forráskódját elemzi és az alapján következtet). A dinamikus mérések különféle stratégiával végezhetőek. Pl. mérést végző kód injektálása az elemzett programba, program futtatása egy mérésre kialakított, speciális környezetben, erőforrásigények nagy frekvenciájú mérése.
Futásidő mérés a time programmal¶
Unix rendszereken elérhető a time nevű program. Nagyon egyszerű mérőeszköz. Az egész program futásidejét lehet a segítségével kimérni. Például egyszerűen összevethetjük vele ugyanannak az algoritmusnak többféle megvalósítását. Például vessük össze a buborék rendezés és a gyorsrendezés időigényét ugyanazokon a tömbökön (bubbleSort.c és quickSort.c). A buborék rendezés egy egyszerűbb, lassabb algoritmus és ezt látjuk is az eredményekben. Már egy 1000 méretű tömb rendezésekor is észlelhető a különbség.
1 2 3 4 5 6 7 8 |
|
A real a teljes futásidő indítástól befejeződésig (beleértve azt az időt is, amit a process pl. I/O műveletre való várakozással tölt). A user az az idő, amit a CPU user módban tölt a process futása közben, a sys pedig a kernel módban töltött időt adja meg.
A time nem egy kifinomult profiling eszköz, de unix rendszereken alapból elérhető és használható a már lefordított binárisokon.
Gprof¶
A standard C/Unix profilozó eszköz a gprof. Mérési módszere a snapshot készítés. Leírás itt: https://sourceware.org/binutils/docs-2.16/gprof/
Rendkívül egyszerű a használata, annyi extra igénye van csak, hogy fordításkor kell egy extra kapcsoló. Le kell futtatni a programot, ekkor a gprof előállítja róla az adatokat, melyeket később ki lehet elemezni.
gcc bubbleSort.c -o bs -Wall -pg
A -pg a profilozó kapcsoló. Ez instrumentálja a kódban lévő függvényeket. A gprof hátránya, hogy ha olyan függvény van behívva, ami nem volt instrumentálva (pl. könyvtári függvények), akkor az abban tölött időről nem mond információt.
Ha a -pg-vel fordított programot lefuttatjuk kapunk egy gmon.out
fájlt a bináris mellett. Ezután az elemzés előállítása az alábbi programmal történik:
gprof bs gmon.out > analysis.txt
Minden profiling információ a txt-ben található. A buborék rendező program ezúttal egy 5000 méretű tömböt rendezett. Érdekesség, hogy a time a profilozáshoz instrumentált bináris futását 0,120 másodpercre mérte, míg a normális binárisét csak 0,072 másodpercre. Látható, hogy a profilozás jelentősen lassíthatja a program futását. A gyors rendezés ugyanekkora méretű tömb esetén sajnos túl gyors volt a gprofnak, nem szolgáltatott hozzá használható futásidő adatot. A buborékrendezés analíziséből részlet:
1 2 3 4 5 |
|
A futásidő 50%-a a bubbleSort függvényben lett eltöltve, 50%-a pedig a swap függvényben. Fontos adat még a self seconds oszlop: mennyi idő lett eltöltve csak abban az egy adott függvényben. Feltűnhet, hogy az összeg nem adja ki a time által mondott teljes futásidőt (nem instrumentált függvények hatása, pl. a printf-nek nyoma sincs az outputban).
Gcov¶
A kód lefedettségét lehet vele vizsgálni. Ez főleg annak meghatározásában hasznos, hogy ha írtunk teszteket a programhoz, azok mennyire fedik le a kódunk kódsorait illetve elágazásait (~ milyen alaposan van tesztelve a program). Használatához a fordításkor meg kell adni két extra kapcsolót:
1 |
|
Majd a futtatás(ok) után az alábbi módon lehet az outputot kinyerni:
1 |
|
Ha a korábban használt vigenere.c programot parancssori argumentum nélkül futtatjuk le az alábbi outputot kapjuk:
1 2 3 |
|
Vagyis a kódsoroknak csak egy kis hányada fog lefutni. Ha még egyszer lefuttatom és adok a programnak argumentumot, akkor a két lefedettség összegődik.
1 2 3 |
|
Rézletesebb, branch (elágazás) szintű lefedettség kapható a -b kapcsolóval:
1 2 3 4 5 6 7 |
|
Valgrind¶
Az egyik leginkább használt profilozó, hibadetektáló eszköz. Sokféle funkcióval, kiegészítő tool-al rendelkezik:
- Massif: heap profilozó
- Cachegrind: L1/L2 gyorsítótárakat szimulálja és számlálja hányszor volt hit/miss (megtalált/nem talált meg valamit a gyorsítótárban)
- Callgrind: a gprof-hoz hasonlóan számolgatja hányszor lett meghívva egy függvény, hány utasítás lett végrehajtva egy függvényben stb. Hívási gráfot is épít.
- Memcheck
A legfontosabb funkciója talán nem is a mérések elvégzése, hanem hogy segít memóriakezelési hibák detektálásában. Ezt alátámasztja, hogy defaultból a memcheck fut le az elemzett programra.
A valgrind egy virtuális gépen futtatja a programot, eközben végzi az ellenőrzéseket. Dinamikusan újrafordítja a programot, eközben egy köztes reprezentációba lesz lefordítva (IR - Intermediate representation), amit az elemzéshez használt programok manipulálhatnak. Sajnos ez az eljárás jelentősen növeli a futásidőt. A valgrind használatához nem kell extra fordítási kapcsoló, de ajánlott debug szimbólumokkal fordítani (-g), hogy pl. sorszámok is kerüljenek a valgrind outputjába.
Profilozás¶
Tekintsük továbbra is a buborékrendezést. A két fő profilozó eszköz a Callgrind és a Cachegrind.
1 2 3 4 5 6 7 8 |
|
A buborékrendezés 5000 elemú tömbön futott le. Ezalatt a valgrind több mint 456 millió "eseményt" röngzített. Ez a végrehajtott assembly utasítások számának fogható fel.
Futás közben keletkezett egy callgrind.out.69911 nevű fájl is a bináris mellett (ahol a szám a process id) Ez egy szöveges output, de még kevésbé érthető, mint amit a gprof produkál. Feldolgozásában segít a callgrind_annotate program. A program használata:
1 |
|
Egy output részlet:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A számok mutatják, hogy az adott C kódsorhoz mennyi gépi utasítás végrehajtása tartozott. Látszik az is, hogy a swap 6,149,290-szor lett meghívva. Ezek a számok exkluzívak, azaz a meghívott függvényekben lefutott utasítások száma nem adódik hozzá a hívó számaihoz.
1 2 3 4 5 6 7 8 9 10 11 |
|
A printArray függvény utasításait nézve látjuk, hogy a könyvtári függvények is fel vannak oldva, azokhoz is tartozik utasításszám.
A program két legtöbbet használt függvénye (és a bennük végrehajtott utasításszám):
1 2 |
|
A gyorsítótár szimuláláshoz a --simulate-cache=yes
kapcsolóval kell futtatni a valgrind-et. Ebbe nem megyünk bele, de érdekességképp megtekinthető milyen eredményt ad.
Ha sokalljuk a callgrind outputját, lehet szűrést is megadni neki, mit mérjen! Például így csak az swa
szubsztringet tartalmazó függvényekben mér (ami a bubbleSort programunk esetén csak a swap függvényre lesz igaz)
1 2 3 4 5 6 |
|
Ha pedig a sok szöveges outputból van már elegünk, akkor erre is van megoldás: a KCacheGrind grafikusan jeleníti meg a callgrind általal generált fájlt.
kcachegrind callgrind.out.69911
A látvány első ránézésre ijesztő, a program rengeteg információt zúdít ránk. Baloldalt a programunkban meghívott függvények listája látható. Legfölül van az, amelyikben a legtöbb időt töltötte. Ha nem értjük mi a legfölső függvény, segít a jobb alul látható hívási gráf kezdemény (Call graph fül). A képen nem látszik az egész gráf, de ez a legfölső függvény a belépési pont: ez fogja meghívni a main függvényt a _start-on és a below main-en át.
A kép jobb fölső részén lévő diagram (Callee Map) azt szemlélteti, hogy mely két függvényben töltötte a legtöbb időt a program. A barna és a kék kockát (bubbleSort, swap függvények) körbebvevő sötétebb illetve világosabb zöld keretek az őket hívó függvényeket (pl. main) reprezentálják.
Az alábbi kép a printArray függvényről kiexportálható hívási gráf: látjuk a hívókat és a meghívottakat (ami érdekesség, hogy látszódnak a printf "alatti" hívások is, amik a tényleges kiíratást végzik)
Tehát a KCacheGrind kiválóan alkalmas a program működésének elemzésére (mi lett hívva, mennyiszer, mi mit hívott stb.)
Memóriahasználat¶
Tekintsük az alábbi programot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
A program a main
függvényben beolvas egy számot és egy sztringet. Ezt a sztringet aztán annyiszor megismétli egy dinamikusan foglalt tömbben, amennyi a beolvasott szám. Hol vannak hibák a programban?
Memória ellenőrzés:
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./memory
Opcionálisan még a verbose
kapcsoló is megadható részletesebb outputért.
Egy output részlet:
1 2 3 4 5 6 |
|
definetly lost:
garantáltan felszabadítatlanul maradt memóriarészek, melyek már nem elérhetőek (megszűnt a rájuk mutató pointer) Példaindirectly lost:
elveszett a pointer egy olyan objektumra, ami további dinamikus memóriaterületre mutatott. Példapossibly lost:
Elméletileg még elérhető volna a lefoglalt memóriaterület valamilyen trükkel, hogy fel legyen szabadítva a terület, de valószínűleg inkább hibáról van szó (például ha elléptettük a tömb elejére mutató pointert) Példastill reachable:
Pl. a dinamikusan foglalt memóriára egy globális változóként tárolt pointer mutat. Vagyis a memória nincs felszabadítva, de még a program futásának a végén is elérhető volna. Példasuppressed:
Ha megadunk a valgrindnek egy listát, ide sorolódnak azok a memory leakek, amiket nem akarunk/tudunk javítani (pl. library hibája)
Példafeladatok¶
- Buborék rendezés
- Gyorsrendezés
- Memóriaszivárgás 1, Memóriaszivárgás 2
- Nem inicializált terület használata
- Nem inicializált terület használata 2
- Invalid memóriaterület írása