Řekněme, že jste vývojář embedded systémů. Potom víte, že C je tím správným jazykem, i když jeho údržba je někdy přinejmenším nudná. Někdy se vám i zdá, že jste při psaní kódu jako automat, pořád dokola vytváříte základní iterační cykly nad strukturami, které jsou pozoruhodně stejné jako ty z minulého týdne nebo minulého měsíce.
Na druhé straně znáte jistě pochvalné řeči o tom, jaký dobrý C++ je, stejně jako jste asi také slyšeli různé historky o jeho vyšších požadavcích na systémové zdroje, kvůli kterým je nevhodný pro embedded aplikace. A také že kvůli jeho komplikovanosti s ním musí být určitě těžká práce.
Zní to povědomě? Trh pro embedded aplikace je velmi rozsáhlý, týká se celé řady různých oborů. Zatímco se jazyk C těší dobré reputaci jako silný a schopný nástroj pro vývoj embedded aplikací, C++ nemá tak dobrou pověst, i když to je vzhledem k jeho propracovanému prostředí často velmi neoprávněné.
Proč je C++ dobrá alternativa pro C? Jako jazyk se samozřejmě vyvinul z jazyka C. Jednoduchým překompilováním projektu jazyka C pomocí kompilátoru C++ získáte přísnější typovou kontrolu kódu (existují některé neshody v deklaracích, které možná budete muset nejdřív překonat). Když se však soustředíte na základní funkce, nabízí C++ větší abstrakci dat, což je důležitý cíl pro větší a složitější softwarové systémy. Objektová orientace dovádí tuto abstrakci ještě dále, takže můžete nahrazovat globální „pracovní“ funkce funkčností tříd.
Šablony jsou většinou při používání C++ tím nejobávanějším rysem a většinou je to nejčastěji citovaný důvod nebo předpoklad pro zvětšení kódu. Ale kód s dobře navrženou šablonou nabízí elegantní způsob zacházení s různými typy dat. Standardní knihovna C++ může být reklamou pro takový způsob generického programování.
Zacházení s výjimkami je další oblastí, která často vzbuzuje strach, nejistotu a pochybnosti. Výjimky jsou ve spoustě softwarových systémů jazyka C většinou manuální programové úkoly. Tento způsob zacházení s výjimkami lze nahradit, i když to něco stojí, elegantnějším řešením výjimek v jazyce C++. A jako vždy se dostane elegantnějšímu řešení uznání až tehdy, když jsou požadavky na změny vzneseny na skupinu vývojářů. Rychlost, se kterou se dobře propracované objektové návrhy mohou v takových situacích adaptovat, závisí částečně i na lepší abstrakci a skrytí implementačních detailů.
Srovnávat implementace překladače C a C++ není dost dobře možné. Je složité získat vědecké výsledky, když srovnáváte jablka a hrušky. Je komické si číst v diskusních příspěvcích, že účinnost překladače co do velikosti a výkonu přeloženého kódu přináší ztráty od 8 do 30 % v neprospěch C++. Samozřejmostí ale je, že přesně stejný kód používající stejnou knihovnu C bude mít stejný i binární kód.
V průběhu vývoje docházelo nepochybně k velmi špatným zkušenostem s C++, co se týče rychlosti a zejména požadavků na velikost spustitelného kódu a paměťových požadavků při běhu. V raném stádiu práce s C++ vedly tyto stížnosti k zajímavé průmyslové formaci. Před deseti lety vedla spolupráce hlavně japonských poskytovatelů embedded systémů k návrhu striktně omezeného jazykového prostředí pro jazyk C++, které by vyhovovalo těmto požadavkům. Tato koncepce znamenala zakázání některých částí jazyka C++ včetně zpracování výjimek, používání typových informací za běhu, jmenných prostorů, šablon, vícenásobné dědičnosti a virtuálních funkcí.
Oprávnění tohoto kroku stálo na dvou hlavních důvodech. Prvním bylo odstranění jazykových konstrukcí, které způsobovaly obávané „zbytnění“ kódu. Druhým byl cíl zjednodušit jazyk C++ tak, aby ho všichni mohli zvládnout, k čemuž vedlo možná i vědomí toho, že inženýři mají nižší úroveň zkušeností s objektově orientovaným programováním.
Ale i se vší úctou k autorům měl tento přístup k embedded C++ základní nedostatky. Pracovití členové výboru pro jazyk C++ museli asi zblednout, když slyšeli o záměru vyhodit celé kusy jejich dobře napsaného standardního jazyka C++. Jejich kolektivní odpovědí na tento návrh bylo zkoumání skutečné výkonnosti jednotlivých prvků jazyka a jeho nejoblíbenějších implementací (překladač a provozní prostředí).
Výsledkem je obsáhlá zpráva vydaná výborem pro standard jazyka C++ o očekávané výkonnosti jednotlivých hlavních rysů celého jazyka C++. V této zprávě je jedna část věnovaná výkonnosti a využití prostoru (jak statického, tak při běhu) u každého rysu jazyka. Tento článek dále shrnuje výsledky pro ty nejzajímavější rysy.
Jmenný prostor – s používáním jmenného prostoru se nepojí žádná prostorová nebo časová režie. Ten ovlivňuje jedině pravidla hledání jmen v době kompilace. Základní výhodou jmenného prostoru je poskytnutí mechanismu pro oddělování jmen ve velkých projektech tak, aby nedocházelo ke shodě jmen. Kromě toho se díky direktivě using vyhnete zbytečnému ťukání na klávesnici při používání přesných kvalifikací jmenného prostoru, protože přesouvá všechny nekvalifikované identifikátory do aktuálního jmenného prostoru.
Konverze typů – C++ s sebou přináší notaci ve stylu C, ale podporuje bezpečnější a explicitnější konverze typů přes čtyři nové operátory, které platí pro různé situace při konverzi. Pro tři z těchto konverzních operátorů nového stylu (const_ cast, static_cast a reinterpret_cast) nedochází k ovlivnění výkonu. Většinou překladač přetransformuje konverzní notaci do jednoho z těchto nových konverzních operátorů, když generuje objektový kód. Pouze dynamická konverze (dynamic_cast) může vyžadovat nějakou dodatečnou režii, pokud požadované přetypování vyžaduje mechanismus poskytování informací o typu za běhu (RTTI, Run-Time Type Information). Příkladem může být třeba křížová konverze v hierarchii tříd, což uvidíme později.
Reprezentace třídy – základní rys třídy (class) ve srovnání se strukturou (struct) jazyka C a ekvivalenty globálních funkcí trochu překvapivě netrpí žádnou režií ohledně prostoru nebo rychlosti. Je to proto, že nevirtuální funkce a statické datové členy jsou uloženy spolu s definicí třídy, a ne v jednotlivých objektech třídy. Volání členské funkce má dodatečný implicitní argument typu ukazatel, který je nutný pro odkazování se na objekt třídy (* this). Na druhé straně volání samostatných funkcí vyžaduje, aby jim byla operační data předána explicitně. Obvykle se tak děje přes ekvivalentní explicitní argument typu ukazatel.
Virtuální funkce – virtuální funkce znamenají dobře definované náklady, které vycházejí z podkladové operace: indexování do pole ukazatelů na funkce. To je implementační technika běžná v kódu C, ale elegantněji vyjádřená v paradigmatu virtuálních funkcí.
Avšak existují situace, kde má použití virtuálních funkcí za následek „zbytnění“ kódu. Pokud se šablonová třída, která obsahuje virtuální funkce, specializuje na mnoho typů, pak každá z těchto specializací obsahuje duplicitní členské funkce a jim přiřazené podpůrné struktury, včetně tabulky odkazů na virtuální funkce. To má obvykle za následek velkou spoustu objektového kódu, protože současná technologie u sestavovacích/ optimalizačních programů není dostatečně sofistikovaná a nedokáže identifikovat tuto okolnost.
Abychom se vyhnuli problému, můžeme přesunout běžný kód (nezávislý na instanci daného typu) z šablonové třídy do pomocných funkcí mimo šablonu nebo také přesunout funkčnost ze šablonové třídy do základní nešablonové třídy.
Funkce inline – kvůli dosažení efektivnosti výkonu je lepší se voláním funkcí vyhýbat. Klíčové slovo inline jazyka C++ upozorňuje překladač na funkce, které by mohly být vloženy přímo do místa, odkud jsou volány. Překladač nemusí toto upozornění využít. Pokročilé techniky optimalizace dokáží identifikovat a odstranit malá a méně složitá volání funkcí automaticky, aniž by kód explicitně poskytnul upozornění na možnost vložení. Dosavadní zkušenosti však ukazují, že implicitní vkládání neposkytuje nějaké stálé výhody, takže je lepší používat explicitně klíčové slovo inline.
Virtuální základní třídy (VZT) – v nevirtuální dědičnosti se v případě volání členské funkce provádí jednoduché neměnné nastavení. Základní rozdíl u VZT je, že členské funkce musejí provádět vyhledávání za běhu, aby zjistily, která funkce ve stromu dědičnosti se má aktivovat. Z toho vyplývá dodatečná režie asi 15 % ve srovnání s nevirtuálním případem.
Pokud však simulujeme rys virtuálních volání pomocí jiného rysu, také to něco stojí. Alternativní technika implementace třídy rozhraní, která prochází přes jednotlivé základní třídy, však sama vyžaduje nepřímý přístup, z čehož vyplývají odpovídající náklady a režie.
Informace o typu za běhu – RTTI se používá k vyžádání informací o typu objektu a je také součástí infrastruktury dynamického přetypování (dynamic_cast). Pro indikaci nákladů na úkor výkonnosti zvažme, co přináší použití dynamické konverze typu: nalezení tabulky odkazů na virtuální funkce pro daný objekt, nalezení nejvíce odvozeného objektu, kterého je ukazatel this součástí, použití informací o typu tohoto objektu k provedení požadovaného přestavení ukazatele this.
Zpracování výjimek – zpracování výjimek v jazyce C++ vyžaduje informace o typu za běhu programu a částečně se překrývá a přesahuje strukturu RTTI. Nicméně všechny ruční kódovací alternativy musí vzít v úvahu styl kódování, celkové pokrytí procedur pro zpracování výjimek, bezpečnost na úrovni vlákna, režii při běhu systému a režii vyplývající ze zpracování chyb. Když vezmeme v úvahu tyto související náklady, režii při běhu a režii spojenou s údržbou kódu je zpracování výjimek rozumnou alternativou.
Šablony – šablony jsou jedním z nejodsuzovanějších rysů jazyka C++ kvůli jejich nárokům na prostor. Když šablonové třídy a funkce vygenerují novou sadu kódu a dat pro každou svou instanci s různými parametry, kód samozřejmě narůstá. Výkonové testy prováděné nad vícenásobnými instancemi stejné specializace indikují ve srovnání s mnoha různými specializacemi velmi odlišné výsledky. Z toho vyplývá, že překladače před sebou mají ještě dlouhou cestu, než v této oblasti splní optimalizační cíle.
Prodejci knihoven mohou tento problém vyřešit tak, že povolí určité vlastnosti překladačů – a to zejména částečnou specializaci – které povedou k optimalizaci výsledného kódu.
Vývojáři mohou použít techniku, pomocí které se vyhnou vícenásobným specializacím, když nasměrují všechny požadavky na tvorbu instancí na společnou šablonovou třídu. Například použitím společné šablonové třídy pro jedinou specializaci založenou na typu void *.
Vraťme se však k myšlení vývojáře v jazyce C. Často se předpokládá, že vývoj v jazyce C++ je založen na přístupu velmi odlišném od tradičního vývoje v jazyce C. Pokud prozkoumáme dvě běžné techniky jazyka C, dojdeme k odlišnému závěru.
Polymorfismus v C – pro záznam, který potřebuje ukládat data různého typu, může definice vypadat takto:
Pak musí každý kód, který se záznamem pracuje, zkontrolovat, jaký druh dat záznam představuje:
Ale to vypadá velmi podobně jako koncepce polymorfismu. Člen kind odpovídá ukazateli z tabulky virtuálních funkcí a části kódu ve struktuře příkazu switch odpovídají sadě virtuálních funkcí. Možnost polymorfismu je elegantnější a jednodušší na údržbu vzhledem k nárůstu množství takovýchto příkazů switch při používání výše zmiňovaného záznamu. Je tu také výhoda, že data se nezarovnávají podle největšího typu tak, jak je tomu zde.
Nárůst kódu v C – v typickém projektu v jazyce C se velmi často setkáme s iteračním kódem, jako je tento:
Knihovna C++ se zaměřuje na tento typ opakujícího se kódu tím, že umísťuje bohaté iterační schopnosti přímo do svých sad kontejnerových tříd. Nabízí snadněji udržovatelnou, kompaktnější a pravděpodobně i rychlejší implementaci, než by bylo možné dosáhnout s ručně kódovanou alternativou.
Možná vás tento důkaz o schopnostech výkonu nepřiměje k okamžitému převodu svého „embedded“ programování do jazyka C++, ale to není koneckonců až tak jednoduché.
Analýza vám pomůže jazyku C++ lépe porozumět, ale sama o sobě vám nepomůže k vytváření dobrého, spolehlivého a především výkonného kódu C++. Existuje spousta rad týkajících se optimalizace v jazyce C++, včetně přednostního uplatňování inicializace před přiřazováním, vyhýbání se nechtěným konverzím, vyhýbání se vytváření dočasných objektů (při předávání parametrů, návratu z funkcí a ve výrazech) a rozumného používání inline funkcí. Kromě takových optimalizací vycházejících z jazyka existují ještě doporučení o správném používání vynikající knihovny C++.
Jednou z možností usnadnění přechodu na C++ a současného získání správných návyků je využití nástroje CSE (Coding Standard Enforcement). Tento nástroj dává k dispozici užitečnou bezpečnostní síť, která nám umožňuje volně vytvářet kód v C++, ale poskytuje včasné varování v případě podezřelého kódu. Nástroj QA·C++ pro pokročilou analýzu společnosti PRQA například označuje kód, který není přenositelný, je složitý na údržbu, příliš složitý, nevyhovující standardu ISO nebo je napsaný nějakým jiným způsobem, který může být problematický. Prohlížeč zpráv zobrazuje varovná hlášení o zdrojovém kódu v rámci celého projektu. Tato hlášení jsou kategorizována a rozdělena do skupin pro všechny zdrojové soubory. Prohlížeč tyto zprávy propojí s pomocnými doplňkovými informacemi a radami o porušování kódovacích pravidel identifikovaných analyzátorem. Zdrojový kód lze analyzovat na základě procházení jednotlivých souborů nebo v rámci celého projektu, čímž lze nalézt potenciálně nebezpečné použití jazyka, a zabránit tak vzniku chyb v systému. Analyzátor je také schopen lokalizovat skryté chyby v kódu, čímž značně redukuje dobu potřebnou na odstraňování chyb.
Protože však velikost projektů neustále roste, je stále více třeba, aby byly dobře navrženy a dobře spravovány. V tomto kontextu je C++ zcela plnohodnotnou alternativou k jazyku C. Podle vlastních zkušeností ze společnosti PRQA nabízí C++ vysokou návratnost v údržbě a reakcích na změněné a rozšířené požadavky. Ve stále přesnějších požadavcích vestavěného vývoje ukazují výsledky tohoto výzkumu, že s trochou péče a pozornosti a s dodatečnou bezpečnostní sítí nástrojů CSE má C++ co nabídnout.
Poděkování:
Tato prezentace využívá a je částečně založena na zprávě ISO/IEC TR 18015:2006(E) „Technická zpráva o výkonnosti C++” od standardizačního výboru C++ (WG 21).
Je dostupná na adrese: http://www.open- -std.org/jtc1/sc22/wg21/docs/TR18015. pdf.