Profiler je program, který umí statisticky analyzovat, ve kterých funkcích a cyklech program tráví nejvíce času. Jen takto vytipované části kódu pak má smysl optimalizovat. Je zbytečné ztrácet čas optimalizováním funkce, která se při běhu programu vyvolá jednou a program v ní stráví pár milisekund.
Profilování se obvykle skládá se dvou kroků. První krok můžeme nazvat měřením. Jde o získávání statistických údajů o běžícím programu. Tyto údaje se obvykle ukládají do nějakého pomocného logovacího souboru. Zaznamenávají se do něj zejména počty vyvolání jednotlivých funkcí, počty iterací v jednotlivých cyklech, počty přístupů do paměti (cache, RAM), případně i počty přístupů na disk nebo k jiným zdrojům (síť, různá zařízení, atd.). Dále se zaznamenává i doba strávená při těchto zaznamenaných činnostech.
Druhý krok profilování lze nazvat profilovací analýza. V tomto kroku získáváme z naměřených údajů různé statistiky. Existují programy s grafickým rozhraním, které umožňují nejenom spočítat, která funkce byla volána nejčastěji, ale dokáží se na naměřená data podívat z různých úhlů a zobrazit výsledky přehledně pomocí tabulek nebo grafů. Pomocí profilovací analýzy se obvykle snažíme vytipovat ta místa programu, v nichž program tráví nejvíce času. Tím, že získáme přehled o tom, jak se jednotlivé funkce podílejí na celkové době běhu programu nebo na přístupu k jiným zdrojům (paměť, síť, atd.), se můžeme lépe rozhodnout která místa a jakým způsobem budeme optimalizovat.
Vysvětlili jsme si, že profilování se skládá ze dvou kroků, měření a analýzy. Tomu odpovídá i rozdělení nástrojů pro profilování. Jedna část nástrojů se zaměřuje na sbírání údajů o běhu programu, další programy se pak zaměřují na analýzu a prezentaci naměřených dat.
Generování údajů o běhu programu úzce souvisí s instrumentací programů. Jednoduchou analýzu můžeme provádět sami tím, že do svého kódu doplníme vhodné výpisy, pomocí nichž poté spočítáme, kolikrát program navštívil sledovaná místa v programu. Vhodné je umístit výpisy na začátky a konce všech funkcí a také všech cyklů, protože to jsou místa, kde program typicky tráví nejvíce času. Nevýhodou tohoto přístupu je pracnost a nespolehlivost. Snadno můžeme na některé významné místo zapomenout. Další nevýhodou může být i fakt, že takto doplněná instrumentace činí zdrojový kód méně přehledným.
Další možností je generování dat pro profilovací analýzu automatizovaným způsobem buďto samotným překladačem nebo specializovaným programem. Princip sběru dat zůstává podobný jako při ruční instrumentaci zdrojového kódu. Rozdíl je ale v tom, že nyní zůstává zdrojový kód nedotčený.
Principiálně podobný způsob sběru dat jako ruční doplňování kódu o pomocné výpisy nabízejí samotné překladače. Pokud se budeme zabývat programováním v jazycích C a C++ a použijeme překladač GCC, můžeme snadno získávat profilovací data tak, že program přeložíme s přepínačem -p
, resp. -pg
. Překladač pak přímo do výsledné aplikace doplní kód, který po spuštění programu generuje tzv. graf volání (call graph) a uloží jej do souboru gmon.out
. Viz ukázka 1.
Pomocí programu prof
, resp. gprof
se pak zobrazí výsledky analýzy v čitelné formě. Viz ukázka 2. Význam jednotlivých sloupců je následující.
Tento přístup k profilování má své výhody i nevýhody. Výhodou je, že zdrojový kód programu zůstává nedotčený a vše za nás udělá překladač. Nevýhoda tohoto přístupu se projeví u aplikací, které jsou složeny z více knihoven, kdy některé části aplikace jsou přeloženy s přepínačem pro generování profilovacích informací a některé ne.
Posledním přístupem, který si zde ukážeme je použití programů valgrind
a KCacheGrind
. Oba programy fungují pouze v operačním systému Linux. Program valgrind
používá pro profilovací analýzu modul callgrind
. Jeho přístup k získávání profilovacích informací je odlišný od předchozích přístupů. Valgrind
funguje jako interpret programu, který sleduje, co program dělá. Praktické použití viz ukázka 3.
Zaznamenávaná data se standardně ukládají do souboru callgrind.out.pid
, kde pid
je číslo procesu. Tímto způsobem lze generovat profilovací údaje pro různé kombinace spouštěcích parametrů a vzájemně je porovnávat. Výsledný soubor lze prohlédnout a analyzovat programem KCacheGrind
, viz obrázek 1.
Základní údaje, které KCacheGrind
zobrazuje v pohledu Flat profile jsou tyto:
Pomocí dalších pohledů lze zobrazit různé další zajímavé informace. Na obrázku je například zobrazen graf volání a mapa volaných funkcí, které zobrazují cenu výpočtu názorněji než tabulka.
Vedlejším efektem získávání profilovacích údajů simulačním způsobem je určité zpomalení vykonávaného programu (cca 5x, profilovací kód generovaný překladačem sice také zpomaluje, ale zřejmě výrazně méně), ale poskytuje to zajímavější informace než výše zmiňované přístupy. Výhodou callgrindu na druhou stranu je, že lze analyzovat prakticky hotové programy, včetně modulů a knihoven třetích stran, na jejichž překlad nemáme nejmenší vliv.
Ideální je vůbec neoptimalizovat. Dnešní překladače umí optimalizovat velice dobře samy. Když už se rozhodnete do optimalizace pustit, měli byste to mít dobře zdůvodněno. Předčasná optimalizace může způsobit více škody než užitku. Když začnete optimalizovat už v průběhu vytváření programu, když ještě nemáte ujasněno, jak bude program nakonec vypadat, může se vzápětí ukázat, že jde o zbytečné úsilí. Navíc přílišné optimalizace na úrovni zdrojového kódu dost často zatemňují podstatu řešeného problému. Zvláště nepříjemné to může být při práci v týmu.
Analýza problému může být užitečnější než optimalizace. Výsledky optimalizace řešení s kvadratickou časovou složitostí budou i po velkém úsilí většinou horší než neoptimalizované řešení se složitostí lineární. Znalosti matematiky a teorie jsou v důsledku lepší než znalosti nejrůznějších exotických konstrukcí programovacího jazyka.
Norma jazyka ISO C99 zavádí možnost použití statických inline funkcí. Použití klíčových slov static inline
před hlavičkou funkce (bližší informace viz dokumentace GCC) nemá žádný vliv na výsledné chování programu, ale překladač potom může takto označené funkce přeložit bez kódu pro volání funkcí. Výsledek je pak podobný, jako když se použije makro, ale s výhodou větší přehlednosti zdrojového kódu.
Tímto způsobem nelze ošetřit všechny funkce. Hodí se to pro velmi jednoduché funkce. Když potom při překladu zapneme optimalizace, dojde ke zrychlení programu, protože se ušetří kód nutný pro zavolání funkce (alokace paměti na zásobníku, skok). Pokud jde o funkci, která je volána opravdu často, může být úspora významná. Nezapomínejte ovšem na předchozí rady.
Označení funkce jako inline je pro překladač jenom doporučení. Překladač se pokusí vytvořit efektivnější kód, až když zapneme generování optimalizovaného kódu pomocí přepínače. I když jsou optimalizace zapnuté, může překladač vyhodnotit, že funkci nelze přeložit efektivněji.