Doporučené postupy optimalizace
Tento dokument popisuje některé osvědčené optimalizační postupy v aplikaci Visual C++.Obsahuje následující témata:
Možnosti kompilátoru a linkeru
Optimalizace na základě profilu
Jaká úroveň optimalizace by měla být použita?
Přepínače čísel s plovoucí desetinnou čárkou
Optimalizační direktivy declspec
Optimalizační direktivy pragma
Klíčová slova __restrict a __assume
Podpora vnitřních volání
Výjimky
Možnosti kompilátoru a linkeru
Optimalizace na základě profilu
Aplikace Visual C++ podporuje optimalizaci na základě profilu (PGO).Tato optimalizace používá profilová data z dřívějších spuštění instrumentované verze aplikace k řízení pozdějších optimalizací aplikace.Použití optimalizace PGO může být časově náročné a nejde tedy o techniku používanou všemi vývojáři, doporučujeme ji však použít pro konečné sestavení produktu.Další informace naleznete v tématu Optimalizace na základě profilu.
Kromě toho byla vylepšena celková optimalizace programu (známá také jako generování kódu při propojování) a optimalizace /O1 a /O2.Obecně bude aplikace zkompilovaná s jednou z těchto možností rychlejší než stejná aplikace zkompilovaná starším kompilátorem.
Další informace naleznete v tématu /GL (celková optimalizace programu) a /O1, /O2 (minimální velikost, maximální rychlost).
Jaká úroveň optimalizace by měla být použita?
Je-li to možné, měla by konečná sestavení být kompilována s optimalizacemi na základě profilu.Není-li sestavení s optimalizacemi PGO možné, ať již kvůli nedostatečné infrastruktuře pro spouštění instrumentovaných sestavení nebo absenci přístupu ke scénářům, doporučuje sestavovat s celkovou optimalizací programu.
Přepínač /Gy je také velmi užitečný.Pro každou funkci generuje samostatnou sekvenci COMDAT, což linkeru umožňuje větší flexibilitu při skládání sekvencí COMDAT nebo jejich odstraňování, nejsou-li odkazovány.Jedinou nevýhodou použití přepínače /Gy je, že může mírně ovlivnit rychlost sestavování.Proto je jeho použití obecně doporučeno.Další informace naleznete v tématu /Gy (povolení propojení na úrovni funkcí).
Pro sestavování v 64bitových prostředích je doporučeno používat možnost linkeru /OPT:REF,ICF, pro 32bitová prostředí je doporučena možnost /OPT:REF.Další informace naleznete v tématu /OPT (optimalizace).
Je také velmi doporučeno generovat symboly ladění, a to i v optimalizovaných sestaveních pro vydání.Symboly nemají vliv na generovaný kód a v případě potřeby je pak mnohem snazší aplikaci ladit.
Přepínače čísel s plovoucí desetinnou čárkou
Možnost kompilátoru /Op byla odstraněna, byly však přidány následující čtyři možnosti kompilátoru zabývající se optimalizacemi čísel s plovoucí desetinnou čárkou:
/fp:precise |
Tento přepínač je výchozím doporučením a měl by být použit ve většině případů. |
/fp:fast |
Doporučeno, je-li nejvyšší důraz kladen na výkon, například ve hrách.Výsledkem bude nejvyšší výkon. |
/fp:strict |
Doporučeno, jsou-li požadováno přesné výjimky čísel s plovoucí desetinnou čárkou a chování dle standardu IEEE.Výsledkem bude nejnižší výkon. |
/fp:except[-] |
Lze použít ve spojení s možnostmi /fp:strict nebo /fp:precise, nikoli však s možností /fp:fast. |
Další informace naleznete v tématu /fp (zadání chování hodnot s plovoucí desetinnou čárkou).
Optimalizační direktivy declspec
V této části jsou popsány dvě direktivy declspec, které lze v programech použít pro zvýšení výkonu: __declspec(restrict) a __declspec(noalias).
Direktivu declspec restrict lze použít pouze pro deklarace funkcí, které vrací ukazatel, například __declspec(restrict) void *malloc(size_t size);
Direktiva declspec restrict se používá s funkcemi vracejícími ukazatele bez aliasů.Toto klíčové slovo se používá v implementaci funkce malloc knihovny C-Runtime, protože tato funkce nikdy nevrátí ukazatel, který je již v aktuálním programu používán (nedojde-li k nepovolené akci, například k použití paměti po jejím uvolnění).
Direktiva restrict poskytuje kompilátoru více informací o provádění optimalizací.Jedním z nejobtížnějších úkolů kompilátoru je určit, které ukazatele poskytují alias jiným ukazatelům, proto využití těchto informací kompilátoru značně pomůže.
Je vhodné poukázat na skutečnost, že jde o příslib kompilátoru, nikoli něco, co kompilátor bude ověřovat.Používá-li program direktivu declspec restrict nevhodně, může dojít k nesprávnému chování.
Další informace naleznete v tématu restrict.
Direktiva declspec noalias je používána také pouze u funkcí a označuje, že funkce je částečně čistá.Částečně čistá funkce je funkce, která odkazuje nebo upravuje pouze místní proměnné, argumenty a dereference argumentů první úrovně.Tato direktiva declspec je příslibem kompilátoru a pokud funkce odkazuje na globální proměnné nebo dereference argumentů ukazatelů druhé úrovně, může kompilátor vygenerovat kód, který aplikaci poškodí.
Další informace naleznete v tématu noalias.
Optimalizační direktivy pragma
Optimalizaci kódu může napomoci také několik užitečných direktiv pragma.Jako první bude popsána direktiva #pragma optimize:
#pragma optimize("{opt-list}", on | off)
Tato direktiva pragma umožňuje nastavit danou úroveň optimalizace na úrovni funkcí.Tento přístup je ideální ve výjimečných případech, kdy v aplikaci dojde k chybě, je-li daná funkce kompilována s optimalizací.Následující sekvencí příkazů lze vypnout optimalizace pro jedinou funkci:
#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)
Další informace naleznete v tématu optimize.
Vkládání je jednou z nejdůležitějších optimalizací, které kompilátor provádí, a zde je diskutováno několik direktiv pragma, které pomáhají upravit toto chování.
Direktiva #pragma inline_recursion slouží k určení, zda má být aplikace schopna vložit rekurzivní volání.Ve výchozím nastavení je tato možnost vypnuta.Lze ji zapnout pro mělké rekurze malých funkcí.Další informace naleznete v tématu inline_recursion.
Další užitečnou direktivou pragma pro omezení hloubky vkládání je #pragma inline_depth.Ta je obvykle užitečná v situacích, kde je žádoucí omezit velikost programu nebo funkce.Další informace naleznete v tématu inline_depth.
Klíčová slova __restrict a __assume
V jazyce Visual C++ existuje dvojice klíčových slov, které mohou zvýšit výkon: __restrict a __assume.
Nejprve je zapotřebí poznamenat, že direktivy __restrict a __declspec(restrict) jsou dvě různé věci.Ačkoli spolu určitým způsobem souvisejí, jejich sémantika se liší.Klíčové slovo __restrict je kvalifikátor typu obdobně jako klíčové slovo const nebo volatile, ale výhradně pro typy ukazatelů.
Ukazatel upravený klíčovým slovem __restrict je označován jako ukazatel __restrict.Ukazatel __restrict je ukazatel, k němuž lze přistoupit pouze pomocí ukazatele __restrict.Jinými slovy, jiný ukazatel nelze použít k přístupu k datům, na která ukazuje ukazatel __restrict.
Klíčové slovo __restrict může být výkonným nástrojem pro optimalizátor jazyka Visual C++, používejte jej však velmi opatrně.Při nesprávném použití může optimalizátor provést optimalizaci, která aplikaci poškodí.
Klíčové slovo __restrict nahrazuje přepínač /Oa z předchozích verzí.
Pomocí klíčového slova __assume, může vývojář přikázat kompilátoru, aby předpokládal hodnotu nějaké proměnné.
Například direktiva __assume(a < 5); oznamuje optimalizátoru, že na daném řádku kódu má proměnná a hodnotu menší než 5.Opět se jedná o příslib kompilátoru.Má-li proměnná a na tomto místě v programu ve skutečnosti hodnotu 6, může se chování programu po provedení optimalizací kompilátorem lišit od očekávání.Klíčové slovo __assume je nejužitečnější před příkazy switch nebo před podmíněnými výrazy.
Pro klíčové slovo __assume existují určitá omezení.Prvním je, že stejně jako v případě klíčového slova __restrict jde pouze o návrh, kompilátor jej tedy může ignorovat.Klíčové slovo __assume také aktuálně funguje pouze s nerovnostmi proměnných vůči konstantám.Symbolické nerovnosti, například assume(a < b), nejsou šířeny.
Podpora vnitřních volání
Vnitřní volání jsou volání funkcí, o kterých má kompilátor vnitřní znalosti a namísto zavolání funkce v knihovně vygeneruje kód dané funkce.Soubor hlaviček intrin.h umístěný na adrese <Instalační_složka>\VC\include\intrin.h obsahuje všechna dostupná vnitřní volání pro každou ze tří podporovaných platforem (x86, x64 a ARM).
Vnitřní volání umožňují programátorovi přejít hluboko do kódu bez nutnosti použít sestavení.Použití vnitřních volání má několik výhod:
Kód je přenosnější.Několik vnitřních volání je dostupných pro více architektur procesoru.
Kód je čitelnější, protože je stále psán v jazyce C/C++.
Kód využívá výhod optimalizací kompilátoru.Vylepšováním kompilátoru se zlepšuje také generování kódu pro vnitřní volání.
Další informace naleznete v tématu Vnitřní funkce kompilátoru a Výhody použití vnitřní objekty serveru.
Výjimky
Při použití výjimek je ovlivněn výkon.Použitím bloků try jsou zavedena některá omezení, která kompilátoru znemožňují provést určité optimalizace.Na platformách x86 dochází při použití bloků try k dalšímu poklesu výkonu kvůli dodatečným informacím o stavech, které musí být při spouštění kódu generovány.Na 64bitových platformách není snížení výkonu bloky try tolik výrazné, ale jakmile dojde k vyvolání výjimky, může být proces nalezení obslužné rutiny a rozbalování zásobníku časově náročný.
Proto je doporučeno se zavedení bloků try/catch do kódu vyhnout, pokud nejsou doopravdy zapotřebí.Pokud výjimky použity být musí, používejte, je-li to možné, synchronní výjimky.Další informace naleznete v tématu Strukturované zpracování výjimek (C/C++).
Nakonec, vyvolávejte výjimky pouze v mimořádných případech.Použití výjimek pro obecný tok řízení pravděpodobně značně sníží výkon.