Psaní rychlejšího spravovaného kódu: Znalost nákladů
Jan Gray
Microsoft CLR Performance Team
Červen 2003
Platí pro:
Microsoft® .NET Framework
Souhrn: Tento článek představuje nízkoúrovňový model pro dobu provádění spravovaného kódu na základě měřených časů operací, aby vývojáři mohli lépe informovat rozhodnutí o kódování a psát rychlejší kód. (30 tištěných stránek)
Stáhněte profileru CLR . (330 kB)
Obsah
Úvod (a závazek)
Směrem k modelu nákladů pro spravovaný kód
Co jsou náklady ve spravovaném kódu
Závěr
Prostředky
Úvod (a závazek)
Existují různé způsoby implementace výpočtu a některé jsou mnohem lepší než jiné: jednodušší, čistější a snadněji udržovatelné. Některé způsoby jsou velmi rychlé a některé jsou úžasně pomalé.
Nepoužívejte pomalý a tlustý kód na světě. Nepohrdáš takový kód? Kód, který se spouští ve vhodných a spuštěných počítačích? Kód, který zamkne uživatelské rozhraní na několik sekund v čase? Kód, který připíná procesor nebo thrashuje disk?
Nedělejte to. Místo toho se postavte a slibujte spolu se mnou:
"Slibuji, že nedoručím pomalý kód. Rychlost je funkce, o kterou se starám. Každý den budu věnovat pozornost výkonu kódu. Pravidelně a metodicky měřím jeho rychlost a velikost. Naučím se, sestavím nebo koupím nástroje, které potřebuji k tomu. Je to moje zodpovědnost."
(Opravdu.) Takže jsi slíbila? Dobře pro tebe.
Jak tedy napíšete nejrychlejší a nejtěsnější den kódu? Je to věc vědomého výběru frugantního způsobu přednost extravagantní, nafoukaný způsob, znovu a znovu, a záležitost promyslet důsledky. Každá daná stránka kódu zachycuje desítky takových malých rozhodnutí.
Pokud ale nevíte, jaké jsou náklady, nemůžete dělat inteligentní volby: nemůžete napsat efektivní kód, pokud nevíte, jaké věci stojí.
Bylo to jednodušší v dobrých starých dnech. Dobře programátori C věděli. Každý operátor a operace v jazyce C, ať už se jedná o přiřazení, celočíselné číslo nebo matematiku s plovoucí desetinou čárkou, dereference nebo volání funkce, namapované více nebo méně 1:1 na jednu primitivní operaci počítače. Pravda, někdy bylo potřeba několik strojových instrukcí umístit správné operandy do správných registrů, a někdy by jedna instrukce mohla zachytit několik operací jazyka C (slavný *dest++ = *src++;
), ale obvykle byste mohli napsat (nebo číst) řádek kódu C a vědět, kde čas probíhal. Pro kód i data byl kompilátor jazyka C WYWIWYG – "to, co píšete, je to, co získáte". (Výjimka byla a je volání funkce. Pokud nevíte, jaké jsou náklady na funkci, nevíte, co se stalo.)
V roce 1990 si můžete vychutnat mnoho softwarových inženýrů a produktivních výhod abstrakce dat, objektově orientovaného programování a opětovného použití kódu, softwarový průmysl pc udělal přechod z jazyka C na C++.
C++ je nadmnožina jazyka C a je "průběžné platby" – nové funkce stojí nic, pokud je nepoužíváte – takže znalosti programování jazyka C, včetně interního nákladového modelu, je přímo použitelné. Pokud potřebujete nějaký funkční kód jazyka C a znovu ho zkompilujete pro jazyk C++, neměl by se příliš měnit čas provádění a režijní náklady na místo.
Na druhou stranu jazyk C++ zavádí mnoho nových jazykových funkcí, včetně konstruktorů, destruktorů, nového, odstranění, jednoho, více a virtuální dědičnosti, přetypování, členských funkcí, virtuálních funkcí, přetížených operátorů, ukazatelů na členy, pole objektů, zpracování výjimek a složení stejných, což způsobuje ne triviální skryté náklady. Například virtuální funkce stojí dvě další nepřímé operace na volání a do každé instance přidají skryté pole ukazatele vtable. Nebo vezměte v úvahu, že tento neškodný kód:
{ complex a, b, c, d; … a = b + c * d; }
zkompiluje do přibližně třinácti implicitních volání členské funkce (snad inlined).
Před devíti lety jsme prozkoumali toto téma v mém článku C++: Under the Hood. Napsal(a) jsem:
"Je důležité pochopit, jak se implementuje váš programovací jazyk. Takové znalosti rozptýlí strach a zázrak "Co na zemi je kompilátor dělá zde?"; poskytuje důvěru při používání nových funkcí; poskytuje přehled o ladění a učení dalších jazykových funkcí. Poskytuje také pocit relativních nákladů na různé volby kódování, které je nezbytné k napsání nejúčinnějšího kódu den denně."
Teď se podíváme na spravovaný kód podobně. Tento článek se zabývá nízké úrovně čas a náklady na prostor spravovaného provádění, takže můžeme v dnešní době chytřejší kompromisy.
A mějte naše sliby.
Proč spravovaný kód?
Pro velkou většinu vývojářů nativního kódu je spravovaný kód lepší a produktivnější platformou pro spouštění softwaru. Odebere celé kategorie chyb, jako jsou poškození haldy a chyby typu index-mimo vazbu, které tak často vedou k frustrování relací ladění v pozdní noci. Podporuje moderní požadavky, jako je bezpečný mobilní kód (prostřednictvím zabezpečení přístupu kódu) a webové služby XML, a ve srovnání se stárnoucí verzí Win32/COM/ATL/MFC/VB je .NET Framework aktualizovaným čistým slate designem, kde se můžete více pracovat s menším úsilím.
Spravovaný kód pro vaši komunitu uživatelů umožňuje bohatší a robustnější aplikace – lepší život prostřednictvím lepšího softwaru.
Jaký je tajný kód pro rychlejší psaní spravovaného kódu?
Jen proto, že můžete více udělat s menším úsilím, není licence na abdicaci vaší odpovědnosti za kód moudře. Nejdřív musíte přiznat, že jsem nováček. Jsi nováček. Jsem taky nováček. Všichni jsme babes ve spravované kódové zemi. Pořád se učíme lana – včetně toho, co stojí.
Pokud jde o bohaté a pohodlné rozhraní .NET Framework, je to jako bychom byli děti v obchodě s klídky. "Wow, nemusím dělat všechno tak zdlouhavé strncpy
věci, můžu jen "+" řetězce dohromady! Wow, můžu načíst megabajt XML v několika řádcích kódu! Whoo-hoo!"
Je to všechno tak jednoduché. Tak snadné, skutečně. Takže snadné vypalovat megabajty paměti RAM parsující XML infosets jen tak, aby z nich vytáhlo několik prvků. V C nebo C++ to bylo tak bolestné, že byste si dvakrát mysleli, možná byste vytvořili stavový počítač na nějakém rozhraní API podobné SAX. S rozhraním .NET Framework stačí načíst celou informační sadu do jedné sady. Možná to dokonce děláš přes a konec. Možná se vaše aplikace už tak rychle nezdálo. Možná má pracovní sadu mnoha megabajtů. Možná byste si měli dvakrát promyslet, jaké ty jednoduché metody stojí...
Podle mého názoru bohužel aktuální dokumentace k rozhraní .NET Framework dostatečně nevypisuje důsledky pro výkon typů a metod rozhraní – ani neurčuje, které metody by mohly vytvářet nové objekty. Modelování výkonu není snadné pokrýt ani dokument; ale i tak , "nevědí" je pro nás mnohem obtížnější činit informovaná rozhodnutí.
Protože jsme tady všichni nováčci a protože nevíme, jaké jsou náklady, a protože náklady nejsou jasně zdokumentované, co máme dělat?
Změřte ho. Tajemstvím je měřit a být ostražití. Všichni se budeme muset dostat do zvyku měřit náklady na věci. Pokud se podíváme na potíže s měřením nákladů, pak nebudeme neúmyslně volat novou metodu, která stojí desetkrát za to, co jsme předpokládat, náklady.
(Mimochodem, abyste získali hlubší přehled o výkonu podporujících základy seznamu BCL (základní knihovny tříd) nebo samotného CLR, zvažte pohled na rozhraní příkazového řádku sdíleného zdroje, a.k.a. Rotor. Rotorový kód sdílí krev s rozhraním .NET Framework a CLR. Není to stejný kód v celém, ale i tak, slibuji vám, že promyšlená studie Rotor vám poskytne nové poznatky o dějích pod kapotou CLR. Ale nezapomeňte nejprve zkontrolovat licenci SSCLI!)
Znalosti
Pokud se snažíte být řidičem taxíku v Londýně, musíte nejprve získat Znalostní. Studenti studují mnoho měsíců, aby si zapamatovali tisíce malých ulic v Londýně a naučili se nejlepší trasy z místa na místo. A každý den jdou na skútrech, aby se skautovali a posílili své učení knihy.
Podobně pokud chcete být vysoce výkonným vývojářem spravovaného kódu, musíte získat znalostníspravovaného kódu . Musíte zjistit, jaké jsou náklady na provoz na nízké úrovni. Musíte zjistit, jaké funkce, jako jsou delegáti a náklady na zabezpečení přístupu kódu. Musíte se naučit náklady na typy a metody, které používáte, a ty, které píšete. A neuškodí vám zjistit, které metody můžou být pro vaši aplikaci příliš nákladné– a vyhněte se jim.
Znalost není v žádné knize, bohužel. Musíte se dostat na svého skútru a prozkoumat – to znamená klika nahoru csc, ildasm, VS.NET ladicí program, CLR Profiler, váš profiler, některé časovače výkonu atd. a zjistit, jaké náklady na kód stojí v čase a prostoru.
Směrem k modelu nákladů pro spravovaný kód
Pojďme se podívat na nákladový model spravovaného kódu. Díky tomu se budete moct podívat na listovou metodu a rychle zjistit, které výrazy a příkazy jsou nákladnější; a budete moct při psaní nového kódu dělat chytřejší volby.
(Tím se nebudou zabývat přechodné náklady na volání metod nebo metod rozhraní .NET Framework. To bude muset počkat na další článek v jiný den.)
Dříve jsem uvedl, že většina nákladového modelu jazyka C stále platí ve scénářích C++. podobně velká část nákladového modelu C/C++ se stále vztahuje na spravovaný kód.
Jak to může být? Znáte model provádění CLR. Svůj kód napíšete v jednom z několika jazyků. Zkompilujete ho do formátu CIL (Common Intermediate Language), který je zabalený do sestavení. Spustíte hlavní sestavení aplikace a spustí se spuštění souboru CIL. Ale není to řádově pomalejší, jako jsou interprety bajtového kódu starých?
Kompilátor za běhu
Ne, to není. CLR používá kompilátor JIT (just-in-time) ke kompilaci každé metody v CIL do nativního kódu x86 a pak spustí nativní kód. I když kompilace JIT každé metody při prvním vyvolání je malá prodleva, každá metoda volá čistý nativní kód bez interpretační režie.
Na rozdíl od tradičního procesu kompilace C++ mimo řádek je čas strávený v kompilátoru JIT zpožděním "hodinových hodin" v tváři každého uživatele, takže kompilátor JIT nemá luxusní možnost úplné optimalizace projde. I tak je seznam optimalizací, které kompilátor JIT provádí, působivé:
- Skládací konstanta
- Šíření konstanty a kopírování
- Běžná eliminace dílčího výrazu
- Pohyb kódu invariantů smyčky
- Odstranění mrtvého úložiště a vyřazení mrtvého kódu
- Registrace přidělení
- Inlining – metoda
- Zrušení registrace smyčky (malé smyčky s malými těly)
Výsledek je srovnatelný s tradičním nativním kódem –alespoň ve stejném ballparku.
Pokud jde o data, použijete kombinaci hodnotových typů nebo referenčních typů. Typy hodnot, včetně integrálních typů, typů s plovoucí desetinou čárkou, výčtů a struktur, obvykle žijí v zásobníku. Jsou stejně malé a rychlé jako místní hodnoty a struktury v jazyce C/C++. Stejně jako u jazyka C/C++ byste se pravděpodobně měli vyhnout předávání velkých struktur jako argumentů metody nebo návratových hodnot, protože režie kopírování může být zakázána.
Odkazové typy a typy hodnot v rámečku jsou aktivní v haldě. Řeší je odkazy na objekty, což jsou jednoduše strojové ukazatele stejně jako ukazatele objektů v jazyce C/C++.
Takže spravovaný kód s řaděnou sadou může být rychlý. S několika výjimkami, které si probereme níže, pokud máte pocit, že náklady na určitý výraz v nativním kódu jazyka C jsou, nebudete daleko špatně modelovat jeho náklady jako ekvivalent ve spravovaném kódu.
Měl bych také zmínit NGEN, nástroj, který "předem"kompiluje CIL do nativních sestavení kódu. Zatímco NGEN sestavení v současné době nemá významný dopad (dobrý nebo špatný) na dobu provádění, může snížit celkovou pracovní sadu pro sdílená sestavení, která jsou načtena do mnoha appdomains a procesů. (Operační systém může sdílet jednu kopii kódu NGEN napříč všemi klienty; zatímco jitted kód se obvykle nesdílí napříč doménami nebo procesy AppDomains. Ale viz také LoaderOptimizationAttribute.MultiDomain
.)
Automatická správa paměti
Nejvýznamnějším odchodem spravovaného kódu (z nativního) je automatická správa paměti. Přidělíte nové objekty, ale uvolňování paměti CLR (GC) je automaticky uvolní za vás, když jsou nedostupné. GC běží teď a znovu, často nepozorovaně, obecně zastavuje vaši aplikaci jen pro milisekundu nebo dvě – občas déle.
Několik dalších článků popisuje dopad uvolňování paměti na výkon a my je zde nebudeme recapitulatovat. Pokud vaše aplikace dodržuje doporučení v těchto dalších článcích, celkové náklady na uvolňování paměti mohou být zanedbatelné, několik procent doby provádění, konkurenceschopné s tradičním objektem C++ new
a delete
. Amortizované náklady na vytvoření a pozdější automatické uvolnění objektu jsou dostatečně nízké, takže můžete vytvořit mnoho desítek milionů malých objektů za sekundu.
Přidělení objektu však stále není volné. Objekty zabírají místo. Přidělování objektů rampant vede k častějším cyklům uvolňování paměti.
Mnohem horší je zbytečně uchovávat odkazy na nepoužité objektové grafy. Někdy vidíme skromné programy s lamentovatelnými pracovními sadami o velikosti 100 MB, jejichž autoři zamítají své vychytávky a místo toho přiřazují nízký výkon některým záhadným, neidentifikovaným (a proto neidentifikovaným) problémům se spravovaným kódem samotným. Je to smutné. Pak ale hodinová studie s profilerem CLR a změny na několika řádcích kódu zkracují využití haldy faktorem deseti nebo více. Pokud máte problém s velkou pracovní sadou, prvním krokem je podívat se na zrcadlo.
Proto zbytečně nevytvářejte objekty. Jen proto, že automatická správa paměti rozptýlí mnoho složitostí, potíží a chyb přidělení a uvolnění objektů, protože je tak rychlá a tak pohodlná, přirozeně vytváříme více a více objektů, jako by se rozrůstaly na stromy. Pokud chcete napsat opravdu rychlý spravovaný kód, vytvořte objekty promyšleně a odpovídajícím způsobem.
To platí také pro návrh rozhraní API. Typ a jeho metody je možné navrhnout tak, aby vyžadovat, aby klienti vytvářet nové objekty s divokým opuštěním. Nedělejte to.
Co jsou náklady ve spravovaném kódu
Teď se podívejme na časové náklady na různé operace spravovaného kódu nízké úrovně.
Tabulka 1 představuje přibližné náklady na různé operace spravovaného kódu na nízké úrovni v nanosekundách v klidovém stavu 1,1 GHz Pentium-III počítače se systémem Windows XP a .NET Framework v1.1 ("Everett"), který se shromažďuje se sadou jednoduchých časovacích smyček.
Testovací ovladač volá každou testovací metodu a určuje počet iterací, které se mají provést, automaticky se škálují na iteraci mezi 218 a 230 iterací, podle potřeby provést každý test alespoň na 50 ms. Obecně řečeno, to je dostatečně dlouho sledovat několik cyklů uvolňování paměti 0 generace v testu, který provádí intenzivní přidělení objektů. V tabulce jsou výsledky v průměru nad 10 pokusů a nejlepší (minimální čas) zkušební verze pro každý předmět testu.
Každá testovací smyčka se rozbalí 4 až 64krát, aby se snížila režie testovací smyčky. Prošel jsem nativní kód vygenerovaný pro každý test, aby se zajistilo, že kompilátor JIT ne optimalizaci testu nebyl pryč – například v několika případech jsem upravil test tak, aby se průběžné výsledky zachovaly během a po testovací smyčce. Podobně jsem udělal změny, aby se zabránilo běžné subexpression odstranění v několika testech.
tabulky 1 Primitive Times (průměr a minimum) (ns)
Průměr | Min | Primitivní | Průměr | Min | Primitivní | Průměr | Min | Primitivní |
---|---|---|---|---|---|---|---|---|
0.0 | 0.0 | Řízení | 2.6 | 2.6 | new valtype L1 | 0.8 | 0.8 | isinst up 1 |
1.0 | 1.0 | Přidat int | 4.6 | 4.6 | new valtype L2 | 0.8 | 0.8 | isinst down 0 |
1.0 | 1.0 | Int sub | 6.4 | 6.4 | new valtype L3 | 6.3 | 6.3 | isinst down 1 |
2.7 | 2.7 | Int mul | 8.0 | 8.0 | new valtype L4 | 10.7 | 10.6 | isinst (up 2) down 1 |
35.9 | 35.7 | Int div | 23.0 | 22.9 | new valtype L5 | 6.4 | 6.4 | isinst down 2 |
2.1 | 2.1 | Směna int | 22.0 | 20.3 | nový reftype L1 | 6.1 | 6.1 | isinst down 3 |
2.1 | 2.1 | long add | 26.1 | 23.9 | nový reftype L2 | 1.0 | 1.0 | získat pole |
2.1 | 2.1 | long sub | 30.2 | 27.5 | nový reftype L3 | 1.2 | 1.2 | získat prop |
34.2 | 34.1 | dlouhý mul | 34.1 | 30.8 | nový reftype L4 | 1.2 | 1.2 | nastavit pole |
50.1 | 50.0 | long div | 39.1 | 34.4 | nový reftype L5 | 1.2 | 1.2 | set prop |
5.1 | 5.1 | dlouhá směna | 22.3 | 20.3 | Nový reftype prázdný ctor L1 | 0.9 | 0.9 | získat toto pole |
1.3 | 1.3 | float add | 26.5 | 23.9 | new reftype empty ctor L2 | 0.9 | 0.9 | získat tuto prop |
1.4 | 1.4 | float sub | 38.1 | 34.7 | Nový reftype prázdný ctor L3 | 1.2 | 1.2 | nastavit toto pole |
2.0 | 2.0 | plovák mul | 34.7 | 30.7 | nový reftype prázdný ctor L4 | 1.2 | 1.2 | nastavit tuto prop |
27.7 | 27.6 | float div | 38.5 | 34.3 | new reftype empty ctor L5 | 6.4 | 6.3 | získání virtuální prop |
1.5 | 1.5 | double add | 22.9 | 20.7 | nový reftype ctor L1 | 6.4 | 6.3 | nastavení virtuální prop |
1.5 | 1.5 | double sub | 27.8 | 25.4 | new reftype ctor L2 | 6.4 | 6.4 | bariéra zápisu |
2.1 | 2.0 | dvojitý mul | 32.7 | 29.9 | nový reftype ctor L3 | 1.9 | 1.9 | load int array elem |
27.7 | 27.6 | double div | 37.7 | 34.1 | nový reftype ctor L4 | 1.9 | 1.9 | store int array elem |
0.2 | 0.2 | vložené statické volání | 43.2 | 39.1 | new reftype ctor L5 | 2.5 | 2.5 | load obj array elem |
6.1 | 6.1 | statické volání | 28.6 | 26.7 | new reftype ctor no-inl L1 | 16.0 | 16.0 | store obj array elem |
1.1 | 1.0 | Volání vložené instance | 38.9 | 36.5 | nový reftype ctor no-inl L2 | 29.0 | 21.6 | box int |
6.8 | 6.8 | volání instance | 50.6 | 47.7 | new reftype ctor no-inl L3 | 3.0 | 3.0 | rozbalení doručené pošty |
0.2 | 0.2 | Inlined this inst call | 61.8 | 58.2 | new reftype ctor no-inl L4 | 41.1 | 40.9 | vyvolání delegáta |
6.2 | 6.2 | toto volání instance | 72.6 | 68.5 | new reftype ctor no-inl L5 | 2.7 | 2.7 | sum array 1000 |
5.4 | 5.4 | virtuální hovor | 0.4 | 0.4 | přetypování 1 | 2.8 | 2.8 | sum array 10000 |
5.4 | 5.4 | toto virtuální volání | 0.3 | 0.3 | přetypovat 0 | 2.9 | 2.8 | sum array 100000 |
6.6 | 6.5 | volání rozhraní | 8.9 | 8.8 | přetypovat 1 | 5.6 | 5.6 | sum array 1000000 |
1.1 | 1.0 | inst itf instance call | 9.8 | 9.7 | přetypování (nahoru 2) dolů 1 | 3.5 | 3.5 | sum list 1000 |
0.2 | 0.2 | toto volání instance itf | 8.9 | 8.8 | přetypovat 2 | 6.1 | 6.1 | sum list 10000 |
5.4 | 5.4 | inst itf virtual call | 8.7 | 8.6 | přetypovat 3 | 22.0 | 22.0 | sum list 1000000 |
5.4 | 5.4 | toto virtuální volání itf | 21.5 | 21.4 | sum list 10000000 |
Právní omezení: Nepřebíjejte tato data příliš doslova. Testování času je plné náporu neočekávaných efektů druhého pořadí. Náhodná náhoda může umístit zasunutý kód nebo nějaká důležitá data, aby přesahovala řádky mezipaměti, zasahovala do něčeho jiného nebo co máte. Je to trochu jako princip nejistoty: časy a časové rozdíly 1 nanosekundy nebo tak jsou na mezích pozorovatelnosti.
Další právní omezení: Tato data jsou relevantní pouze pro malé scénáře kódu a dat, které jsou zcela v mezipaměti. Pokud se "horké" části vaší aplikace nevejdou do mezipaměti čipů, možná budete mít jinou sadu problémů s výkonem. O mezipamětí blízko konce papíru máme mnohem více informací.
A další právní omezení: jedna z dílčích výhod dodávání komponent a aplikací jako sestavení CIL je to, že váš program může automaticky získat rychlejší každou sekundu a rychleji každý rok – "rychlejší každou sekundu", protože modul runtime může (teoreticky) znovu naladit kompilovaný kód JIT při spuštění programu; a "rychlejší vždy rok", protože s každou novou verzí modulu runtime, lépe, inteligentnějšími a rychlejšími algoritmy mohou při optimalizaci kódu vzít novou tabulku. Takže pokud se několik z těchto časování zdá být méně než optimální v .NET 1.1, vezměte si srdce, že by se měly zlepšit v následných verzích produktu. Následuje, že jakákoli daná sekvence nativního kódu hlášená v tomto článku se může v budoucích verzích rozhraní .NET Framework změnit.
Zřeknutí se, že data poskytují rozumné chování při současném výkonu různých primitiv. Čísla mají smysl a podstavují můj kontrolní výraz, že většina řazených spravovaných kódů běží "blízko počítače" stejně jako zkompilovaný nativní kód. Primitivní celočíselné a plovoucí operace jsou rychlé, metody volání různých druhů méně tak, ale (věřte mi) stále srovnatelné s nativníM jazykem C/C++; a přesto vidíme, že některé operace, které jsou obvykle levné v nativním kódu (přetypování, pole a úložiště polí, ukazatele funkcí (delegáti)), jsou nyní dražší. Proč? Pojďme se podívat.
Aritmetické operace
tabulky 2 aritmetické operace (ns)
Průměr | Min | Primitivní | Průměr | Min | Primitivní |
---|---|---|---|---|---|
1.0 | 1.0 | int add | 1.3 | 1.3 | float add |
1.0 | 1.0 | int sub | 1.4 | 1.4 | float sub |
2.7 | 2.7 | int mul | 2.0 | 2.0 | plovák mul |
35.9 | 35.7 | int div | 27.7 | 27.6 | float div |
2.1 | 2.1 | int shift | |||
2.1 | 2.1 | long add | 1.5 | 1.5 | double add |
2.1 | 2.1 | long sub | 1.5 | 1.5 | double sub |
34.2 | 34.1 | dlouhý mul | 2.1 | 2.0 | dvojitý mul |
50.1 | 50.0 | long div | 27.7 | 27.6 | double div |
5.1 | 5.1 | dlouhá směna |
Ve starých dnech byla matematika s plovoucí desetinnou čárkou pravděpodobně řádově pomalejší než celočíselná matematika. Jak ukazuje tabulka 2 s moderními jednotkami s plovoucí desetinou čárkou s plovoucí desetinou čárkou, zdá se, že existuje malý rozdíl nebo žádný rozdíl. Je úžasné si myslet, že průměrný počítač poznámkového bloku je nyní gigaflop třídy počítač (pro problémy, které se vejdou do mezipaměti).
Pojďme se podívat na čáru zařazeného kódu z celého čísla a přidání testů s plovoucí desetinou čárkou:
Zpětné překlady 1 int přidat a plovoucí přidat
int add a = a + b + c + d + e + f + g + h + i;
0000004c 8B 54 24 10 mov edx,dword ptr [esp+10h]
00000050 03 54 24 14 add edx,dword ptr [esp+14h]
00000054 03 54 24 18 add edx,dword ptr [esp+18h]
00000058 03 54 24 1C add edx,dword ptr [esp+1Ch]
0000005c 03 54 24 20 add edx,dword ptr [esp+20h]
00000060 03 D5 add edx,ebp
00000062 03 D6 add edx,esi
00000064 03 D3 add edx,ebx
00000066 03 D7 add edx,edi
00000068 89 54 24 10 mov dword ptr [esp+10h],edx
float add i += a + b + c + d + e + f + g + h;
00000016 D9 05 38 61 3E 00 fld dword ptr ds:[003E6138h]
0000001c D8 05 3C 61 3E 00 fadd dword ptr ds:[003E613Ch]
00000022 D8 05 40 61 3E 00 fadd dword ptr ds:[003E6140h]
00000028 D8 05 44 61 3E 00 fadd dword ptr ds:[003E6144h]
0000002e D8 05 48 61 3E 00 fadd dword ptr ds:[003E6148h]
00000034 D8 05 4C 61 3E 00 fadd dword ptr ds:[003E614Ch]
0000003a D8 05 50 61 3E 00 fadd dword ptr ds:[003E6150h]
00000040 D8 05 54 61 3E 00 fadd dword ptr ds:[003E6154h]
00000046 D8 05 58 61 3E 00 fadd dword ptr ds:[003E6158h]
0000004c D9 1D 58 61 3E 00 fstp dword ptr ds:[003E6158h]
Zde vidíme, že kód s tečkovanými tečkami je blízko optimálnímu. V int add
případě kompilátor dokonce začlenil pět místních proměnných. V případě přidání float jsem byl povinen učinit proměnné a
prostřednictvím h
třídy statické, aby porazit běžné subexpression eliminace.
Volání metod
V této části prozkoumáme náklady a implementace volání metod. Předmět testu je třída T
implementace rozhraní I
, s různými druhy metod. Viz výpis 1.
Výpis metody 1 metody volání testovací metody
interface I { void itf1();… void itf5();… }
public class T : I {
static bool falsePred = false;
static void dummy(int a, int b, int c, …, int p) { }
static void inl_s1() { } … static void s1() { if (falsePred) dummy(1, 2, 3, …, 16); } … void inl_i1() { } … void i1() { if (falsePred) dummy(1, 2, 3, …, 16); } … public virtual void v1() { } … void itf1() { } … virtual void itf5() { } …}
Zvažte tabulku 3. se zobrazí, k první aproximaci, metoda je buď vložena (abstrakce nic) nebo ne (abstrakce náklady >5X celočíselná operace). Zdá se, že nezpracované náklady na statické volání, volání instance, virtuální volání nebo volání rozhraní nejsou významné.
Table 3 – časy volání metody (ns)
Průměr | Min | Primitivní | Volaný | Průměr | Min | Primitivní | Volaný |
---|---|---|---|---|---|---|---|
0.2 | 0.2 | vložené statické volání | inl_s1 |
5.4 | 5.4 | virtuální hovor | v1 |
6.1 | 6.1 | statické volání | s1 |
5.4 | 5.4 | toto virtuální volání | v1 |
1.1 | 1.0 | Volání vložené instance | inl_i1 |
6.6 | 6.5 | volání rozhraní | itf1 |
6.8 | 6.8 | volání instance | i1 |
1.1 | 1.0 | inst itf instance call | itf1 |
0.2 | 0.2 | Inlined this inst call | inl_i1 |
0.2 | 0.2 | toto volání instance itf | itf1 |
6.2 | 6.2 | toto volání instance | i1 |
5.4 | 5.4 | inst itf virtual call | itf5 |
5.4 | 5.4 | toto virtuální volání itf | itf5 |
Tyto výsledky jsou však nerepresentativní nejlepších případů, což má vliv na provádění časově omezených časových smyček milionykrát. V těchto testovacích případech jsou weby volání virtuálních metod a rozhraní monomorfní (například pro každou lokalitu volání, cílová metoda se v průběhu času nemění), takže kombinace mechanismů odesílání virtuální metody a rozhraní do mezipaměti (ukazatele a položky mapování tabulek metod metod a rozhraní) a velkolepě provident branch prediction umožňuje procesoru provádět nerealisticky efektivní úlohy volání těchto jinak obtížné-predikce, větve závislé na datech. V praxi může zmeškat mezipaměť dat u jakéhokoli z dat mechanismu odeslání nebo chybné diktování větve (to je povinná kapacitou miss nebo polymorfní volání webu), může a zpomalí volání virtuálních a rozhraní podle desítek cyklů.
Pojďme se podrobněji podívat na každou z těchto časů volání metod.
V prvním případě vložené statické volání, voláme řadu prázdných statických metod s1_inl()
atd. Vzhledem k tomu, že kompilátor zcela vyřadí všechna volání, skončíme načasováním prázdné smyčky.
Aby bylo možné měřit přibližné náklady na volání statické metody, vytvoříme statické metody s1()
atd. tak velké, že jsou pro vložení do volajícího nedostupné.
Všimněte si, že musíme dokonce použít explicitní proměnnou predikátu false falsePred
. Pokud jsme napsali
static void s1() { if (false) dummy(1, 2, 3, …, 16); }
kompilátor JIT eliminuje mrtvé volání dummy
a vložené celé (nyní prázdné) tělo metody jako předtím. Mimochodem, zde některé z 6,1 ns volání čas musí být přiřazovat (false) predikát test a přeskočit uvnitř volanou statickou metodu s1
. (Mimochodem, lepší způsob, jak zakázat vkládání, je CompilerServices.MethodImpl(MethodImplOptions.NoInlining)
atribut.)
Stejný přístup byl použit pro vložené volání instance a pravidelné časování volání instance. Vzhledem k tomu, že specifikace jazyka C# zajišťuje, že jakékoli volání odkazu na objekt null vyvolá NullReferenceException, musí každá lokalita volání zajistit, aby instance nebyla null. To se provádí dereferencováním odkazu na instanci; pokud je null, vygeneruje chybu, která se změní na tuto výjimku.
Při zpětném překladu 2 používáme jako instanci statickou proměnnou t
, protože když jsme použili místní proměnnou
T t = new T();
kompilátor nahodil instanci null rezervovat smyčku.
zpětné volání metody instance 2 s instancí null "check"
t.i1();
00000012 8B 0D 30 21 A4 05 mov ecx,dword ptr ds:[05A42130h]
00000018 39 09 cmp dword ptr [ecx],ecx
0000001a E8 C1 DE FF FF call FFFFDEE0
Případy vystihovají volání této instance a volání této instance jsou stejné, s výjimkou instance je this
; zde byla odstraněna kontrola null.
Disassembly 3 Tato metoda instance volání lokality
this.i1();
00000012 8B CE mov ecx,esi
00000014 E8 AF FE FF FF call FFFFFEC8
volání virtuálních metod fungují stejně jako v tradičních implementacích jazyka C++. Adresa každé nově zavedené virtuální metody je uložena v novém slotu v tabulce metod typu. Tabulka metod každého odvozeného typu odpovídá a rozšiřuje její základní typ a každá přepsání virtuální metody nahrazuje adresu virtuální metody základního typu adresou virtuální metody odvozeného typu v odpovídajícím slotu v tabulce metod odvozeného typu.
V lokalitě volání virtuální metody se ve srovnání s voláním instance vyvolá dvě další načtení, jedno pro načtení adresy tabulky metod (vždy nalezeno v *(this+0)
) a další pro načtení příslušné adresy virtuální metody z tabulky metod a jeho volání. Viz Demontáž 4.
zpětného překladu 4 virtuálních metod volání lokality
this.v1();
00000012 8B CE mov ecx,esi
00000014 8B 01 mov eax,dword ptr [ecx] ; fetch method table address
00000016 FF 50 38 call dword ptr [eax+38h] ; fetch/call method address
Nakonec se dostaneme k volání metody rozhraní (Disassembly 5). V jazyce C++ tyto hodnoty nemají žádný přesný ekvivalent. Každý daný typ může implementovat libovolný počet rozhraní a každé rozhraní logicky vyžaduje vlastní tabulku metod. Abychom mohli odeslat metodu rozhraní, vyhledáme tabulku metod, její mapu rozhraní, položku rozhraní v této mapě a pak zavoláme nepřímou prostřednictvím příslušné položky v části rozhraní tabulky metod.
zpětné volání metody rozhraní 5
i.itf1();
00000012 8B 0D 34 21 A4 05 mov ecx,dword ptr ds:[05A42134h]; instance address
00000018 8B 01 mov eax,dword ptr [ecx] ; method table addr
0000001a 8B 40 0C mov eax,dword ptr [eax+0Ch] ; interface map addr
0000001d 8B 40 7C mov eax,dword ptr [eax+7Ch] ; itf method table addr
00000020 FF 10 call dword ptr [eax] ; fetch/call meth addr
Zbývající primitivní časování, inst itf instance volání, to itf volání, inst itf virtuální volání, toto virtuální volání zvýraznit myšlenku, že pokaždé, když metoda odvozeného typu implementuje metodu rozhraní, zůstane volán prostřednictvím webu volání metody instance.
Například pro testovací tuto instanci itf volání, volání implementace metody rozhraní prostřednictvím odkazu instance (nikoli rozhraní), metoda rozhraní je úspěšně vložena a náklady jsou 0 ns. I implementace metody rozhraní je potenciálně vložená, když ji zavoláte jako metodu instance.
Volání metod, která mají být dosud zaměněná
U volání statických metod a metod instance (ale ne volání metody virtuálního rozhraní) kompilátor JIT vygeneruje v současné době různé sekvence volání metody v závislosti na tom, jestli cílová metoda už byla jitká časem, kdy je web volání jit.
Pokud volaný (cílová metoda) ještě nebyl zasunut, kompilátor vygeneruje volání nepřímým voláním prostřednictvím ukazatele, který je nejprve inicializován s "prejit stub". První volání cílové metody přichází na zástupný kód, který aktivuje kompilaci JIT metody, generování nativního kódu a aktualizaci ukazatele na adresu nového nativního kódu.
Pokud už je volaný, jeho nativní adresa kódu je známá, aby kompilátor vygeneroval přímé volání.
Vytvoření nového objektu
Vytvoření nového objektu se skládá ze dvou fází: přidělení objektu a inicializace objektů.
Pro odkazové typy se objekty přidělují na haldě s uvolňováním paměti. U typů hodnot, bez ohledu na to, zda je objekt typu stack-rezident nebo vložen v jiném odkazu nebo hodnotě, je nalezen v určitém konstantním posunu od nadřazené struktury – nevyžaduje se přidělení.
U typických objektů s malými referenčními typy je přidělení haldy velmi rychlé. Po každém uvolňování paměti s výjimkou přítomnosti připnutých objektů se živé objekty z haldy generace 0 komprimují a propagují na generaci 1, a proto má alokátor paměti pěkný velký souvislý volný paměť aréna pro práci s. Většina přidělení objektů způsobuje pouze kontrolu přírůstku ukazatele a mezí, což je levnější než typický alokátor seznamu C/C++ (malloc/operator new). Systém uvolňování paměti dokonce bere v úvahu velikost mezipaměti vašeho počítače, aby se pokusil zachovat objekty Gen0 v rychlém sladkém místě hierarchie mezipaměti/paměti.
Vzhledem k tomu, že upřednostňovaným stylem spravovaného kódu je přidělit většinu objektů s krátkými životnostmi a rychle je uvolnit, zahrneme také (v časových nákladech) amortizované náklady na uvolňování paměti těchto nových objektů.
Všimněte si, že uvolňování paměti neutratí žádný čas smutku mrtvých objektů. Pokud je objekt mrtvý, GC ho nevidí, neprojde ho, nedává mu myšlenku nanosekundy. GC se týká pouze dobrých životních podmínek.
(Výjimka: Finalizovatelné mrtvé objekty jsou zvláštní případ. GC tyto objekty sleduje a speciálně podporuje mrtvé finalizovatelné objekty na další generaci čekající na dokončení. To je nákladné a v nejhorším případě může tranzitivně propagovat rozsáhlé grafy mrtvých objektů. Proto objekty nedokončíte, pokud to není nezbytně nutné; a pokud je to nutné, zvažte použití Dispose Pattern, volání GC.SuppressFinalizer
, pokud je to možné.) Pokud to Finalize
metoda nevyžaduje, neuchovávejte odkazy z objektu finalizovatelného objektu na jiné objekty.
Samozřejmě, amortizované GC náklady na velký krátkodobý objekt je větší než náklady na malý krátkodobý objekt. Každý přidělení objektů nám přináší mnohem blíž k dalšímu cyklu uvolňování paměti; větší objekty to dělají mnohem dříve než ty malé. Dříve (nebo později) nastane okamžik, kdy se bude počítat. Cykly uvolňování paměti, zejména kolekce generace 0, jsou velmi rychlé, ale nejsou volné, i když velká většina nových objektů je mrtvá: najít (označit) živé objekty, je nejprve nutné pozastavit vlákna a pak procházet zásobníky a další datové struktury shromažďovat kořenové odkazy na objekty do haldy.
(Možná výrazněji, méně větších objektů se vejde do stejné velikosti mezipaměti jako menší objekty. Efekty chybějící mezipaměti můžou snadno dominovat efekty délky cesty kódu.)
Jakmile je přidělen prostor pro objekt, zůstane inicializovat (sestavit). CLR zaručuje, že všechny odkazy na objekty jsou předinicializovány na hodnotu null a všechny primitivní skalární typy jsou inicializovány na hodnotu 0, 0.0, false atd. (Proto není nutné je v uživatelem definovaných konstruktorech provádět redundantně. Nebojte se, samozřejmě. Mějte ale na paměti, že kompilátor JIT aktuálně neoptimalizuje vaše redundantní úložiště.)
Kromě vynulování polí instance clR inicializuje (pouze odkazové typy) interní implementační pole objektu: ukazatel tabulky metody a slovo záhlaví objektu, které předchází ukazatel tabulky metody. Pole také získávají pole Délka a pole objektů získávají pole typu Délka a typ prvku.
ClR pak volá konstruktor objektu, pokud existuje. Konstruktor každého typu, ať už je vygenerovaný uživatelem nebo kompilátor, nejprve volá konstruktor základního typu a potom spustí inicializaci definovanou uživatelem, pokud existuje.
Teoreticky by to mohlo být nákladné pro scénáře hluboké dědičnosti. Pokud E rozšiřuje D rozšiřuje jazyk C rozšiřuje A (rozšiřuje System.Object), inicializace E by vždy způsobovala pět volání metody. V praxi nejsou věci tak špatné, protože kompilátor zařadí (do neprázdnosti) volání prázdných konstruktorů základního typu.
Když odkazujeme na první sloupec tabulky 4, všimněte si, že můžeme vytvořit a inicializovat strukturu D
se čtyřmi poli int v přibližně 8 int-add-times. Zpětný překlad 6 je vygenerovaný kód ze tří různých smyček časování, vytvoření A, C a E. (V rámci každé smyčky upravíme každou novou instanci, což kompilátor JIT udržuje v optimalizaci všeho mimo.)
hodnoty tabulky 4 a časy vytváření objektů typu odkazu (ns)
Průměr | Min | Primitivní | Průměr | Min | Primitivní | Průměr | Min | Primitivní |
---|---|---|---|---|---|---|---|---|
2.6 | 2.6 | new valtype L1 | 22.0 | 20.3 | nový reftype L1 | 22.9 | 20.7 | new rt ctor L1 |
4.6 | 4.6 | new valtype L2 | 26.1 | 23.9 | nový reftype L2 | 27.8 | 25.4 | new rt ctor L2 |
6.4 | 6.4 | new valtype L3 | 30.2 | 27.5 | nový reftype L3 | 32.7 | 29.9 | new rt ctor L3 |
8.0 | 8.0 | new valtype L4 | 34.1 | 30.8 | nový reftype L4 | 37.7 | 34.1 | new rt ctor L4 |
23.0 | 22.9 | new valtype L5 | 39.1 | 34.4 | nový reftype L5 | 43.2 | 39.1 | new rt ctor L5 |
22.3 | 20.3 | new rt empty ctor L1 | 28.6 | 26.7 | new rt no-inl L1 | |||
26.5 | 23.9 | new rt empty ctor L2 | 38.9 | 36.5 | new rt no-inl L2 | |||
38.1 | 34.7 | new rt empty ctor L3 | 50.6 | 47.7 | new rt no-inl L3 | |||
34.7 | 30.7 | new rt empty ctor L4 | 61.8 | 58.2 | new rt no-inl L4 | |||
38.5 | 34.3 | new rt empty ctor L5 | 72.6 | 68.5 | new rt no-inl L5 |
Demontáž objektu typu 6 Typ hodnoty
A a1 = new A(); ++a1.a;
00000020 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
00000027 FF 45 FC inc dword ptr [ebp-4]
C c1 = new C(); ++c1.c;
00000024 8D 7D F4 lea edi,[ebp-0Ch]
00000027 33 C0 xor eax,eax
00000029 AB stos dword ptr [edi]
0000002a AB stos dword ptr [edi]
0000002b AB stos dword ptr [edi]
0000002c FF 45 FC inc dword ptr [ebp-4]
E e1 = new E(); ++e1.e;
00000026 8D 7D EC lea edi,[ebp-14h]
00000029 33 C0 xor eax,eax
0000002b 8D 48 05 lea ecx,[eax+5]
0000002e F3 AB rep stos dword ptr [edi]
00000030 FF 45 FC inc dword ptr [ebp-4]
Dalších pět časů (nový reftype L1, ... Nové reftype L5) jsou určeny pro pět úrovní dědičnosti referenčních typů A
, ..., E
, sans uživatelem definované konstruktory:
public class A { int a; }
public class B : A { int b; }
public class C : B { int c; }
public class D : C { int d; }
public class E : D { int e; }
Při porovnávání časů referenčního typu s časy typů hodnot vidíme, že amortizované přidělení a uvolnění nákladů na každou instanci je přibližně 20 ns (20x int add time) na testovacím počítači. To je rychlé – přidělování, inicializace a uvolnění asi 50 milionů krátkodobých objektů za sekundu, trvalé. Pro objekty tak malé jako pět polí, přidělení a kolekce účty pouze pro polovinu času vytvoření objektu. Viz Demontáž 7.
Zpětné rozebrání 7 konstrukce objektu referenčního typu
new A();
0000000f B9 D0 72 3E 00 mov ecx,3E72D0h
00000014 E8 9F CC 6C F9 call F96CCCB8
new C();
0000000f B9 B0 73 3E 00 mov ecx,3E73B0h
00000014 E8 A7 CB 6C F9 call F96CCBC0
new E();
0000000f B9 90 74 3E 00 mov ecx,3E7490h
00000014 E8 AF CA 6C F9 call F96CCAC8
Poslední tři sady pěti časování představují varianty tohoto zděděného scénáře vytváření tříd.
New rt empty ctor L1, ..., new rt empty ctor L5: Každý typ
A
, ...,E
má prázdný uživatelem definovaný konstruktor. Všechny jsou vložené a vygenerovaný kód je stejný jako výše uvedený.New rt ctor L1, ..., new rt ctor L5: Každý typ
A
, ...,E
má uživatelem definovaný konstruktor, který nastavuje jeho proměnnou instance na 1:public class A { int a; public A() { a = 1; } } public class B : A { int b; public B() { b = 1; } } public class C : B { int c; public C() { c = 1; } } public class D : C { int d; public D() { d = 1; } } public class E : D { int e; public E() { e = 1; } }
Kompilátor vnoří každou sadu vnořených konstruktorů základní třídy volání do lokality new
. (Demontáž 8).
disassembly 8 Hluboce vložené konstruktory
new A();
00000012 B9 A0 77 3E 00 mov ecx,3E77A0h
00000017 E8 C4 C7 6C F9 call F96CC7E0
0000001c C7 40 04 01 00 00 00 mov dword ptr [eax+4],1
new C();
00000012 B9 80 78 3E 00 mov ecx,3E7880h
00000017 E8 14 C6 6C F9 call F96CC630
0000001c C7 40 04 01 00 00 00 mov dword ptr [eax+4],1
00000023 C7 40 08 01 00 00 00 mov dword ptr [eax+8],1
0000002a C7 40 0C 01 00 00 00 mov dword ptr [eax+0Ch],1
new E();
00000012 B9 60 79 3E 00 mov ecx,3E7960h
00000017 E8 84 C3 6C F9 call F96CC3A0
0000001c C7 40 04 01 00 00 00 mov dword ptr [eax+4],1
00000023 C7 40 08 01 00 00 00 mov dword ptr [eax+8],1
0000002a C7 40 0C 01 00 00 00 mov dword ptr [eax+0Ch],1
00000031 C7 40 10 01 00 00 00 mov dword ptr [eax+10h],1
00000038 C7 40 14 01 00 00 00 mov dword ptr [eax+14h],1
New rt no-inl L1, ..., new rt no-inl L5: Každý typ
A
, ...,E
má uživatelem definovaný konstruktor, který je záměrně napsán tak, aby byl příliš nákladný na vložený. Tento scénář simuluje náklady na vytváření složitých objektů s hierarchiemi hloubkové dědičnosti a nepravidelnými konstruktory.public class A { int a; public A() { a = 1; if (falsePred) dummy(…); } } public class B : A { int b; public B() { b = 1; if (falsePred) dummy(…); } } public class C : B { int c; public C() { c = 1; if (falsePred) dummy(…); } } public class D : C { int d; public D() { d = 1; if (falsePred) dummy(…); } } public class E : D { int e; public E() { e = 1; if (falsePred) dummy(…); } }
Posledních pět časování v tabulce 4 ukazuje další režii volání vnořených základních konstruktorů.
Interlude: Ukázka profileru CLR
Teď pro rychlou ukázku profileru CLR. Profiler CLR, dříve označovaný jako Profiler přidělení, používá rozhraní API pro profilaci CLR ke shromažďování dat událostí, zejména volání, vrácení a přidělení objektů a událostí uvolňování paměti při spuštění vaší aplikace. (CLR Profiler je "invazní" profiler, což znamená, že profilovaná aplikace bohužel podstatně zpomaluje.) Po shromáždění událostí můžete pomocí profileru CLR prozkoumat chování přidělení paměti a GC vaší aplikace, včetně interakce mezi hierarchickým grafem volání a vzory přidělování paměti.
Modul CLR Profiler stojí za to se naučit, protože pro mnoho aplikací spravovaného kódu s výzvou k výkonu poskytuje pochopení profilu přidělení dat kritický přehled potřebný ke snížení pracovní sady a zajištění rychlých a frugalačních komponent a aplikací.
Profiler CLR může také odhalit, které metody přidělují více úložiště, než jste čekali, a může odhalit případy, kdy neúmyslně uchováváte odkazy na nepoužité objektové grafy, které by jinak mohly být uvolněny GC. (Běžný vzor návrhu problému je softwarová mezipaměť nebo vyhledávací tabulka položek, které už nejsou potřeba nebo jsou bezpečné k pozdějšímu rekonstituci. Je to smutné, když mezipaměť udržuje grafy objektů naživu po jejich užitečném životě. Místo toho nezapomeňte zrušit odkazy na objekty, které už nepotřebujete.)
Obrázek 1 je zobrazení časové osy haldy během provádění testovacího ovladače časování. Vzor sawtooth označuje přidělení mnoha tisíc instancí objektů C
(purpurová), D
(fialová) a E
(modrá). Každých několik milisekund budeme v nové haldě (generace 0) žvýkat další přibližně 150 kB paměti RAM a systém uvolňování paměti běží krátce, aby ho recykloval a propagoval všechny živé objekty na gen 1. Je pozoruhodný, že i pod tímto invazním (pomalým) profilačním prostředím v intervalu 100 ms (2,8 s až 2,9s) procházíme ~8 generace 0 cyklů GC. Pak na 2,977 s, aby místo pro jinou E
instanci, uvolňování paměti provede 1. generace uvolňování paměti, která shromažďuje a komprimuje haldu Gen 1– a tak pila pokračuje od nižší počáteční adresy.
obrázek 1 zobrazení časového řádku profileru CLR
Všimněte si, že čím větší je objekt (E větší než D větší než C), tím rychleji se halda gen 0 vyplní a častější cyklus GC.
Přetypování a kontroly typů instancí
Základem bezpečného, bezpečného ověřitelného spravovaného kódu je bezpečnost typů. Bylo by možné přetypovat objekt na typ, který není, bylo by jednoduché ohrozit integritu CLR a tak mít ho na milost nedůvěryhodného kódu.
tabulky 5 cast and isinst Times (ns)
Průměr | Min | Primitivní | Průměr | Min | Primitivní |
---|---|---|---|---|---|
0.4 | 0.4 | přetypování 1 | 0.8 | 0.8 | isinst up 1 |
0.3 | 0.3 | přetypovat 0 | 0.8 | 0.8 | isinst down 0 |
8.9 | 8.8 | přetypovat 1 | 6.3 | 6.3 | isinst down 1 |
9.8 | 9.7 | přetypování (nahoru 2) dolů 1 | 10.7 | 10.6 | isinst (up 2) down 1 |
8.9 | 8.8 | přetypovat 2 | 6.4 | 6.4 | isinst down 2 |
8.7 | 8.6 | přetypovat 3 | 6.1 | 6.1 | isinst down 3 |
Tabulka 5 ukazuje režii těchto povinných kontrol typů. Přetypování odvozeného typu na základní typ je vždy bezpečné a volné; zatímco přetypování ze základního typu na odvozený typ musí být kontrolováno.
Přetypování (zaškrtnuto) převede odkaz na objekt na cílový typ nebo vyvolá InvalidCastException
.
Naproti tomu instrukce isinst
CIL se používá k implementaci klíčového slova as
jazyka C#:
bac = ac as B;
Pokud ac
není B
nebo odvozena z B
, výsledek je null
, nikoli výjimka.
Výpis 2 ukazuje jednu ze smyček časování přetypování a Disassembly 9 ukazuje vygenerovaný kód pro jeden přetypování na odvozený typ. Aby bylo možné přetypování provést, kompilátor vygeneruje přímé volání pomocné rutiny.
Výpis 2 smyček pro testování časování přetypování
public static void castUp2Down1(int n) {
A ac = c; B bd = d; C ce = e; D df = f;
B bac = null; C cbd = null; D dce = null; E edf = null;
for (n /= 8; --n >= 0; ) {
bac = (B)ac; cbd = (C)bd; dce = (D)ce; edf = (E)df;
bac = (B)ac; cbd = (C)bd; dce = (D)ce; edf = (E)df;
}
}
zpětné přetypování 9
bac = (B)ac;
0000002e 8B D5 mov edx,ebp
00000030 B9 40 73 3E 00 mov ecx,3E7340h
00000035 E8 32 A7 4E 72 call 724EA76C
Vlastnosti
Ve spravovaném kódu je vlastnost dvojicí metod, getter vlastnosti a setter vlastnosti, která funguje jako pole objektu. Metoda get_ načte vlastnost; metoda set_ aktualizuje vlastnost na novou hodnotu.
Kromě toho se vlastnosti chovají a stojí stejně jako běžné metody instance a virtuální metody. Pokud k jednoduchému načtení nebo uložení pole instance používáte vlastnost, obvykle je vložena stejně jako u jakékoli malé metody.
Tabulka 6 ukazuje čas potřebný k načtení (a přidání) a k uložení sady celočíselné instance polí a vlastností. Náklady na získání nebo nastavení vlastnosti jsou skutečně stejné jako přímý přístup k podkladovému poli, , pokud vlastnost není deklarována virtuální, v takovém případě jsou náklady přibližně na volání virtuální metody. Žádné překvapení.
pole a časy vlastností tabulky 6 (ns)
Průměr | Min | Primitivní |
---|---|---|
1.0 | 1.0 | získat pole |
1.2 | 1.2 | získat prop |
1.2 | 1.2 | nastavit pole |
1.2 | 1.2 | set prop |
6.4 | 6.3 | získání virtuální prop |
6.4 | 6.3 | nastavení virtuální prop |
Překážky zápisu
Systém uvolňování paměti CLR využívá dobrou výhodu "generační hypotézy",většina nových objektů zemře mladou– aby se minimalizovala režie na sběr.
Halda je logicky rozdělená do generací. Nejnovější objekty žijí ve generaci 0 (gen 0). Tyto objekty ještě nepřežily kolekci. Během kolekce gen 0 GC určuje, které objekty gen0 jsou dostupné z kořenové sady GC, která zahrnuje odkazy na objekty v registrech počítačů, na zásobníku, odkazy na objekty statického pole třídy atd. Přechodně dosažitelné objekty jsou "živé" a povýšené (zkopírované) na generaci 1.
Vzhledem k tomu, že celková velikost haldy může být stovky MB, zatímco velikost haldy gen 0 může být pouze 256 kB, omezení rozsahu trasování grafu objektů GC na haldu gen 0 je optimalizace nezbytná pro dosažení velmi krátkých časů pozastavení kolekce CLR.
Odkaz na objekt Gen 0 je však možné uložit do pole odkazu na objekt gen 1 nebo gen2. Vzhledem k tomu, že během kolekce gen 0 neskenujeme objekty gen 1 nebo Gen 2, může být tento objekt chybně uvolněný uvolňováním paměti. Nemůžeme to nechat!
Místo toho všechna úložiště všech referenčních polí objektů v haldě účtují bariéru zápisu. Toto je účetní kód, který efektivně zaznamenává ukládání odkazů na objekty nové generace do polí objektů starší generace. Tato stará referenční pole objektu jsou přidána do kořenové sady GC následných GC(s).
Režie při zápisu z úložiště objektů a odkazů na pole je srovnatelná s náklady na jednoduché volání metody (Tabulka 7). Jedná se o nové výdaje, které nejsou přítomné v nativním kódu C/C++, ale obvykle je to malá cena platit za super rychlé přidělování objektů a GC a mnoho výhod produktivity automatické správy paměti.
tabulka 7 – doba bariéry (ns)
Průměr | Min | Primitivní |
---|---|---|
6.4 | 6.4 | bariéra zápisu |
Překážky zápisu můžou být nákladné v těsné vnitřní smyčce. V letech se ale můžeme těšit na pokročilé techniky kompilace, které snižují počet překážek zápisu a celkové amortizované náklady.
Můžete si myslet, že překážky zápisu jsou nezbytné pouze u úložišť odkazovaných na objektová pole referenčních typů. V rámci metody typu hodnoty však ukládá do svých referenčních polí objektu (pokud existuje) jsou také chráněny překážkami zápisu. To je nezbytné, protože samotný typ hodnoty může být někdy vložen do typu odkazu umístěného v haldě.
Přístup k elementu Array
Chcete-li diagnostikovat a vyloučit chyby a poškození haldy pole mimo hranice a chránit integritu samotného modulu CLR, jsou zatížení a úložiště prvků pole kontrolovány a zajišťuje, že index je v intervalu [0,pole. Délka-1] včetně nebo vyhazování IndexOutOfRangeException
.
Naše testy měří čas načtení nebo uložení prvků pole int[]
a pole A[]
. (Tabulka 8).
tabulce 8 polí (ns)
Průměr | Min | Primitivní |
---|---|---|
1.9 | 1.9 | load int array elem |
1.9 | 1.9 | store int array elem |
2.5 | 2.5 | load obj array elem |
16.0 | 16.0 | store obj array elem |
Kontrola mezí vyžaduje porovnání indexu pole s implicitním polem. Pole Délka Jak ukazuje Funkce zpětného překladu 10, v pouhých dvou pokynech zkontrolujeme, že index není menší než 0 ani větší než nebo rovno poli. Délka – pokud ano, vytvoříme větev na odřádkovou sekvenci, která vyvolá výjimku. Totéž platí pro načtení prvků pole objektů a pro uložení do polí int a dalších jednoduchých hodnotových typů. (Load obj pole elem čas je (bezvýznamně) pomalejší kvůli mírnému rozdílu ve vnitřní smyčce.)
zpětného překladu 10 prvků pole zatížení
; i in ecx, a in edx, sum in edi
sum += a[i];
00000024 3B 4A 04 cmp ecx,dword ptr [edx+4] ; compare i and array.Length
00000027 73 19 jae 00000042
00000029 03 7C 8A 08 add edi,dword ptr [edx+ecx*4+8]
… ; throw IndexOutOfRangeException
00000042 33 C9 xor ecx,ecx
00000044 E8 52 78 52 72 call 7252789B
Díky optimalizaci kvality kódu kompilátor JIT často eliminuje redundantní kontroly hranic.
Připomeňme si předchozí části, můžeme očekávat, že element pole objektů ukládá bude výrazně dražší. Pokud chcete uložit odkaz na objekt do pole odkazů na objekty, modul runtime musí:
- kontrola indexu pole je v mezích;
- check object is an instance of the array element type;
- proveďte bariéru zápisu (zapisování jakéhokoli odkazu mezigeneračního objektu z pole na objekt).
Tato posloupnost kódu je poměrně dlouhá. Místo toho, aby ho vygeneroval v každé lokalitě úložiště polí objektů, kompilátor vysílá volání sdílené pomocné funkce, jak je znázorněno v disassembly 11. Toto volání plus tyto tři účty akcí pro další čas potřebný v tomto případě.
Disassembly 11 Store object array element
; objarray in edi
; obj in ebx
objarray[1] = obj;
00000027 53 push ebx
00000028 8B CF mov ecx,edi
0000002a BA 01 00 00 00 mov edx,1
0000002f E8 A3 A0 4A 72 call 724AA0D7 ; store object array element helper
Boxing and Unboxing
Partnerství mezi kompilátory .NET a CLR umožňuje používat typy hodnot, včetně primitivních typů, jako je int (System.Int32), aby se účastnily, jako by šlo o odkazy na odkazy na objekty. Tato cenová dostupnost – tento syntaktický cukr – umožňuje předání hodnotových typů metodám jako objektům, ukládání do kolekcí jako objektů atd.
Chcete-li "box" typ hodnoty vytvořit objekt typu odkazu, který obsahuje kopii jeho typu hodnoty. To je koncepčně stejné jako vytvoření třídy s nepojmenovaným polem instance stejného typu jako typ hodnoty.
Pokud chcete "rozbalit" typ hodnoty, je zkopírovat hodnotu z objektu do nové instance typu hodnoty.
Jak tabulka 9 ukazuje (ve srovnání s tabulkou 4), amortizovaný čas potřebný k balení int a později k uvolnění paměti je srovnatelný s časem potřebným k vytvoření instance malé třídy s jedním int polem.
tabulky 9 Box and Unbox int Times (ns)
Průměr | Min | Primitivní |
---|---|---|
29.0 | 21.6 | box int |
3.0 | 3.0 | rozbalení doručené pošty |
Chcete-li rozbalit boxovaný int objekt vyžaduje explicitní přetypování na int. Tento příkaz se zkompiluje do porovnání typu objektu (reprezentovaného adresou tabulky metod) a adresy tabulky s vloženou metodou. Pokud jsou stejné, hodnota se zkopíruje z objektu. V opačném případě je vyvolán výjimka. Viz Demontáž 12.
zpětného překladu 12 boxů a rozbalení
box object o = 0;
0000001a B9 08 07 B9 79 mov ecx,79B90708h
0000001f E8 E4 A5 6C F9 call F96CA608
00000024 8B D0 mov edx,eax
00000026 C7 42 04 00 00 00 00 mov dword ptr [edx+4],0
unbox sum += (int)o;
00000041 81 3E 08 07 B9 79 cmp dword ptr [esi],79B90708h ; "type == typeof(int)"?
00000047 74 0C je 00000055
00000049 8B D6 mov edx,esi
0000004b B9 08 07 B9 79 mov ecx,79B90708h
00000050 E8 A9 BB 4E 72 call 724EBBFE ; no, throw exception
00000055 8D 46 04 lea eax,[esi+4]
00000058 3B 08 cmp ecx,dword ptr [eax]
0000005a 03 38 add edi,dword ptr [eax] ; yes, fetch int field
Deleguje
V jazyce C je ukazatel na funkci primitivní datový typ, který doslova ukládá adresu funkce.
C++ přidává ukazatele na členské funkce. Ukazatel na členovou funkci (PMF) představuje vyvolání odložené členské funkce. Adresa jiné než virtuální členské funkce může být jednoduchá adresa kódu, ale adresa virtuální členské funkce musí ztělesnit konkrétní volání virtuální členské funkce – dereference takového PMF je volání virtuální funkce.
Pokud chcete dereference pmF jazyka C++, musíte zadat instanci:
A* pa = new A;
void (A::*pmf)() = &A::af;
(pa->*pmf)();
Před lety jsme se na vývojovém týmu kompilátoru Visual C++ ptali sami sebe, jaký druh bestie je nahý výraz pa->*pmf
(operátor volání funkce sans)? Nazvali jsme ho vázaného ukazatele na členovou funkci, ale volání latentní členské funkce je stejně apt.
Když se vrátíte do země spravovaného kódu, delegovací objekt je právě to – opožděné volání metody. Objekt delegáta představuje metodu volání i instanci, která ji má volat – nebo pro delegáta na statickou metodu, pouze statickou metodu, která se má volat.
(Jak uvádí naše dokumentace: Deklarace delegáta definuje typ odkazu, který lze použít k zapouzdření metody s konkrétním podpisem. Instance delegáta zapouzdřuje statickou metodu nebo metodu instance. Delegáti jsou zhruba podobné ukazatelům funkcí v jazyce C++; delegáti jsou však typově bezpečné a zabezpečené.)
Typy delegátů v jazyce C# jsou odvozené typy MulticastDelegate. Tento typ poskytuje bohatou sémantiku, včetně možnosti vytvořit seznam vyvolání párů (object,method), které se mají vyvolat při vyvolání delegáta.
Delegáti také poskytují zařízení pro vyvolání asynchronní metody. Jakmile definujete typ delegáta a vytvoříte instanci, inicializuje se s latentním voláním metody, můžete ho vyvolat synchronně (syntaxe volání metody) nebo asynchronně prostřednictvím BeginInvoke
. Pokud se volá BeginInvoke
, modul runtime zařadí volání do fronty a okamžitě se vrátí volajícímu. Cílová metoda se později volá ve vlákně fondu vláken.
Všechny tyto bohaté sémantiky nejsou levné. Při porovnání tabulky 10 a tabulky 3 mějte na paměti, že volání delegáta je ** přibližně osmkrát pomalejší než volání metody. Počítejte s tím, že se v průběhu času zlepší.
tabulka 10 – Čas vyvolání delegáta (ns)
Průměr | Min | Primitivní |
---|---|---|
41.1 | 40.9 | vyvolání delegáta |
Chyby stránek, chyby v mezipaměti a architektura počítače
Zpátky v "dobrých starých dnech", circa 1983, procesory byly pomalé (~,5 milionu instrukcí/s) a relativně řečeno, paměť RAM byla dostatečně rychlá, ale malá (přibližně 300 ns přístup krát na 256 kB DRAM) a disky byly pomalé a velké (přibližně 25 ms přístup na 10 MB disků). Mikroprocesory počítačů byly skalární cisc, většina plovoucí desetiny byla v softwaru a nebyly k dispozici žádné mezipaměti.
Po dvaceti letech Mooreova zákona, circa 2003, procesory jsou rychlé (vydávání až tří operací na cyklus při 3 GHz), RAM je relativně velmi pomalé (přibližně 100 ns přístup krát na 512 MB DRAM) a disky jsou glacialně pomalé a obrovské (přibližně 10 ms přístup na 100 GB disků). Mikroprocesory počítačů jsou teď mimo pořadí, což je superscalar hyperthreadingový hyperthreading RISCs (spouštění dekódovaných instrukcí CISC) a existuje několik vrstev mezipamětí – například určitý serverově orientovaný mikroprocesor má 32kB mezipaměť dat úrovně 1 (třeba 2 cykly latence), mezipaměť dat L2 512 KB L2 a mezipaměť dat 2 MB L3 (možná desítky cyklů latence), všechno na čipu.
V dobrých starých dnech jste mohli a někdy mohli spočítat bajty kódu, který jste napsali, a spočítat počet cyklů potřebných ke spuštění kódu. Zatížení nebo úložiště trvalo přibližně stejný počet cyklů jako sčítání. Moderní procesor používá predikci větví, spekulaci a provádění mimo pořadí (tok dat) napříč několika jednotkami funkcí k vyhledání paralelismu na úrovni instrukce a tak na několika frontách najednou.
Naše nejrychlejší počítače teď můžou vydávat až 9 000 operací za mikrosekundy, ale ve stejném mikrosekundách se načítají nebo ukládají pouze do paměti DRAM ~10 řádků mezipaměti. V počítačových architekturách se to označuje jako dosažení pamětizdi. Mezipaměti skryjí latenci paměti, ale pouze do bodu. Pokud se kód nebo data nevejdou do mezipaměti a/nebo vykazují špatnou lokalitu odkazu, naše 9000 operací na mikrosekundový jet degeneruje na 10 tricyklů zatížení na mikrosekundu.
A (nenechte to udělat pro vás) by měla pracovní sada programu překročit dostupnou fyzickou paměť RAM a program začne provádět chyby pevných stránek, a pak v každé 10 000-mikrosekundové službě selhání stránky (přístup k disku) vynecháme příležitost přivést uživatele až 90 milionů operací blíže ke své odpovědi. To je jen tak hrozné, že věřím, že od tohoto dne se postaráte o měření své pracovní sady (vadump) a pomocí nástrojů, jako je CLR Profiler, eliminovat zbytečné přidělení a neúmyslné uchovávání grafů objektů.
Ale co to všechno musí dělat s vědomím nákladů na primitivy spravovaného kódu?Všechno*.*
Připomeňme si tabulku 1, omnibusový seznam primitivních časů spravovaného kódu měřený na 1,1 GHz P-III, všimněte si, že pokaždé, i amortizované náklady na přidělování, inicializaci a uvolnění pěti objektů pole s pěti úrovněmi explicitních volání konstruktoru, je rychlejší než jeden přístup DRAM. Pouze jedno načtení, které vynechá všechny úrovně mezipaměti na čipu, může trvat déle než téměř jakákoli operace spravovaného kódu.
Takže pokud se zajímáte o rychlost kódu, je nezbytné zvážit a měřit hierarchii mezipaměti a paměti při návrhu a implementaci algoritmů a datových struktur.
Čas na jednoduchou ukázku: Je rychlejší sečíst pole int nebo součet ekvivalentního propojeného seznamu int? Co, kolik a proč?
Chvíli si to rozmyslete. U malých položek, jako jsou inty, je paměťová stopa na prvek pole jedním čtvrtým prvkem propojeného seznamu. (Každý uzel propojeného seznamu má dvě slova režie objektu a dvě slova polí (další odkaz a položka int).) To by uškodilo využití mezipaměti. 1. Skóre pro přístup k poli
Při procházení pole ale může dojít ke kontrole maticových hranic pro každou položku. Právě jste viděli, že kontrola hranic nějakou dobu trvá. Možná, že to tipy měřítka ve prospěch propojeného seznamu?
zpětného překladu 13 součet pole int versus součet propojených seznamů
sum int array: sum += a[i];
00000024 3B 4A 04 cmp ecx,dword ptr [edx+4] ; bounds check
00000027 73 19 jae 00000042
00000029 03 7C 8A 08 add edi,dword ptr [edx+ecx*4+8] ; load array elem
for (int i = 0; i < m; i++)
0000002d 41 inc ecx
0000002e 3B CE cmp ecx,esi
00000030 7C F2 jl 00000024
sum int linked list: sum += l.item; l = l.next;
0000002a 03 70 08 add esi,dword ptr [eax+8]
0000002d 8B 40 04 mov eax,dword ptr [eax+4]
sum += l.item; l = l.next;
00000030 03 70 08 add esi,dword ptr [eax+8]
00000033 8B 40 04 mov eax,dword ptr [eax+4]
sum += l.item; l = l.next;
00000036 03 70 08 add esi,dword ptr [eax+8]
00000039 8B 40 04 mov eax,dword ptr [eax+4]
sum += l.item; l = l.next;
0000003c 03 70 08 add esi,dword ptr [eax+8]
0000003f 8B 40 04 mov eax,dword ptr [eax+4]
for (m /= 4; --m >= 0; ) {
00000042 49 dec ecx
00000043 85 C9 test ecx,ecx
00000045 79 E3 jns 0000002A
Odkaz na Disassembly 13, jsem skládal palubu ve prospěch propojeného seznamu procházení, zrušení jeho čtyřikrát, dokonce i odebrání obvyklé nulové ukazatele konec seznamu. Každá položka ve smyčce pole vyžaduje šest instrukcí, zatímco každá položka ve smyčce propojeného seznamu potřebuje pouze 11/4 = 2,75 instrukce. Co si myslíte, že je rychlejší?
Testovací podmínky: nejprve vytvořte pole jednoho milionu int a jednoduchý tradiční propojený seznam jednoho milionu int (1 uzly seznamu M). Poté doba, po kterou je potřeba přidat prvních 1 000, 10 000, 100 000, 100 000 a 1 000 000 položek. Opakováním každé smyčky mnohokrát změřte nejplošnější chování mezipaměti pro každý případ.
Co je rychlejší? Po odhadu se podívejte na odpovědi: posledních osm položek v tabulce 1.
Zajímavý! Časy jsou podstatně pomalejší, protože odkazovaná data rostou větší než následné velikosti mezipaměti. Verze pole je vždy rychlejší než verze propojeného seznamu, i když se spustí dvakrát tolik instrukcí; pro 100 000 položek je verze pole sedmkrát rychlejší!
Proč je to tak? Za prvé, méně propojených položek seznamu se vejde do libovolné úrovně mezipaměti. Všechny tyto záhlaví objektů a propojení ho prohazují mezery. Za druhé, náš moderní procesor toku dat mimo pořadí může potenciálně přiblížit dopředu a současně provádět pokrok na několika položkách v poli. Naproti tomu s propojeným seznamem, dokud nebude aktuální uzel seznamu v mezipaměti, procesor nemůže začít načítat další odkaz na uzel.
V případě 100 000 položek procesor utrácí (průměrně) přibližně (22–3,5)/22 = 84% času, kdy se jeho palec čeká na načtení řádku mezipaměti některého uzlu seznamu z DRAM. To zní špatně, ale věci by mohly být mnohem horší. Vzhledem k tomu, že propojené položky seznamu jsou malé, mnoho z nich se vejde na řádek mezipaměti. Vzhledem k tomu, že seznam procházíme v pořadí přidělení a vzhledem k tomu, že systém uvolňování paměti zachovává pořadí přidělení i v případě, že komprimuje mrtvé objekty mimo haldu, je pravděpodobné, že po načtení jednoho uzlu na řádku mezipaměti je teď v mezipaměti také několik dalších uzlů. Pokud byly uzly větší nebo pokud byly uzly seznamu v náhodném pořadí adres, může být každý navštívený uzel v plné mezipaměti. Přidání 16 bajtů do každého uzlu seznamu zdvojnásobí čas procházení na položku na 43 ns; +32 bajtů, 67 ns/položka; a přidáním 64 bajtů se znovu zdvojnásobí na 146 ns/položka, pravděpodobně průměrná latence DRAM na testovacím počítači.
Tak co je tady lekce s poznatky? Vyhněte se propojeným seznamům 100 000 uzlů? žádné. Lekce spočívá v tom, že efekty mezipaměti můžou dominovat všem aspektům nízké efektivity spravovaného kódu a nativního kódu. Pokud píšete spravovaný kód, který je důležitý pro výkon, zejména správu velkých datových struktur, mějte na paměti účinky mezipaměti, zamyslete se nad vzory přístupu ke struktuře dat a snažte se o menší nároky na data a dobrou lokalitu odkazu.
Mimochodem, trendem je, že paměťová stěna, poměr doby přístupu DRAM dělené časem operace procesoru, bude stále horší v průběhu času.
Tady jsou některá pravidla "návrhu s vědomím mezipaměti":
- Experimentujte s vašimi scénáři a měřením, protože je obtížné předpovědět efekty druhého pořadí a protože pravidla palce nestojí za papír, na který jsou vytištěny.
- Některé datové struktury, které jsou exemplifikované poli, využívají implicitní sousedství představují vztah mezi daty. Jiné, exemplifikované propojenými seznamy, používají explicitní ukazatele (odkazy) reprezentovat vztah. Implicitní sousedství je obecně vhodnější – implicitnost šetří místo v porovnání s ukazateli; a sousedství poskytuje stabilní umístění odkazu a může procesoru umožnit zahájit další práci před honěním dalšího ukazatele.
- Některé vzory použití upřednostňují hybridní struktury – seznamy malých polí, polí polí nebo B-stromů.
- Možná algoritmy plánování citlivé na přístup k diskům, které jsou navržené zpět, když přístup k disku stojí jenom 50 000 instrukcí procesoru, by se teď měly recyklovat, když přístupy DRAM můžou trvat tisíce operací procesoru.
- Vzhledem k tomu, že clR mark-and-compact uvolňování paměti zachovává relativní pořadí objektů, objekty přidělené v čase (a ve stejném vlákně) mají tendenci zůstat společně v prostoru. Tento jev můžete použít k promyšlené kolaci dat cliquish na běžných řádech mezipaměti.
- Data můžete chtít rozdělit do horkých částí, které se často procházejí a musí se vejít do mezipaměti a studené části, které se často používají a dají se "ukládat do mezipaměti".
Do-It-Yourself Time Experiments
Pro měření časování v tomto dokumentu jsem použil čítač výkonu Win32 s vysokým rozlišením QueryPerformanceCounter
(a QueryPerformanceFrequency
).
Snadno se volají prostřednictvím volání nespravovaného kódu:
[System.Runtime.InteropServices.DllImport("KERNEL32")]
private static extern bool QueryPerformanceCounter(
ref long lpPerformanceCount);
[System.Runtime.InteropServices.DllImport("KERNEL32")]
private static extern bool QueryPerformanceFrequency(
ref long lpFrequency);
Voláte QueryPerformanceCounter
těsně před a těsně za smyčkou časování, odečítáte počty, vynásobíte 1,0e9, vydělíte četností, vydělíte počtem iterací a to je přibližný čas pro iteraci v ns.
Kvůli omezením prostoru a času jsme nepokryli zamykání, zpracování výjimek ani systém zabezpečení přístupu kódu. zvažte cvičení pro čtenáře.
Mimochodem, vytvořil jsem demontáže v tomto článku pomocí zpětného okna v VS.NET 2003. Je to však trik. Pokud aplikaci spustíte v ladicím programu VS.NET, a to i jako optimalizovaný spustitelný soubor integrovaný v režimu vydání, spustí se v režimu ladění, ve kterém jsou zakázány optimalizace, jako je vkládání. Jediný způsob, jak jsem zjistil, jak získat náhled na optimalizovaný nativní kód, který kompilátor JIT generuje, bylo spustit testovací aplikaci mimo ladicí program a pak k němu připojit pomocí Debug.Processes.Attach.
Model nákladů na prostor?
Ironicky, úvahy o vesmíru brání důkladné diskuzi o vesmíru. Pár krátkých odstavců.
Aspekty nízké úrovně (několik aspektů c# (výchozí TypeAttributes.SequentialLayout) a x86 specifické):
- Velikost typu hodnoty je obecně celková velikost polí, přičemž 4 bajtová nebo menší pole jsou zarovnaná k jejich přirozeným hranicím.
- K implementaci sjednocení je možné použít atributy
[StructLayout(LayoutKind.Explicit)]
a[FieldOffset(n)]
. - Velikost referenčního typu je 8 bajtů plus celková velikost polí, zaokrouhlená nahoru na další 4 bajtovou hranici a se 4 bajty nebo menšími poli zarovnanými k jejich přirozeným hranicím.
- V jazyce C# mohou deklarace výčtu určovat libovolný celočíselný základní typ (kromě znaku), takže je možné definovat 8bitové, 16bitové, 32bitové a 64bitové výčty.
- Stejně jako v jazyce C/C++ můžete často oholit několik desítek procent místa z většího objektu tím, že odpovídajícím způsobem zvětšíte celočíselné pole.
- Velikost přiděleného referenčního typu můžete zkontrolovat pomocí profileru CLR.
- Velké objekty (mnoho desítek kB nebo více) se spravují v samostatné velké haldě objektu, aby se zabránilo nákladnému kopírování.
- Finalizovatelné objekty přebírají další generování GC zpět – používejte je střídmě a zvažte použití modelu Dispose Pattern.
Důležité informace o velkém obrázku:
- Každá doména AppDomain v současné době způsobuje značné režijní náklady na místo. Mnoho struktur modulu runtime a architektury se nesdílí napříč doménami AppDomains.
- V rámci procesu se zamknutý kód obvykle nesdílí napříč doménami AppDomains. Pokud je modul runtime speciálně hostovaný, je možné toto chování přepsat. Viz dokumentace pro
CorBindToRuntimeEx
a příznakSTARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN
. - V každém případě se kód s tečkovanými tečkami nesdílí mezi procesy. Pokud máte komponentu, která se načte do mnoha procesů, zvažte předkompilování pomocí NGEN ke sdílení nativního kódu.
Odraz
Bylo řečeno, že "pokud se musíte zeptat, jaké náklady reflexe, nemůžete si to dovolit". Pokud jste si tento postup přečetli, víte, jak důležité je zeptat se, jaké náklady stojí, a změřit tyto náklady.
Reflexe je užitečná a výkonná, ale ve srovnání s jitovaným nativním kódem není rychlá ani malá. Byli jste varováni. Změřte si to sami.
Závěr
Teď víte (více nebo méně), jaké náklady na spravovaný kód stojí na nejnižší úrovni. Teď máte základní znalosti nezbytné k tomu, aby inteligentnější implementace kompromisy a napsali rychleji spravovaný kód.
Viděli jsme, že zasunutí spravovaného kódu může být jako nativní kód "pedál na kov". Vaším úkolem je moudře kódovat a zvolit moudře mezi mnoha bohatými a snadno použitelnými zařízeními v rámci architektury.
Existují nastavení, kde nezáleží na výkonu a nastavení, kde je to nejdůležitější funkce produktu. Předčasně optimalizace je kořenem všeho zla. Je to ale bezstarostné nepozorování na efektivitu. Jste profesionální, umělec, řemeslník. Takže se ujistěte, že znáte náklady na věci. Pokud nevíte, nebo i když si myslíte, že to děláte – pravidelně změřte.
Pokud jde o tým CLR, stále pracujeme na poskytování platformy, která je podstatně produktivnější než nativní kód a přesto je rychlejší než nativní kód. Očekávat, že věci budou lepší a lepší. Zůstaňte v obraze.
Vzpomeň si na svůj slib.
Prostředky
- David Stutz et al, Shared Source CLI Essentials. O'Reilly a Assoc., 2003. ISBN 059600351X.
- Jan Gray, C++: Pod pokličkou.
- Gregor Noriskin, psaní High-Performance spravovaných aplikací: Základní, MSDN.
- Rico Mariani, základy uvolňování paměti a rady výkonu, MSDN.
- Řešení Schanzer, tipy a triky v aplikacích .NET, MSDN.
- Řešení Důležité informace o výkonu pro technologie Run-Time v rozhraní .NET Framework, MSDN.
- vadump (nástroje sady SDK platformy), MSDN.
- .NET Show, [Managed] Code Optimization, 10. září 2002, MSDN.