Tak dlouho jsem váhál, zda programovat některé věci tak či onak, až jsem se jednoho krásného večera rozhodl vyřešit to jednou provždy, a změřit, co je rychlejší. Předem upozorňuji, že se nejedná o žádné vědecké testy ze seriálové laboratoře. Jen jednoduchý prográmek, který může zkusit každý – zajímalo mě, jak to dopadne, tak třeba i někoho z vás.
Testování probíhalo na Meridianu/P, v kofiguraci release s připojeným debuggerem. Kód konzolové aplikace vypadal následovně:
using System; using Microsoft.SPOT; namespace MFConsoleApplication1 { public class Program { public static void Main() { DateTime start; TimeSpan cas1, cas2, cas3; start = DateTime.Now; Test1(); cas1 = DateTime.Now - start; start = DateTime.Now; Test1(); cas2 = DateTime.Now - start; start = DateTime.Now; Test1(); cas3 = DateTime.Now - start; Debug.Print("Test1: " + cas1 + "," + cas2 + "," + cas3); start = DateTime.Now; Test2(); cas1 = DateTime.Now - start; start = DateTime.Now; Test2(); cas2 = DateTime.Now - start; start = DateTime.Now; Test2(); cas3 = DateTime.Now - start; Debug.Print("Test2: " + cas1 + "," + cas2 + "," + cas3); } ... } }
Rozdíly, které nás zajímají, jsou velmi malé, řádově mikrosekundy nebo menší. Tak malé intervaly se obtížně měří, takže musíme instrukce mnohokrát opakovat, abychom dostali nějaká směrodatná čísla. Zvolil jsem opakování sto tisíckrát, tak aby výsledky byly zhruba kolem vteřiny. To je dost dlouho na schování režie CLR, přerušení a ostatního "šumu", ale ne zas tolik, abychom při měření usnuli. Pro ověření konzistentnosti výsledků jsem každé měření třikrát zopakoval.
Pokusy jsou seřazeny vzestupně dle velikosti rozdílu.
Toto je vlastně jediný případ, který mě překvapil, ačkoliv rozdíl je opravdu zanedbatelný. Vypadá to, že negace je o něco dražší než xor. Zajímalo by mě, jestli je tedy !b pomalejší než b == false...
Klasická lekce C++. Post-increment by měl být pomalejší, jelikož potřebuje naalokovat a udělat dočasnou kopii hodnoty, pak ji zvýšit a vrátit kopii. Pre-increment jen hodnotu zvýší a vrátí přímo. Zde je třeba připomenout, že kompilace byla optimalizována, a jelikož výsledek operace není nijak využit, v praxi bude rozdíl obvykle větší. Přesto však je patrný i zde, zajímavé.
Procesory, na kterých běží .NET Micro Framework jsou 32bitové, tedy mají i 32bitové registry. Je tedy přirozené, že práce s typy jako long (nebo ulong či double), které jsou 64bitové, stojí něco navíc. Jak je to ale s menšími typy? Stojí něco předávání např. bajtů?
Posun je hardwarově velmi jednoduché implementovat (pomocí klopných obvodů), za to dělení je mnohem náročnější. Není tedy tolik překvapující, že i tady je posun rychlejší.
Získat zbytek po dělení je sice o něco rychlejší než podíl, ale maskování je mnohem více rychlejší než posun. Test posledního bitu má tedy jasného favorita:
Tak tady to máme: O kolik je foreach pomalejší než for? Pro ty, co nevědí, proč tomu tak je, foreach musí nejdřív vytvořit instanci enumerátoru a pak každou iteraci zavolat jeho metodu MoveNext() a přečíst vlastnost Current.
Donedávna jsem se domníval, jak je prohazování proměnných xorem mazané. Pak nám na univerzitě řekli, že existují lidé, kteří si to myslí, ale že je to většinou stejně mnohem pomalejší — měli samozřejmě pravdu. (Stále si však vyhrazuji právo se domnívat, že prohazování ukazatelů na velké objekty xorem je dost mazané.)
Druhá nejčastěji opakovaná věc po záležitosti s foreach je upředňostňování proměnných před vlastnostmi (vlastnosti jsou kompilované do get_ resp. set_ metod), ale neuvědomoval jsem si, že rodíl je tak podstatný. Zhruba 12 μs každé zavolání!
Kromě rozdílů v testovaných případech jsme také získali pojem, kolik času zabírají různé úkony, a zjistili, že každé opakování testu vedlo k rychlejšímu celkovému času. Nicméně, vše zde uvedené je pouze pro vytvoření představy. Přestože by embedded zařízení měla být navrhována s výkonem na paměti, dobře zvažte, zda čitelnost kódu stojí za to, co získáte. Pokud je použitelnost vaší aplikace ovlivněná záchranou několika mikrosekund, pak už je pravděpodobně něco špatně někde jinde.
Jak rychle můžeme měnit výstupní hodnotu na nožičce procesoru? Nebo co je praktičtější, jak krátký impuls dokážeme vyrobit? A co je důležitější, pokude ne dost, zachrání nás porting kit?
Kód na zodpovězení těchto otázek je celkem snadný:
OutputPort port = new OutputPort(pin, false); bool value = false; while (true) { port.Write(value); value ^= true; }
A zde jsou výsledky (release build, bez debuggeru):
Řekněme tedy, že se 100 MHz procesorem vyrobíme řízeným kódem pulsy o šířce zhruba 20 μs (okolo 25 kHz).
Avšak, zde nastupuje fakt, že .NET Micro Framework není systém reálného času: nikdo vám tyto hodnoty nezaručí. Nic nebrání CLR, aby si zrovna mezitím přepnula vlákno, nebo spustila garbage collector. I kdybyste se to pokusili zajistit, stále může nastat nativní přerušení během vykonávání metody Write. Například, uvedená smyčka v konzolové aplikaci bez jakéhokoliv dalšího kódu nevyprodukovala jen ten hezký obrázek, co jsme viděli, ale také tyto (a celkem často):
Dobrá, potřebujeme tedy rychlejší změny, například k implementaci sběrnice 1-Wire®, kde jedničku reprezentuje puls o maximální šířce 15 μs. Co s tím? Zřejmě si na tomto hardwaru v řízeném kódu nevystačíme. Takže buď pořídit rychlejší procesor, anebo zkusit přesunout kritickou část kódu na nativní stranu. Ale jak rychle to funguje tam?
V porting kitu vlastně existuje několik vrstev, které lze pro tento účel použít, a vždy jde o kompromis mezi rychlostí a abstrakcí – nezávislostí na hardware. Na ten úplně nejkratší impuls byste museli zjistit jaký procesor vaše hardwarová platforma používá, sehnat jeho dokumentaci, zjistit, ve kterém registru se nachází stav daného pinu, a pomocí assembleru bit měnit. Jenže tohle je .NET Micro Framework, pojďme to udělat nezávisle na hardwaru:
while (TRUE) { ::CPU_GPIO_SetPinState(portId, TRUE); ::CPU_GPIO_SetPinState(portId, FALSE); }
A výsledek:
2.16 μs (kolem 463 kHz) není úplně zlé, ne? Jen pro zajímavost, jelikož ani tento kousek kódu nezakázal přerušení procesoru, stále nedostaneme 100 % pravidelný signál:
(Zaznamenal jsem pulsy od 2.12 μs do 2.56 μs, což je ještě dobré) — ale na tom není nic špatného, neboť na generování pravidelného signálu je určen PWM, ne tento pochybný postup!