Psaní rychlejšího spravovaného kódu: Informace o nákladech

 

Jan Gray
Tým výkonu Microsoft CLR

Dne

Platí pro:
   Microsoft® .NET Framework

Shrnutí: Tento článek představuje nízkoúrovňový model nákladů na dobu provádění spravovaného kódu, který je založený na naměřených časech operací, aby vývojáři mohli dělat informovanější rozhodnutí o kódování a psát kód rychleji. (30 vytištěných stránek)

Stáhněte si profiler CLR. (330 kB)

Obsah

Úvod (a příslib)
Směrem k modelu nákladů pro spravovaný kód
Náklady ve spravovaném kódu
Závěr
Zdroje informací

Úvod (a příslib)

Existuje nesčetné způsoby implementace výpočtu a některé jsou mnohem lepší než jiné: jednodušší, čistší a snadněji se udržují. Některé způsoby jsou bleskově rychlé a některé jsou překvapivě pomalé.

Nechovejte na světě pomalý a tlustý kód. Nepohrdnete takovým kódem? Kód, který se spouští ve vhodných a spuštěných počítačích? Kód, který uzamkne uživatelské rozhraní na několik sekund v čase? Kód, který uchytá procesor nebo zatěžuje disk?

Nedělejte to. Místo toho se postavte a slibte spolu se mnou:

"Slibuji, že nedoručím pomalý kód. Rychlost je funkce, na které mi záleží. Každý den budu věnovat pozornost výkonu kódu. Budu pravidelně a metodicky měřit jeho rychlost a velikost. Naučím se, sestavím nebo koupím nástroje, které k tomu potřebuji. Je to moje zodpovědnost."

(Opravdu.) Takže jsi to slíbil? Dobře pro tebe.

Takže jak napíšete nejrychlejší a nejtěsnější kód den co den? Je to věc vědomého výběru štědrého způsobu v preferencích extravagantního, nafouklého způsobu, znovu a znovu, a věc promýšlení důsledků. Každá daná stránka kódu zachycuje desítky takových malých rozhodnutí.

Pokud ale nevíte, co stojí, nemůžete dělat chytrá rozhodnutí mezi alternativami: nemůžete psát efektivní kód, pokud nevíte, co stojí.

Za starých dobrých časů to bylo jednodušší. Dobří programátoři jazyka C to věděli. Každý operátor a operace v jazyce C, ať už je to přiřazení, celé číslo nebo matematika s plovoucí desetinou čárkou, dereference nebo volání funkce, mapují více nebo méně 1:1 na jednu primitivní operaci počítače. Pravda, někdy bylo k vložení správných operandů do správných registrů vyžadováno několik strojových instrukcí a někdy jedna instrukce mohla zachytit několik operací jazyka C (pověstně *dest++ = *src++;), ale obvykle jste mohli napsat (nebo přečíst) řádek kódu jazyka C a zjistit, kam čas uplynul. Pro kód i data byl kompilátor jazyka C WYWIWYG – "to, co napíšete, je to, co získáte". (Výjimkou byla a je volání funkcí. Pokud nevíte, jaké jsou náklady na tuto funkci, nevíte, že ano.)

V roce 1990 se počítačový softwarový průmysl rozhodl přejít z jazyka C na C++, aby se v roce 1990 těšil mnoha výhodám v oblasti softwarového inženýrství a produktivity, které přináší abstrakce dat, objektově orientované programování a opakované použití kódu.

C++ je nadmnožina jazyka C a je to "průběžné platby" – nové funkce nic nestojí, pokud je nepoužíváte , takže jsou přímo použitelné znalosti programování jazyka C, včetně interního nákladového modelu. Pokud vezmete nějaký funkční kód C a znovu ho zkompilujete pro C++, doba provádění a prostorová režie by se moc změnit neměla.

Na druhé straně C++ zavádí mnoho nových jazykových funkcí, včetně konstruktorů, destruktorů, nové, odstranění, jednoduché, více a virtuální dědičnosti, přetypování, členské funkce, virtuální funkce, přetížené operátory, ukazatele na členy, pole objektů, zpracování výjimek a složení stejných, které představují netriviální skryté náklady. Například virtuální funkce stojí další dvě nepřímé hodnoty za volání a do každé instance přidají skryté pole ukazatele tabulky. Nebo si představte, že tento neškodně vypadající kód:

{ complex a, b, c, d; … a = b + c * d; }

se zkompiluje do přibližně třinácti implicitních volání členské funkce (snad inlined).

Před devíti lety jsme toto téma prozkoumali v mém článku C++: Pod kapotou. Napsal(a):

"Je důležité pochopit, jak je váš programovací jazyk implementovaný. Takové znalosti rozptýlí strach a úžas "Co tady proboha kompilátor dělá?"; poskytuje důvěru při používání nových funkcí; a poskytuje přehled při ladění a učení dalších jazykových funkcí. Dává také pocit relativních nákladů na různé volby kódování, které jsou nezbytné k napsání nejúčinnějšího kódu den od dne."

Teď se podíváme na spravovaný kód podobně. Tento článek se zabývá nízkými časovými a prostorovými náklady na spravované provádění, abychom mohli v našem každodenním kódování udělat chytřejší kompromisy.

A dodržte 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. Odstraňuje celé kategorie chyb, jako jsou poškození haldy a chyby typu matice-index-mimo-vázané, které tak často vedou k frustrujícím ladicím relacím pozdě v 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ími Win32/COM/ATL/MFC/VB je rozhraní .NET Framework obnovující návrh s čistou tabulí, kde můžete s menším úsilím dělat více.

Pro vaši komunitu uživatelů umožňuje spravovaný kód bohatší a robustnější aplikace – lepší život díky lepšímu softwaru.

Jaký je tajný kód pro psaní rychlejšího spravovaného kódu?

Jen proto, že toho zvládnete více s menším úsilím, není licence na to, abyste se vzdali své odpovědnosti za kódování moudře. Nejdřív si to musíte přiznat sami sobě: "Jsem nováček." Jsi nováček. Jsem taky nováček. Všichni jsme holky v zemi spravovaného kódu. Všichni se pořád učíme provazy – včetně toho, co stojí.

Pokud jde o bohatou a pohodlnou platformu .NET Framework, je to, jako bychom byli děti v obchodě s candy. "Wow, nemusím dělat všechny ty nudné strncpy věci, můžu jen '+' řetězce dohromady! Wow, můžu načíst megabajt XML v několika řádcích kódu! Óó!"

Je to tak snadné. Tak snadné, opravdu. Takže snadno vypálit megabajty paměti RAM parsující xml infoset, jen aby se z nich vytáhlo několik prvků. V C nebo C++ to bylo tak bolestivé, že byste si to dvakrát rozmysleli, možná byste vytvořili stavový počítač na nějakém rozhraní API podobném SAX. S rozhraním .NET Framework stačí načíst celou informační sadu v jednom souboru. Možná to dokonce děláš znovu a znovu. Pak se vaše aplikace už nezdá být tak rychlá. Možná má pracovní sadu mnoha megabajtů. Možná jste si měli dvakrát rozmyslet, co tyto jednoduché metody stojí...

Podle mého názoru aktuální dokumentace k rozhraní .NET Framework bohužel dostatečně nevypisuje důsledky pro výkon typů a metod rozhraní – ani neurčí, které metody mohou vytvářet nové objekty. Modelování výkonu není snadným předmětem krytí nebo dokumentu; ale přesto, "nevědět" je pro nás mnohem obtížnější dělat informovaná rozhodnutí.

Vzhledem k tomu, že jsme tady všichni nováčci, a protože nevíme, co něco stojí, a protože náklady nejsou jasně zdokumentovány, co máme dělat?

Změřte ho. Tajemstvím je změřit abýt ostražitý. Všichni si budeme muset zvyknout měřit náklady na věci. Pokud se dostaneme k potížím s měřením nákladů, pak nebudeme těmi, kdo neúmyslně volají novou metodu, která stojí desetkrát víc, než jsme předpokládali .

(Mimochodem, pokud chcete získat hlubší přehled o výkonu, který je základem seznamu BCL (knihovny základních tříd) nebo samotné knihovny CLR, zvažte možnost podívat se na rozhraní příkazového řádku sdíleného zdroje, neboli Rotor. Kód rotoru sdílí pokrevní linii s rozhraním .NET Framework a CLR. Není to stejný kód, ale i tak vám slibuji, že promyšlená studie Rotoru vám poskytne nové poznatky o dějích pod pokličkou CLR. Nejprve si ale nezapomeňte projít licenci SSCLI.)

Znalosti

Pokud chcete být taxikár v Londýně, musíte nejprve získat Znalosti. 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 vyjdou na skútrech, aby se skautovali a posílili svou výuku knih.

Podobně pokud chcete být vysoce výkonným vývojářem spravovaného kódu, musíte získat znalosti spravovaného kódu. Musíte se dozvědět, jaké jsou náklady na provoz na nízké úrovni. Musíte se dozvědět, jaké funkce, jako jsou delegáti a zabezpečení přístupu kódu. Musíte se seznámit s náklady na typy a metody, které používáte, a těch, které píšete. A není na škodu zjistit, které metody můžou být pro vaši aplikaci příliš nákladné , a proto se jim vyhněte.

Znalosti nejsou v žádné knize, bohužel. Musíte se dostat na svůj skútr a prozkoumat – to znamená csc, ildasm, ladicí program VS.NET, CLR Profiler, váš profiler, některé časovače výkonu atd. a zjistit, co váš kód stojí v čase a prostoru.

Směrem k modelu nákladů pro spravovaný kód

Kromě předběžných předpokladů se podívejme na model nákladů pro spravovaný kód. Díky tomu se budete moct podívat na listovou metodu a na první pohled poznat, které výrazy a příkazy jsou nákladnější; a při psaní nového kódu budete moct dělat inteligentnější volby.

(Tím se neřeší tranzitivní 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ě většina nákladového modelu C/C++ stále platí pro spravovaný kód.

Jak je to možné? Znáte model spouštění CLR. Kód napíšete v jednom z několika jazyků. Kompilujete ho do formátu CIL (Common Intermediate Language) zabaleného do sestavení. Spustíte hlavní sestavení aplikace a spustí se CIL. Ale není to o řád pomalejší, jako bytecode interprety starých?

Kompilátor za běhu

Ne, není. ClR používá kompilátor JIT (za běhu) ke kompilaci každé metody v CIL do nativního kódu x86 a pak spustí nativní kód. I když je kompilace JIT každé metody při prvním zavolání malá prodleva, každá volaná metoda spouští čistě nativní kód bez interpretační režie.

Na rozdíl od tradičního off-line procesu kompilace C++ je čas strávený v kompilátoru JIT zpožděním "nástěnného času" v obličeji každého uživatele, takže kompilátor JIT nemá luxus vyčerpávajících průchodů optimalizace. I tak je seznam optimalizací, které kompilátor JIT provádí, působivý:

  • Konstantní skládání
  • Šíření konstant a kopírování
  • Běžná eliminace dílčího výrazu
  • Pohyb kódu invariantů smyčky
  • Vyřazení mrtvého úložiště a nedosažujícího kódu
  • Registrace přidělení
  • Vkládání metod
  • Odváření 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 odkazových typů. Hodnotové typy, 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 jsou v C/C++. Stejně jako u 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 neúnosně nákladná.

Odkazové a krabicové typy hodnot jsou v haldě. Jsou adresovány odkazy na objekty, které jsou jednoduše strojové ukazatele stejně jako ukazatele na objekty v C/C++.

Proto může být vychýcený spravovaný kód rychlý. S několika výjimkami, které probereme níže, pokud máte dobrý pocit z nákladů na nějaký výraz v nativním kódu jazyka C, nebudete daleko špatně modelovat jeho náklady jako ekvivalent ve spravovaném kódu.

Měl bych také zmínit nástroj NGEN, který "předem" kompiluje CIL do sestavení nativního kódu. Zatímco funkce NGEN sestavení v současné době nemá podstatný vliv (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 aplikačních domén a procesů. (Operační systém může sdílet jednu kopii kódu NGEN mezi všemi klienty; zatímco zachytěný kód se obvykle v současné době nesdílí napříč doménami nebo procesy AppDomains. Ale viz také LoaderOptimizationAttribute.MultiDomain.)

Automatická správa paměti

Nejvýznamnější odchylkou spravovaného kódu (od 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í, když se stanou nedostupnými. GC běží teď a znovu, často nepostřehnutelně, a obecně zastavuje vaši aplikaci jen na milisekundu nebo dva – někdy i déle.

Několik dalších článků popisuje důsledky uvolňování paměti na výkon a nebudeme je tady rekapitulaci. Pokud se vaše aplikace řídí doporučeními v těchto dalších článcích, celkové náklady na uvolňování paměti mohou být nevýznamné, několik procent doby provádění, konkurenceschopné s tradičními objekty C++ nebo vyšší než tradiční objekt new C++ a delete. Amortizované náklady na vytvoření a pozdější automatické uvolnění objektu jsou dostatečně nízké, abyste mohli vytvořit mnoho desítek milionů malých objektů za sekundu.

Přidělení objektů ale stále není zadarmo. Objekty zabírají místo. Alokace nekontrolovatých objektů vede k častějším cyklům uvolňování paměti.

Ještě horší je, že zbytečné uchovávání odkazů na nepotřebné grafy objektů je udržuje při životě. Někdy vidíme skromné programy s více než 100 MB pracovních sad, jejichž autoři popírají jejich vinu a místo toho přisuzují jejich nízký výkon nějakému záhadným, neidentifikovaným (a proto neřešitelným) problému se spravovaným kódem samotným. Je to tragické. Pak ale hodinová studie s profilerem CLR a změny na několik řádků kódu sníží využití haldy o deset nebo více. Pokud máte velký problém s pracovní sadou, prvním krokem je podívat se do zrcadla.

Nevytvářejte proto zbytečně objekty. Právě proto, že automatická správa paměti rozptyluje mnoho složitostí, potíží a chyb při přidělování a uvolnění objektů, protože je tak rychlá a pohodlná, máme přirozeně tendenci vytvářet další a další objekty, jako by rostly na stromech. Pokud chcete psát opravdu rychle spravovaný kód, vytvářejte objekty promyšleně a správně.

To platí i pro návrh rozhraní API. Je možné navrhnout typ a jeho metody tak, aby klienti museli vytvářet nové objekty s wild abandon. Nedělejte to.

Náklady ve spravovaném kódu

Teď se podívejme na časové náklady různých operací spravovaného kódu na nízké úrovni.

Tabulka 1 uvádí přibližné náklady na celou řadu operací spravovaného kódu na nízké úrovni v nanosekundách na klidovém počítači Pentium-III s Windows XP a .NET Framework v1.1 ("Everett") shromážděných se sadou jednoduchých časování smyček.

Testovací ovladač volá každou testovací metodu a určuje počet iterací, které se mají provést, a automaticky škáluje tak, aby iterovaly mezi 218 a 230 iteracemi, podle potřeby k provedení každého testu po dobu nejméně 50 ms. Obecně řečeno, to je dostatečně dlouhé na to, aby bylo vidět několik cyklů uvolňování paměti generace 0 v testu, který provádí intenzivní alokaci objektů. V tabulce jsou uvedeny výsledky s průměrem více než 10 pokusů a také nejlepší zkušební verze (minimální doba) pro každý testovací předmět.

Každá testovací smyčka se podle potřeby rozbalí 4 až 64krát, aby se snížila režie testovací smyčky. Zkontroloval(a) jsem nativní kód vygenerovaný pro každý test, aby se zajistilo, že kompilátor JIT ne optimalizuje test . V několika případech jsem například upravil test tak, aby během testovací smyčky a po jeho skončení zůstaly průběžné výsledky živé. Podobně jsem udělal změny, aby se zabránilo běžné subexpression eliminace v několika testech.

Tabulka 1 Primitivní časy (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 nový valtype L1 0.8 0.8 isinst up 1
1.0 1.0 Int add 4,6 4,6 nový valtype L2 0.8 0.8 isinst down 0
1.0 1.0 Int sub 6.4 6.4 nový valtype L3 6.3 6.3 Isinst down 1
2.7 2.7 Int mul 8.0 8.0 nový valtype L4 10,7 10.6 isinst (nahoru 2) dolů 1
35.9 35.7 Int div 23,0 22.9 nový valtype L5 6.4 6.4 isinst down 2
2.1 2.1 Int shift 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 get field
2.1 2.1 dlouhé předplatné 30.2 27.5 nový reftype L3 1.2 1.2 získat rekvizitu
34.2 34.1 dlouhá mula 34.1 30.8 nový typ L4 1.2 1.2 set field
50.1 50,0 dlouhé div 39.1 34,4 nový reftype L5 1.2 1.2 nastavit prop
5,1 5,1 dlouhá směna 22,3 20.3 new reftype empty 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 new reftype empty ctor L3 1.2 1.2 nastavit toto pole
2.0 2.0 float mul 34.7 30.7 new reftype empty ctor L4 1.2 1.2 nastavit tuto vlastnost
27.7 27.6 float div 38.5 34.3 new reftype empty ctor L5 6.4 6.3 získat virtuální prop
1.5 1.5 double add 22.9 20.7 new reftype ctor L1 6.4 6.3 nastavit virtuální prop
1.5 1.5 dvojitá sub 27.8 25.4 new reftype ctor L2 6.4 6.4 bariéra zápisu
2.1 2.0 dvojitá mula 32.7 29.9 new 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 new 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 unbox int
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 volání této 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 cast down 0 2.9 2,8 sum array 100000
6.6 6.5 volání rozhraní 8.9 8.8 odsadí 1 5.6 5.6 sum array 1000000
1.1 1.0 volání instance inst itf 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řetypování 2 6.1 6.1 sum list 10000
5.4 5.4 inst itf virtual call 8.7 8.6 přetypování 3 22,0 22,0 sum list 100000
5.4 5.4 toto virtuální volání ITF       21.5 21.4 sum list 1000000

Právní omezení: Neberte tato data příliš doslova. Testování času je plné nebezpečí neočekávaných efektů druhého řádu. Náhodná náhoda může umístit zamíchaný kód nebo nějaká důležitá data tak, aby překlenoval řádky mezipaměti, zasahoval do něčeho jiného nebo co jste udělali. Je to trochu jako princip nejistoty: časy a časové rozdíly 1 nanosekundy jsou na hranicích pozorovatelného.

Další právní omezení: Tato data jsou relevantní pouze pro scénáře s malým kódem a daty, které se zcela zapadají do mezipaměti. Pokud se "horké" části vaší aplikace nevejdou do mezipaměti na čipu, můžete mít jinou sadu problémů s výkonem. O mezipamětí ke konci dokumentu máme ještě mnohem víc co říct.

A ještě další právní omezení: jednou ze vznešených výhod dodání komponent a aplikací jako sestavení CIL je, že váš program může být automaticky rychlejší každou sekundu a každý rok rychlejší – "rychlejší každou sekundu", protože modul runtime může (teoreticky) přeladit kód zkompilovaný jit při spuštění programu; a "rychlejší celý rok", protože s každou novou vydanou verzí modulu runtime mohou lepší, chytřejší a rychlejší algoritmy získat nový bod pro optimalizaci vašeho kódu. Pokud se tedy některé z těchto časování v .NET 1.1 zdají být méně než optimální, mějte na srdci, že by se mělo zlepšit v následných verzích produktu. Z toho vyplývá, ž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.

Kromě toho se zřeknou, že data poskytují přiměřenou cit pro aktuální výkon různých primitiv. Čísla mají smysl a dokládají moje tvrzení, že většina zamotaných spravovaných kódů běží "blízko počítače" stejně jako kompilovaný nativní kód. Primitivní celočíselné a plovoucí operace jsou rychlé, volání metod různých druhů méně, ale (věřte mi) stále srovnatelné s nativní C/C++; a přesto vidíme, že některé operace, které jsou v nativním kódu obvykle levné (přetypování, úložiště polí a polí, ukazatele na funkce (delegáty)), jsou teď dražší. Proč? Uvidíme.

Aritmetické operace

Tabulka 2 – časy aritmetických operací (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 plovoucí mula
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 dlouhé předplatné 1.5 1.5 dvojitá sub
34.2 34.1 dlouhá mula 2.1 2.0 dvojitá mula
50.1 50,0 dlouhé div 27.7 27.6 double div
5,1 5,1 dlouhá směna      

Ve starých dobách byla matematika s plovoucí desetinnou čárkou možná o řád pomalejší než celočíselná matematika. Jak ukazuje tabulka 2, u moderních jednotek s plovoucí desetinou čárkou v kanálu se zdá, že je malý nebo žádný rozdíl. Je úžasné si myslet, že průměrný počítač s poznámkovým blokem je nyní počítač třídy gigaflop (pro problémy, které se vejdou do mezipaměti).

Pojďme se podívat na řádek zamíchaného kódu z celočíselného kódu a testů s plovoucí desetinou čárkou:

Demontáž 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] 

Tady vidíme, že kód s vychytávkovaným kódem je blízko optimálnímu. V tomto int add případě kompilátor dokonce zapregistroval pět místních proměnných. V případě plovoucího přidání jsem byl povinen vytvořit proměnné a prostřednictvím h statiky třídy, abych porazil běžné eliminace dílčích výrazů.

Volání metod

V této části prozkoumáme náklady a implementace volání metod. Předmět testu je třída T implementuje rozhraní Is různými druhy metod. Viz výpis 1.

Výpis 1 testovacích metod volání 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()  { } …}

Podívejme se na tabulku 3. Zdá se, že při první aproximaci je metoda buď vložená (abstrakce nic nestojí) nebo ne (abstrakce stojí >5krát celočíselnou operaci). Nezdá se, že by byl výrazný rozdíl v nezpracovaných nákladech statického volání, volání instance, virtuálního volání nebo volání rozhraní.

Table 3 – časy volání metod (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 volání instance inst itf 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 volání této 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 nereprezentativním nejlepším případem, vliv běžících těsných časových smyček miliónkrát. V těchto testovacích případech jsou weby volání virtuálních metod a metod rozhraní monomorfní (např. cílová metoda na lokalitu volání se v průběhu času nemění), takže kombinace mechanismů volání virtuální metody a metody rozhraní (ukazatele a položky mapové tabulky metody a rozhraní) a úžasně provident branch predikce umožňuje procesoru provést nerealisticky efektivní úlohu volání prostřednictvím těchto jinak obtížně předvídatelných, větve závislé na datech. V praxi může a bude zpomalovat virtuální volání a volání rozhraní v desítkách cyklů kvůli chybějícím datům v mezipaměti dat nebo chybné předvolání větve (ať už jde o povinnou chybějící kapacitu nebo polymorfní lokalitu volání).

Pojďme se podrobněji podívat na každou z těchto časů volání metody.

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í, načasujeme časování prázdné smyčky.

Abychom změřili přibližné náklady na volání statické metody, nastavíme statické metody s1() atd. tak velké, že nejsou výhodné pro vložení do volajícího.

Všimněte si, že musíme dokonce použít explicitní proměnnou falsePredpredikátu false . Kdybychom napsali

static void s1() { if (false) dummy(1, 2, 3, …, 16); }

kompilátor JIT by vyřadil 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 doby volání musí být přiřazeny (false) predikát testu a přeskočit v rámci volal statická metoda s1. (Mimochodem, lepší způsob, jak zakázat vkládání, je atribut . CompilerServices.MethodImpl(MethodImplOptions.NoInlining) )

Stejný přístup byl použit pro volání vložené instance a běžné časování volání instance. Vzhledem k tomu, že specifikace jazyka C# zajišťuje, že každé volání na objekt null odkaz vyvolá Výjimku NullReferenceException, každý web volání musí zajistit, aby instance nebyla null. To se provádí dereferencováním odkazu na instanci; Pokud je hodnota null, vygeneruje chybu, která se změní na tuto výjimku.

V části Disassembly 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 vytáhl kontrolu instance null ze smyčky.

Disassembly 2 Instance method call site with null instance "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 volání vložené této instance a volání této instance jsou stejné, s výjimkou instance je this; zde byla vyvolána kontrola hodnoty null.

Zpětný překlad 3 Tato instance metody volání webu

               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 tabulku jeho základního typu a jakékoli přepsání virtuální metody nahradí 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í vyvolá volání virtuální metody v porovnání s voláním instance dvě další načtení: jedno pro načtení adresy tabulky metody (vždy se nachází na adrese *(this+0)) a druhé pro načtení příslušné adresy virtuální metody z tabulky metod a volání. Viz Demontáž 4.

Zpětný překlad 4 Místo volání virtuální metody

               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 dostáváme k volání metod rozhraní (Disassembly 5). V jazyce C++ 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. K odeslání metody rozhraní vyhledáme tabulku metody, její mapu rozhraní, položku rozhraní v tomto mapování a pak zavoláme nepřímo prostřednictvím příslušné položky v oddílu rozhraní tabulky metody.

Disassembly 5 Interface call site

               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

Zbytek primitivních časování, inst itf volání instance, toto volání instance itf, inst itf virtuální volání, toto itf virtuální volání zdůrazňují myšlenku, že pokaždé, když metoda odvozeného typu implementuje metodu rozhraní, zůstává volatelná prostřednictvím webu volání metody instance.

Například pro test tohoto itf volání instance, volání na implementaci metody rozhraní prostřednictvím instance (ne rozhraní) odkaz, metoda rozhraní je úspěšně vložena a náklady jdou na 0 ns. Dokonce i implementace metody rozhraní je potenciálně vložená, když ji zavoláte jako metodu instance.

Volání metod, které ještě mají být jitted

Pro statická volání a volání metod instance (ale ne volání virtuálních metod a metod rozhraní) kompilátor JIT v současné době generuje různé sekvence volání metod v závislosti na tom, zda cílová metoda již byla zamíchaná v okamžiku, kdy je zamíchaná lokalita volání.

Pokud volaný (cílová metoda) ještě nebyl jitted, kompilátor vygeneruje volání nepřímé prostřednictvím ukazatele, který je nejprve inicializován pomocí "prejit stub". První volání cílové metody přichází na zástupný kód, který aktivuje kompilaci JIT metody, vygeneruje nativní kód a aktualizuje ukazatel na adresu nového nativního kódu.

Pokud již byl volaný volaný, jeho adresa nativního kódu je známa, takže kompilátor vygeneruje 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 objektu.

Pro odkazové typy jsou objekty přiděleny na haldě uvolňování paměti. U hodnotových typů, ať už se jedná o rezidentní zásobník nebo vložený do jiného typu odkazu nebo typu hodnoty, se objekt typu hodnoty nachází v určitém konstantním posunu od nadřazené struktury – není vyžadováno přidělení.

U typických malých objektů odkazového typu 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ů, jsou živé objekty z haldy generace 0 zkomprimovány a povýšeny na generaci 1, a tak má přidělování paměti pěknou velkou souvislou volnou paměť arénu pro práci s. U většiny objektů dochází pouze ke zvýšení počtu ukazatelů a kontrole hranic, což je levnější než obvyklý bezplatný alokátor seznamů C/C++ (malloc/operator new). Systém uvolňování paměti dokonce bere v úvahu velikost mezipaměti vašeho počítače a snaží se udržet objekty gen 0 na rychlém místě v hierarchii mezipaměti/paměti.

Vzhledem k tomu, že upřednostňovaným stylem spravovaného kódu je přidělení většiny objektů s krátkou životností a jejich rychlé uvolnění, zahrneme také (do časových nákladů) amortizované náklady na uvolňování paměti těchto nových objektů.

Všimněte si, že systém uvolňování paměti nestrání žádný čas truchlením nad mrtvými předměty. Pokud je objekt mrtvý, GC ho nevidí, nechodí po něm, nedává na něj nanosekundu. GC se zabývá pouze blaho života.

(Výjimka: Finalizovatelné mrtvé objekty jsou zvláštní případ. GC je sleduje a speciálně podporuje nedokončící objekty na další generaci čekající na dokončení. To je nákladné a v nejhorším případě může přechodně zvýšit úroveň grafů velkých neaktivních objektů. Proto nedělejte objekty finalizovatelné, pokud to není nezbytně nutné; a pokud je to nutné, zvažte použití vzoru Dispose a volání GC.SuppressFinalizer , pokud je to možné.) Pokud to vaše Finalize metoda nevyžaduje, neuchovávejte odkazy z finalizovatelného objektu na jiné objekty.

Amortizované náklady na uvolňování paměti velkého krátkodobého objektu jsou samozřejmě vyšší než náklady na malý krátkodobý objekt. Každé přidělení objektu nás o hodně blíže k dalšímu cyklu uvolňování paměti; Větší objekty tak udělají mnohem dříve než ty malé. Dříve (nebo později) přijde okamžik zúčtování. Cykly uvolňování paměti, zejména kolekce generace 0, jsou velmi rychlé, ale nejsou volné, i když je velká většina nových objektů neaktivní: pokud chcete najít (označit) živé objekty, je nejprve nutné pozastavit vlákna a pak procházet zásobníky a další datové struktury, aby se shromáždily odkazy na kořenové objekty do haldy.

(Možná významnější je, že menší počet větších objektů se vejde do stejné velikosti mezipaměti jako menší objekty. Efekty zmeškané v mezipaměti můžou snadno převládat efekty délky cesty kódu.)

Jakmile je pro objekt přidělen prostor, zbývá ho inicializovat (sestavit). CLR zaručuje, že všechny odkazy na objekt jsou předicializovány na hodnotu null a všechny primitivní skalární typy jsou inicializovány na hodnotu 0, 0.0, false atd. (Proto není nutné provádět redundantní v uživatelem definovaných konstruktorech. Nebojte se, samozřejmě. Mějte ale na paměti, že kompilátor JIT v současné době nemusí nutně optimalizovat vaše redundantní úložiště.)

Kromě vynulování polí instancí clr inicializuje (pouze typy odkazů) interní implementační pole objektu: ukazatel tabulky metody a slovo záhlaví objektu, které předchází ukazatel tabulky metody. Pole také získají pole Délka a pole objektů získají pole délka a typ prvku.

ClR pak zavolá konstruktor objektu, pokud existuje. Konstruktor každého typu, ať už definovaný uživatelem nebo vygenerovaný kompilátorem, nejprve zavolá konstruktor základního typu a pak spustí uživatelem definovanou inicializaci, 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 C rozšiřuje B rozšiřuje A (rozšiřuje System.Object), pak inicializace E vždy způsobí pět volání metody. V praxi to není tak špatné, protože kompilátor inlines pryč (do prázdna) volá prázdné konstruktory základního typu.

S odkazem na první sloupec tabulky 4 si všimněte, že můžeme vytvořit a inicializovat strukturu D se čtyřmi poli int za přibližně 8 int-add-times. Disassembly 6 je vygenerovaný kód ze tří různých časových smyček, které vytvářejí A, C a E. (V každé smyčce upravíme každou novou instanci, což brání kompilátoru JIT v optimalizaci všeho.)

Tabulka 4 Časy vytvoření objektu typu hodnoty a odkazu (ns)

Průměr Min Primitivní Průměr Min Primitivní Průměr Min Primitivní
2,6 2,6 nový valtype L1 22,0 20.3 nový reftype L1 22.9 20.7 new rt ctor L1
4,6 4,6 nový valtype L2 26.1 23.9 nový reftype L2 27.8 25.4 new rt ctor L2
6.4 6.4 nový valtype L3 30.2 27.5 nový reftype L3 32.7 29.9 new rt ctor L3
8.0 8.0 nový valtype L4 34.1 30.8 nový reftype L4 37.7 34.1 new rt ctor L4
23,0 22.9 nový 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áž 6 Konstrukce objektu typu hodnota

               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 časování (nový reftype L1, ... nové reftype L5) jsou pro pět úrovní dědičnosti odkazových typů A, ..., E, bez uživatelem definovaných konstruktorů:

    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ání časů referenčního typu s časy typu hodnoty vidíme, že amortizované přidělení a náklady na uvolnění každé instance jsou na testovacím počítači přibližně 20 ns (20krát více času přidání). 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í, alokace a kolekce zaúčtují pouze polovinu času vytváření objektu. Viz Demontáž 7.

Demontáž 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 scénáře konstrukce zděděné třídy.

  1. 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ý kód.

  2. New rt ctor L1, ..., new rt ctor L5: Každý typ A, ..., E má uživatelem definovaný konstruktor, který nastaví proměnnou instance na hodnotu 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 volání konstruktoru základní třídy do new webu. (Demontáž 8).

Demontáž 8 Hluboce vložené zděděné 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 
  1. New rt no-inl L1, ..., new rt no-inl L5: Každý typ A, ..., má uživatelem definovaný konstruktor, který byl záměrně napsán tak, E aby byl příliš nákladný na vložený. Tento scénář simuluje náklady na vytváření složitých objektů pomocí hierarchií hluboké dědičnosti a opožděných konstruktorů.

      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ů.

Mezihra: Ukázka profileru CLR

Nyní pro rychlou ukázku modulu CLR Profiler. ClR Profiler, dříve označovaný jako alokační profiler, používá rozhraní API pro profilaci CLR ke shromažďování dat událostí, zejména událostí volání, vrácení a přidělení objektů a uvolňování paměti při spuštění vaší aplikace. (ClR Profiler je "invazní" profiler, což znamená, že profilovanou aplikaci bohužel výrazně zpomaluje.) Po shromáždění událostí můžete pomocí modulu CLR Profiler prozkoumat přidělování paměti a chování uvolňování paměti vaší aplikace, včetně interakce mezi hierarchickým grafem volání a vzory přidělování paměti.

ClR Profiler stojí za to se naučit, protože pro mnoho aplikací spravovaného kódu s problémy s výkonem poskytuje porozumění profilu přidělení dat důležité přehledy potřebné ke snížení pracovní sady, a tak poskytovat rychlé a náročné komponenty a aplikace.

Modul CLR Profiler také může odhalit, které metody přidělují více úložiště, než jste očekávali, a může odhalit případy, kdy neúmyslně uchováváte odkazy na nepotřebné grafy objektů, které by jinak mohly být uvolňováním paměti uvolněny. (Běžným vzorem návrhu problémů je softwarová mezipaměť nebo vyhledávací tabulka položek, které už nejsou potřeba nebo je lze bezpečně rekonstituovat později. Je to tragické, když mezipaměť udržuje grafy objektů naživu po jejich životnosti. Místo toho nezapomeňte vynulovat odkazy na objekty, které už nepotřebujete.)

Obrázek 1 je zobrazení časové osy haldy během provádění ovladače testu časování. Model sawtooth označuje přidělení mnoha tisíc instancí objektů C (purpurová), D (fialová) a E (modrá). Každých pár milisekund v nové haldě objektu (generace 0) ožvýkáme dalších přibližně 150 kB paměti RAM a systém uvolňování paměti spustí krátce, aby ji recykloval a povýšil všechny živé objekty na gen 1. generace. Je pozoruhodné, že i v tomto invazním (pomalém) profilačním prostředí v intervalu 100 ms (2,8 s až 2,9s) procházíme cykly GC přibližně 8 generace 0. Pak ve 2,977 s vytvoří místo pro další E příklad, systém uvolňování paměti provede uvolňování paměti generace 1, který shromažďuje a komprimuje haldu 1. generace – a tak pila pokračuje z nižší počáteční adresy.

Obrázek 1 Zobrazení časové osy 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 generace 0 zaplní a tím častější je cyklus uvolňování paměti.

Přetypování a kontroly typů instancí

Základem bezpečného, zabezpečeného a ověřitelného spravovaného kódu je bezpečnost typu. Pokud by bylo možné přetypovat objekt na typ, který není, bylo by jednoduché ohrozit integritu CLR a nechat ho tak napospas nedůvěryhodnému kódu.

Tabulka 5 Přetypování a časy (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řetypování 0 0.8 0.8 isinst down 0
8.9 8.8 přehozen 1 6.3 6.3 isinst down 1
9.8 9.7 přetypování (nahoru 2) dolů 1 10,7 10.6 isinst (nahoru 2) dolů 1
8.9 8.8 přetypování dolů 2 6.4 6.4 isinst down 2
8.7 8.6 přetypování 3 6.1 6.1 isinst down 3

Tabulka 5 ukazuje režijní náklady na tyto povinné kontroly typů. Přetypování z 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 zkontrolováno.

Přetypování (zaškrtnuté) převede odkaz na objekt na cílový typ nebo vyvolá InvalidCastException.

Naproti tomu isinst instrukce CIL se používá k implementaci klíčového slova jazyka C# as :

bac = ac as B;

Pokud ac není B nebo je odvozen z B, výsledek je null, není výjimkou.

Výpis 2 ukazuje jednu ze smyček časování přetypování a Disassembly 9 zobrazuje vygenerovaný kód pro jedno přetypování na odvozený typ. K provedení přetypování kompilátor vygeneruje přímé volání pomocné rutiny.

Výpis smyčky 2 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;
    }
}

Demontáž 9 odlitek dolů

               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í dvojice metod, getter vlastností a setter vlastností, které se chovají 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 používáte vlastnost k jednoduchému načtení nebo uložení pole instance, je obvykle vložená, 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 není vlastnost deklarována jako virtuální. V takovém případě jsou náklady přibližně stejné jako náklady na volání virtuální metody. Žádné překvapení.

Časy polí a vlastností tabulky 6 (ns)

Průměr Min Primitivní
1.0 1.0 získat pole
1.2 1.2 získat rekvizitu
1.2 1.2 set field
1.2 1.2 nastavit prop
6.4 6.3 získat virtuální prop
6.4 6.3 nastavit virtuální prop

Překážky zápisu

Systém uvolňování paměti CLR využívá výhod "generační hypotézy" – většina nových objektů zemře mladá – k minimalizaci režie shromažďování.

Halda je logicky rozdělená do generací. Nejnovější objekty jsou v generaci 0 (gen 0). Tyto objekty ještě nepřežily kolekci. Během kolekce gen 0 GC určuje, které objekty generace 0 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ě dostupné 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, je omezení rozsahu trasování grafu objektů GC na haldu gen 0 optimalizace nezbytná pro dosažení velmi krátkých časů pozastavení kolekce CLR.

Je však možné uložit odkaz na objekt gen 0 v referenčním poli objektu gen 1 nebo gen2. Vzhledem k tomu, že během kolekce gen 0 neskenujeme objekty gen 1 nebo gen2, pokud je to jediný odkaz na daný objekt gen 0, může být objekt GC chybně uvolněn. Nemůžeme dopustit, aby se to stalo!

Místo toho všechna úložiště do všech polí odkazu na objekt v haldě za následek překážku 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 na objekt jsou přidána do kořenové sady GC následných GC.

Režie za bariéru zápisu úložiště pro jednotlivé objekty odkazy na pole je srovnatelná s náklady na volání jednoduché metody (tabulka 7). Jedná se o nový náklad, který není k dispozici v nativním kódu C/C++, ale obvykle je to malá cena za superrychlé přidělení objektů a uvolňování paměti a mnoho výhod produktivity automatické správy paměti.

Tabulka 7 Doba bariéry zápisu (ns)

Průměr Min Primitivní
6.4 6.4 bariéra zápisu

Překážky zápisu mohou být v těsných vnitřních smyčkách nákladné. V nadcházejících letech se ale můžeme těšit na pokročilé techniky kompilace, které snižují počet překážek pro zápis a celkové amortizované náklady.

Možná si myslíte, že překážky zápisu jsou nezbytné pouze v úložištích pro referenční pole objektů odkazových typů. V rámci metody typu hodnoty jsou však úložiště do polí odkazu na objekt (pokud existují) chráněna také 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 elementům pole

Aby bylo možné diagnostikovat a zabránit chybám polí mimo rozsah a poškození haldy a k ochraně integrity samotného modulu CLR, jsou zatížení prvků pole a úložiště kontrolovány hranice, čímž se zajistí, že index je v intervalu [0,pole. Length-1] včetně nebo házení IndexOutOfRangeException.

Naše testy měří čas načtení nebo uložení prvků int[] pole a A[] pole. (tabulka 8).

Tabulka 8 Časy přístupu k poli (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 hranic vyžaduje porovnání indexu pole s implicitním polem. Pole Délka Jak ukazuje Disassembly 10, v pouhých dvou pokynech zkontrolujeme, že index není menší než 0 ani větší než nebo roven poli. Délka – pokud je, vytvoříme větev na sekvenci mimo řádek, která vyvolá výjimku. Totéž platí pro zatížení elementů pole objektů a pro úložiště do polí int a jiných jednoduchých typů hodnot. (Load obj pole elem čas je (nevýznamně) pomalejší kvůli mírnému rozdílu v jeho vnitřní smyčce.)

Disassembly 10 Load int array element

                          ; 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 kontroly redundantních hranic.

Když si vzpomeneme na předchozí části, můžeme očekávat, že úložiště prvků pole objektů budou výrazně dražší. Pokud chcete uložit odkaz na objekt do pole odkazů na objekty, modul runtime musí:

  1. kontrola indexu pole je v mezích;
  2. check objekt je instance typu prvku pole;
  3. provádět bariéru zápisu (zapisování mezigeneračních odkazů na objekt z pole na objekt).

Tato sekvence kódu je poměrně dlouhá. Místo toho, aby ji vygeneroval v každém úložišti pole 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 akce zaúčtují 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

Zabalení a rozbalení

Partnerství mezi kompilátory .NET a CLR umožňuje, aby se hodnotové typy, včetně primitivních typů, jako je int (System.Int32), účastnily, jako by šlo o referenční typy – a byly adresovány jako odkazy na objekty. Tato cenová dostupnost – tento syntaktický cukr – umožňuje, aby se hodnotové typy předávaly metodám jako objekty, ukládaly se v kolekcích jako objekty atd.

"box" typ hodnoty je vytvoření objektu 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.

"Unbox" typ hodnoty v rámečku je zkopírování hodnoty z objektu do nové instance typu hodnoty.

Jak ukazuje tabulka 9 (v porovnání s tabulkou 4), amortizovaný čas potřebný k balení int a později k jeho uvolnění do koše je srovnatelný s časem potřebným k vytvoření instance malé třídy s jedním int polem.

Tabulka 9 Box a Unbox int Times (ns)

Průměr Min Primitivní
29.0 21.6 box int
3,0 3,0 unbox int

K rozbalení objektu int v rámečku je potřeba explicitní přetypování na int. To se zkompiluje do porovnání typu objektu (reprezentovaného adresou tabulky metody) a adresy tabulky metody int v rámečku. Pokud jsou stejné, hodnota se zkopíruje z objektu . V opačném případě dojde k výjimce. Viz Demontáž 12.

Demontáž 12 Box a unbox int

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

Delegáti

V jazyce C je ukazatel na funkci primitivní datový typ, který doslova ukládá adresu funkce.

Jazyk C++ přidává ukazatele na členské funkce. Ukazatel na členovou funkci (PMF) představuje volání odložené členské funkce. Adresa ne virtuální členské funkce může být jednoduchá adresa kódu, ale adresa virtuální členské funkce musí ztělesňovat konkrétní volání virtuální členské funkce – dereference takového SOUBORU PMF je volání virtuální funkce.

Pokud chcete přes odkazovat na C++ PMF, musíte zadat instanci:

    A* pa = new A;
    void (A::*pmf)() = &A::af;
    (pa->*pmf)();

Před lety jsme se ve vývojovém týmu kompilátoru Visual C++ ptali sami sebe, co je to za pa->*pmf bestii (operátor volání funkce sans)? Nazvali jsme ho vázaným ukazatelem na členovou funkci , ale stejně tak je i volání latentní členské funkce .

Při návratu do spravovaného kódu je objekt delegáta právě to – latentní volání metody. Objekt delegáta představuje jak metodu volání, tak instanci, na které se má volat , nebo pro delegáta statické metody, 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áty jsou zhruba podobné ukazatelům na funkce v jazyce C++; delegáti jsou však typově bezpečné a zabezpečené.)

Typy delegátů v jazyce C# jsou odvozené typy funkce MulticastDelegate. Tento typ poskytuje bohatou sémantiku, včetně možnosti vytvořit seznam vyvolání párů (objekt,metoda), které se mají vyvolat při vyvolání delegáta.

Delegáti také poskytují zařízení pro asynchronní volání metody. Po definování typu delegáta a vytvoření instance typu inicializovaného s voláním latentní metody ho můžete vyvolat synchronně (syntaxe volání metody) nebo asynchronně přes BeginInvoke. Pokud BeginInvoke je volána, modul runtime zařadí volání do fronty a okamžitě se vrátí volajícímu. Cílová metoda je volána později ve vlákně fondu vláken.

Všechny tyto bohaté sémantiky nejsou levné. Při porovnání tabulky 10 a tabulky 3 si všimněte, že volání delegáta je ** přibližně osmkrát pomalejší než volání metody. Očekávejte, že se to časem zlepší.

Tabulka 10 Čas vyvolání delegáta (ns)

Průměr Min Primitivní
41.1 40.9 vyvolání delegáta

Neúspěšných přístupů do mezipaměti, chyb stránky a architektury počítače

V "starých dobrých časech", kolem roku 1983, byly procesory pomalé (přibližně 0,5 milionu instrukcí/s) a relativně řečeno, paměť RAM byla dostatečně rychlá, ale malá (přístupové doby přibližně 300 ns na 256 kB paměti DRAM) a disky byly pomalé a velké (přístupové doby přibližně 25 ms na 10 MB discích). Pc mikroprocesory byly skalární CIC, většina plovoucí desetiny byla v softwaru a nebyly k dispozici žádné mezipaměti.

Po dalších dvaceti letech Moorova zákona, kolem roku 2003, jsou procesory rychlé (až tři operace za cyklus při 3 GHz), paměť RAM je relativně velmi pomalá (přístupová doba přibližně 100 ns při 512 MB PAMĚTI DRAM) a disky jsou velmi pomalé a obrovské (přístupové časy přibližně 10 ms na 100GB discích). Mikroprocesory počítačů jsou nyní mimo provoz superskalární hyperthreadingové mezipaměti trasování mezipaměti (spouští dekódované instrukce CISC) a existuje několik vrstev mezipamětí – například určitý serverově orientovaný mikroprocesor má mezipaměť dat 32 kB úrovně 1 (možná 2 cykly latence), 512 kB mezipaměti dat L2 a 2 MB datové mezipaměti L3 (možná tucet cyklů latence), vše na čipu.

Za starých dobrých časů jste mohli (a někdy i) spočítat bajty kódu, který jste napsali, a počet cyklů, které kód potřeboval ke spuštění. Zatížení nebo úložiště trvalo přibližně stejný počet cyklů jako při přidání. Moderní procesor používá predikci větví, spekulaci a provádění mimo pořadí (tok dat) napříč několika jednotkami funkcí, aby zjistil paralelismus na úrovni instrukcí, a tak postupujte na několika frontách najednou.

Naše nejrychlejší počítače teď můžou provádět až 9 000 operací za mikrosekundu, ale ve stejné mikrosekundě se načítají nebo ukládají do paměti DRAM ~10 řádků mezipaměti. V počítačích architektury se to označuje jako zásah do zdi paměti. Mezipaměti skrývají latenci paměti, ale pouze k určitému bodu. Pokud se kód nebo data nevejdou do mezipaměti a/nebo vykazují špatnou referenční lokalitu, náš nadzvukový proud 9000 operací za mikrosekundu degeneruje na 10 zátěžových za mikrosekundový tříkolek.

A (nedovolte, aby se to stalo vám) pokud pracovní sada programu překročí dostupnou fyzickou paměť RAM a program začne přijímat chyby pevné stránky, pak v každé 10 000-mikrosekundové službě selhání stránky (přístup k disku) promeškáme příležitost přiblížit uživateli až 90 milionů operací blíže k jeho odpovědi. To je tak hrozné, že věřím, že se od tohoto dne postaráte o měření pracovní sady (vadump) a použití nástrojů jako CLR Profiler k odstranění zbytečných přidělení a neúmyslného uchovávání objektů grafů.

Ale co to všechno má společného se znalostmi nákladů na primitiva spravovaného kódu?Všechno*.*

Když si vzpomeneme na 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ělení, inicializaci a uvolnění pěti objektů pole s pěti úrovněmi explicitních volání konstruktoru, jsou rychlejší než jeden přístup DRAM. Jen jedno zatížení, které vynechá všechny úrovně mezipaměti na čipu, může trvat déle než téměř jakákoli operace s jedním spravovaným kódem.

Pokud vás tedy zajímá rychlost kódu, je nutné při návrhu a implementaci algoritmů a datových struktur zvážit a měřit hierarchii mezipaměti a paměti.

Čas na jednoduchou ukázku: Je rychlejší sečíst pole int nebo sečíst ekvivalentní propojený seznam int? Kolik a proč?

Chvilku si to rozmyslete. U malých položek, jako jsou ints, je využití paměti na prvek pole jedna čtvrtina oproti propojenému seznamu. (Každý uzel propojeného seznamu má dvě slova o režii objektu a dvě slova polí (další odkaz a položka int).) To bude bolet využití mezipaměti. Vyhodnotte jeden pro přístup k poli.

Při procházení polí ale může dojít ke kontrole mezí pole pro jednotlivé položky. Právě jste viděli, že kontrola hranic chvíli trvá. Možná to vychytává měřítka ve prospěch propojeného seznamu?

Demontáž 13 Součet int matice versus součet int propojený 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 naskládal balíček ve prospěch propojeného seznamu procházení, rozbalit ho čtyřikrát, dokonce i odstranit obvyklý nulový ukazatel konec-of-seznam kontrola. Každá položka v maticové smyčce 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 je teď podle vás rychlejší?

Testovací podmínky: Nejprve vytvořte pole jednoho milionu int a jednoduchý tradiční propojený seznam jednoho milionu int (1 M uzlů seznamu). Pak dejte určitou dobu, jak dlouho trvá sečíst prvních 1 000, 10 000, 100 000 a 1 000 000 položek. Opakujte každou smyčku mnohokrát, abyste pro každý případ změřili chování mezipaměti, které nejvíce lichotivá.

Co je rychlejší? Až uhodnete, podívejte se na odpovědi: posledních osm položek v tabulce 1.

Zajímavé! Časy se výrazně zkrátí s tím, jak se odkazovaná data zvětší, než jsou po sobě jdoucí velikosti mezipaměti. Verze pole je vždy rychlejší než verze propojeného seznamu, i když provádí dvakrát tolik instrukcí. u 100 000 položek je maticová verze sedmkrát rychlejší!

Proč tomu tak je? Za prvé, méně propojených položek seznamu se vejde do libovolné úrovně mezipaměti. Všechna tato záhlaví a odkazy objektů plýtvou místem. Zadruhé, náš moderní procesor toku dat mimo pořadí může potenciálně přiblížit dopředu a provádět pokroky u několika položek v poli současně. Naproti tomu s propojeným seznamem, dokud není aktuální uzel seznamu v mezipaměti, nemůže procesor začít načítat další odkaz na uzel.

V případě 100 000 položek procesor utrácí (v průměru) přibližně (22-3,5)/22 = 84 % času, kdy se pohybuje palcem a č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 procházíme seznamem v pořadí přidělení a protože uvolňování paměti zachovává pořadí přidělení, i když komprimuje mrtvé objekty z haldy, je pravděpodobné, že po načtení jednoho uzlu na řádku mezipaměti je nyní 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, pak každý navštívený uzel může být zcela chybějící v mezipaměti. Přidání 16 bajtů do každého uzlu seznamu zdvojnásobí dobu 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 ta lekce nabídku? Chcete se vyhnout propojeným seznamům 100 000 uzlů? Ne. Z toho plyne, že efekty mezipaměti můžou dominovat všem aspektům nízké úrovně efektivity spravovaného kódu oproti nativnímu kódu. Pokud píšete spravovaný kód s kritickým výkonem, zejména kód spravující velké datové struktury, mějte na paměti efekty mezipaměti, promyslete si vzory přístupu k datovým strukturám a snažte se o menší stopy dat a dobrou lokalitu referencí.

Mimochodem, trendem je, že paměťová stěna, poměr doby přístupu k DRAM dělený časem operace procesoru, se bude v průběhu času dál zhoršovat.

Tady jsou některá pravidla návrhu s vědomím mezipaměti:

  • Experimentujte se svými scénáři a změřte je, protože je těžké předpovědět účinky druhého řádu a protože za papír, na který jsou vytištěná, nestojí žádná pravidla.
  • Některé datové struktury, například pole, využívají implicitní sousedství k reprezentaci vztahu mezi daty. Jiné, například propojené seznamy, používají explicitní ukazatele (odkazy) k reprezentaci relace. Implicitní sousedství je obecně vhodnější – "implicitnost" šetří místo ve srovnání s ukazateli; a sousedství poskytuje stabilní umístění odkazu a může procesoru umožnit zahájit další práci před spuštěním dalšího ukazatele.
  • Některé vzorce použití upřednostňují hybridní struktury – seznamy malých polí, pole polí nebo B-stromy.
  • Možná by se teď měly recyklovat algoritmy plánování citlivé na přístup k disku, které byly navrženy v případě, že přístupy na disk stojí pouze 50 000 instrukcí procesoru, protože přístupy DRAM můžou trvat tisíce operací procesoru.
  • Vzhledem k tomu, že uvolňování paměti clR mark-and-compact zachovává relativní pořadí objektů, objekty přidělené společně v čase (a ve stejném vlákně) mají tendenci zůstat pohromadě v prostoru. Tento jev můžete použít k promyšlené kolokaci cliquish dat na běžných linkách mezipaměti.
  • Možná budete chtít rozdělit data na horké části, 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".

Experimenty s časem pro sebe

Pro měření časování v tomto dokumentu jsem použil čítač QueryPerformanceCounter výkonu Win32 s vysokým rozlišením (a QueryPerformanceFrequency).

Snadno se volají prostřednictvím P/Invoke:

    [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);

Zavoláte QueryPerformanceCounter těsně před a hned za smyčkou časování, odečtete počty, vynásobíte číslem 1,0e9, vydělíte frekvencí, vydělíte počtem iterací a to je váš přibližný čas na iteraci v ns.

Kvůli prostorovým a časovým omezením jsme se nezabírali zamykáním, zpracováním výjimek ani systémem zabezpečení přístupu kódu. Představte si to jako cvičení pro čtenáře.

Mimochodem, v tomto článku jsem vygeneroval demontáže pomocí okna pro demontáž v VS.NET 2003. Má to ale trik. Pokud aplikaci spustíte v ladicího 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 získat náhled optimalizovaného nativního kódu generovaného kompilátorem JIT, bylo spustit testovací aplikaci mimo ladicí program a pak se k ní připojit pomocí Debug.Processes.Attach.

Model prostorových nákladů?

Je ironií, že úvahy o prostoru brání důkladné diskuzi o prostoru. Takže pár krátkých odstavců.

Aspekty nízké úrovně (některé jsou C# (výchozí TypeAttributes.SequentialLayout) a x86 specifické):

  • Velikost typu hodnoty je obecně celková velikost jeho polí, přičemž 4 bajtová nebo menší pole jsou zarovnaná s jejich přirozenými hranicemi.
  • K implementaci sjednocení je možné použít [StructLayout(LayoutKind.Explicit)] atributy a [FieldOffset(n)] .
  • Velikost referenčního typu je 8 bajtů plus celková velikost jeho polí zaokrouhlených nahoru na další 4 bajtovou hranici a se 4 bajtovými nebo menšími poli zarovnanými k jejich přirozeným hranicím.
  • V jazyce C# můžou deklarace výčtu určovat libovolný celočíselný základní typ (s výjimkou znaku char), takže je možné definovat 8bitové, 16bitové, 32bitové a 64bitové výčty.
  • Stejně jako v C/C++ můžete často oholit několik desítek procent místa od většího objektu správnou velikostí integrálních polí.
  • Velikost přiděleného typu odkazu můžete zkontrolovat pomocí modulu CLR Profiler.
  • Velké objekty (mnoho desítek a více kB) se spravují v samostatné haldě velkých objektů, aby se zabránilo nákladnému kopírování.
  • Finalizovatelné objekty využívají k uvolnění další generace uvolňování paměti – používejte je střídmě a zvažte použití vzoru Dispose.

Důležité informace:

  • Každá doména AppDomain v současné době nese značné režijní náklady na místo. Mnoho struktur modulu runtime a framework se nesdílí mezi doménami AppDomains.
  • V rámci procesu se vysoučený kód obvykle nesdílí mezi doménami AppDomains. Pokud je modul runtime speciálně hostovaný, je možné toto chování přepsat. Informace o příznaku a najdete v STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN dokumentaciCorBindToRuntimeEx.
  • V každém případě se vysoučený kód nesdílí mezi procesy. Pokud máte komponentu, která bude načtena do mnoha procesů, zvažte předkompilování pomocí nástroje NGEN ke sdílení nativního kódu.

Reflexe

Bylo řečeno, že "pokud se musíte zeptat, jaké náklady reflexe, nemůžete si to dovolit". Pokud jste si to přečetli, víte, jak důležité je ptát se, co stojí, a měřit tyto náklady.

Reflexe je užitečná a výkonná, ale ve srovnání s jitkovaným nativním kódem není ani rychlá, ani malá. Byli jste varováni. Změřte si to sami.

Závěr

Teď už více či méně víte, jaké náklady na spravovaný kód mají nejnižší úroveň. Teď máte základní znalosti potřebné k tomu, abyste dosáhli chytřejších kompromisů implementace a rychlejšího psaní spravovaného kódu.

Viděli jsme, že vychýcený spravovaný kód může být "pedálem na kov" jako nativní kód. Vaším úkolem je moudře programovat a moudře si vybrat z mnoha bohatých a snadno použitelných zařízení v rámci architektury.

Existují nastavení, u kterých nezáleží na výkonu, a nastavení, kde je nejdůležitější funkcí produktu. Předčasné optimalizace je kořenem veškerého zla. Ale stejně tak je neopatrná nepozornost k efektivitě. Jste profesionál, umělec, řemeslník. Takže se ujistěte, že znáte náklady na věci. Pokud nevíte, nebo si myslíte, že to děláte, měříte to pravidelně.

Pokud jde o tým CLR, i nadá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ávejte, že se věci budou zlepšovat a zlepšovat. Sledujte nejnovější informace.

Zapamatujte si svůj slib.

Zdroje informací