Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Ez a cikk tippeket nyújt a nagyméretű .NET-keretrendszer alkalmazások vagy nagy mennyiségű adatot, például fájlokat vagy adatbázisokat feldolgozó alkalmazások teljesítményének javításához. Ezek a tippek a C# és a Visual Basic fordítók felügyelt kódban történő újraírásából származnak, és ez a cikk számos valós példát tartalmaz a C# fordítóból.
A .NET-keretrendszer rendkívül hatékony az alkalmazások létrehozásához. A hatékony és biztonságos nyelvek, valamint a tárak gazdag gyűjteménye rendkívül gyümölcsözővé teszi az alkalmazásépítést. A nagy termelékenység azonban felelősséggel jár. A .NET-keretrendszer minden erejét használnia kell, de szükség esetén készen kell állnia a kód teljesítményének finomhangolására.
Miért érvényes az új fordító teljesítménye az alkalmazásra?
A .NET Fordítóplatform ("Roslyn") csapata átírta a C# és a Visual Basic fordítókat felügyelt kódban, hogy új API-kat biztosítson a kód modellezéséhez és elemzéséhez, az eszközök készítéséhez, valamint a Visual Studióban sokkal gazdagabb, kódtudatos élmények biztosításához. A fordítók újraírása és a Visual Studio-élmények új fordítókon való létrehozása hasznos teljesítményelemzéseket hozott létre, amelyek alkalmazhatók bármely nagy .NET-keretrendszer alkalmazásra vagy bármely olyan alkalmazásra, amely sok adatot dolgoz fel. Nem kell tudnia a fordítókról, hogy kihasználhassa a C#-fordítóból származó megállapításokat és példákat.
A Visual Studio a fordító API-kkal hozza létre a felhasználók által kedvelt IntelliSense funkciókat, például az azonosítók és kulcsszavak színezését, a szintaxis-kiegészítési listákat, a hibák alatti hullámvonalakat, a paramétertippeket, a kódproblémákat és a kódműveleteket. A Visual Studio segítséget nyújt, miközben a fejlesztők gépelnek és módosítják a kódjukat, és a Visual Studiónak rugalmasnak kell maradnia, miközben a fordító folyamatosan modellezi a kódfejlesztőket.
Amikor a végfelhasználók interakcióba lépnek az alkalmazással, elvárják, hogy válaszkész legyen. A gépelést és a parancskezelést soha nem szabad letiltani. A súgónak gyorsan fel kell bukkannia, vagy fel kell adnia, ha a felhasználó továbbra is gépel. Az alkalmazásnak kerülnie kell a felhasználói felületi szál blokkolását olyan hosszú számításokkal, amelyek miatt az alkalmazás lassúnak érzi magát.
A Roslyn fordítóiról további információt a .NET Fordítóplatform SDK-jában talál.
Csak a tények
A teljesítmény finomhangolása és a rugalmas .NET-keretrendszer alkalmazások létrehozásakor vegye figyelembe ezeket a tényeket.
1. tény: Az idő előtti optimalizálás nem mindig éri meg a fáradtságot
A szükségesnél összetettebb kód írása karbantartási, hibakeresési és polírozási költségekkel jár. A tapasztalt programozók intuitív módon értelmezik a kódolási problémák megoldását és a hatékonyabb kódírást. Néha azonban idő előtt optimalizálják a kódjukat. Például kivonattáblát használnak, ha egy egyszerű tömb elegendő lenne, vagy bonyolult gyorsítótárazást használnak, amely memóriavesztést okozhat az értékek egyszerű újrafordítása helyett. Még akkor is, ha tapasztalt programozó, tesztelnie kell a teljesítményt és elemeznie a kódot, amikor problémák merülnek fel.
2. tény: Ha nem mér, akkor csak találgat.
A profilok és a mérések nem hazudnak. A profilok azt jelzik, hogy a processzor teljesen ki van-e használva, vagy hogy akadozik-e a lemez I/O miatt. A profilok azt jelzik, hogy milyen típusú és mennyiségű memóriát szeretne lefoglalni, és hogy a PROCESSZOR sok időt tölt-e szemétgyűjtéssel (GC).
Meg kell adnia a teljesítménycélokat az alkalmazás legfontosabb ügyfélélményeihez vagy forgatókönyveihez, és teszteket kell írnia a teljesítmény méréséhez. Vizsgálja meg a sikertelen teszteket a tudományos módszer alkalmazásával: használjon profilokat az útmutatóhoz, hipotézist állítson fel, mi lehet a probléma, és tesztelje a hipotézisét egy kísérlettel vagy kódmódosítással. Alapszintű teljesítménymérések létrehozása rendszeres teszteléssel, így elkülönítheti azokat a változásokat, amelyek regressziót okoznak a teljesítményben. A teljesítmény munkájának szigorú megközelítésével elkerülheti az időpazarlást a szükségtelen kódfrissítésekkel.
3. tény: A jó eszközök teszik a különbséget
A jó eszközökkel gyorsan feltárhatja a legnagyobb teljesítményproblémákat (CPU, memória vagy lemez), és segíthet megtalálni a szűk keresztmetszeteket okozó kódot. A Microsoft számos különböző teljesítményeszközt kínál, például a Visual Studio Profilert és a PerfView-t.
A PerfView egy hatékony eszköz, amely segít a mély problémákra, például a lemez I/O-jára, a GC-eseményekre és a memóriára összpontosítani. A teljesítményhez kapcsolódó Event Tracing for Windows (ETW) események rögzíthetők, és az információk egyszerűen megtekinthetők alkalmazásonként, folyamatonként, veremként és szálanként. A PerfView megmutatja, hogy mennyi és milyen memóriát foglal le az alkalmazás, és hogy mely függvények vagy hívásveremek járulnak hozzá a memóriafoglalásokhoz. További részletekért tekintse meg az eszközhöz mellékelt súgótémaköröket, bemutatókat és videókat.
4. tény: Minden a foglalásokról szól
Azt gondolhatja, hogy egy rugalmas .NET-keretrendszer-alkalmazás létrehozása olyan algoritmusokról szól, mint például a gyors rendezés használata buborékos rendezés helyett, de ez nem így van. A rugalmas alkalmazások létrehozásának legnagyobb tényezője a memória kiosztása, különösen akkor, ha az alkalmazás nagy méretű, vagy nagy mennyiségű adatot dolgoz fel.
Az új fordító API-k rugalmas IDE-élményének kialakításához a munka nagy része arról szólt, hogy elkerülje a foglalásokat és kezelje a gyorsítótárazási stratégiákat. A PerfView-nyomkövetések azt mutatják, hogy az új C#- és Visual Basic-fordítók teljesítménye ritkán van processzorhoz kötve. A fordítók több százezer vagy több millió sornyi kód olvasása, metaadatok olvasása vagy generált kód kibocsátásakor I/O-kötöttek lehetnek. A felhasználói felületi szál késése szinte mind a szemétgyűjtésnek köszönhető. A .NET-keretrendszer GC nagy mértékben a teljesítményre van hangolva, és az alkalmazáskód végrehajtása során a munka nagy részét egyidejűleg végzi. Egyetlen foglalás azonban egy költséges gen2-gyűjteményt indíthat el, amely leállítja az összes szálat.
Gyakori elosztások és példák
Az ebben a szakaszban található példakifejezések rejtett foglalásokat tartalmaznak, amelyek kicsinek tűnnek. Ha azonban egy nagy alkalmazás elégszer hajtja végre a kifejezéseket, száz megabájttól akár gigabájtig terjedő memóriamennyiséget is foglalhat. Például egy perces tesztek, amelyek egy fejlesztő gépelését szimulálták a szerkesztőben, gigabájtnyi memóriát osztottak ki, és a teljesítményért felelős csapat a gépelési forgatókönyvekre összpontosított.
Ökölvívás
A dobozolás akkor fordul elő, ha a veremen vagy adatstruktúrákban általában élő értéktípusok egy objektumba vannak csomagolva. Vagyis lefoglal egy objektumot az adatok tárolásához, majd egy mutatót ad vissza az objektumhoz. A .NET-keretrendszer néha egy metódus aláírása vagy egy tárolási hely típusa miatt jelöl meg értékeket. Egy objektum értéktípusának körbefuttatása memóriafoglalást okoz. Számos dobozolási művelet megabájtos vagy gigabájtos foglalásokat adhat hozzá az alkalmazáshoz, ami azt jelenti, hogy az alkalmazás több GCs-t fog okozni. A .NET-keretrendszer és a nyelvi fordítók lehetőség szerint kerülik a dobozolást, de néha előfordul, amikor a legkevésbé számítasz rá.
Ha a PerfView-ban szeretné látni a "boxing"-ot, nyisson meg egy nyomkövetést, és tekintse meg a GC Heap Alloc Stacks-t az alkalmazás folyamata alatt (ne feledje, a PerfView jelentések az összes folyamatról szólnak). Ha olyan típusokat lát, mint a System.Int32 és System.Char az allokációk alatt, az értéktípusokat "boxolja." Az ilyen típusok egyikének kiválasztásával megjelennek azok a veremek és függvények, amelyekben be vannak jelölve.
1. példa: karakterlánc-metódusok és értéktípus-argumentumok
Ez a mintakód a lehetségesen szükségtelen és túlzott boxolást szemlélteti:
public class Logger
{
public static void WriteLine(string s) { /*...*/ }
}
public class BoxingExample
{
public void Log(int id, int size)
{
var s = string.Format("{0}:{1}", id, size);
Logger.WriteLine(s);
}
}
Ez a kód naplózási funkciókat biztosít, így egy alkalmazás gyakran, akár több milliószor is meghívhatja a Log függvényt. A probléma az, hogy a string.Format hívás az Format(String, Object, Object) túlterhelésére vonatkozik.
Ez a túlterhelés megköveteli, hogy a .NET-keretrendszer az int értékeket objektumokba írja be, hogy átadhassa őket ennek a metódushívásnak. A részleges javítás az, hogy meghívja a id.ToString() és size.ToString() függvényeket, és átadja az összes sztringet (amelyek objektumok) a string.Format hívásának. A ToString() való hívás egy sztringet oszt ki, de ez a kiosztás mindenképpen megtörténik a string.Format belsejében.
Úgy gondolhatja, hogy ez az alapvető hívás string.Format csak sztringösszefűzés, ezért inkább így írhatja a kódot:
var s = id.ToString() + ':' + size.ToString();
Azonban ez a kódsor egy boxing allokációt vezet be, mert az Concat(Object, Object, Object)-re fordul le. A .NET-keretrendszernek be kell kereteznie a karakterliterált a(z) Concat meghívásához.
Javítás az 1. példában
A teljes javítás egyszerű. Csak cserélje le a karakterkonstanst egy sztringkonstansra, amely nem jár boxolással, mert a sztringek már objektumok:
var s = id.ToString() + ":" + size.ToString();
2. példa: enum típus felcsomagolás
Ez a példa az új C#- és Visual Basic-fordítókban a számbavételi típusok gyakori használatának, különösen a szótárkeresési műveleteknek köszönhetően nagy mennyiségű foglalásért volt felelős.
public enum Color
{
Red, Green, Blue
}
public class BoxingExample
{
private string name;
private Color color;
public override int GetHashCode()
{
return name.GetHashCode() ^ color.GetHashCode();
}
}
Ez a probléma nagyon árnyalt. A PerfView ezt GetHashCode() dobozolásként jelenti, mert a metódus az enumeráció típusának mögöttes ábrázolását dobozolja megvalósítási okokból. Ha alaposan megtekinti a PerfView-t, két boxing-foglalás jelenhet meg minden egyes híváshoz GetHashCode(). A fordító beszúrja az egyiket, a .NET-keretrendszer pedig a másikat.
Javítás a 2. példában
A két foglalást könnyen elkerülheti, ha a hívás előtt a mögöttes ábrázolásra konvertál, mielőtt GetHashCode()-t meghívná.
((int)color).GetHashCode()
Az enumerálási típusok dobozolásának másik gyakori forrása a Enum.HasFlag(Enum) módszer. Az átadott HasFlag(Enum) argumentumot be kell jelölni. A legtöbb esetben a hívások Enum.HasFlag(Enum) bitenkénti tesztre való cseréje egyszerűbb és kiosztásmentes.
Tartsa szem előtt az első teljesítménybeli tényt (azaz ne optimalizálja idő előtt), és ne kezdje el újraírni az összes kódot így. Vegye figyelembe ezeket a boxing költségeket, de csak az alkalmazás profilkészítése és a forró pontok megkeresése után módosítsa a kódot.
Sztringek
A sztringmanipulációk a foglalások legnagyobb bűnösei közé tartoznak, és gyakran megjelennek a PerfView-ban az első öt foglalásban. A programok sztringeket használnak szerializáláshoz, JSON- és REST API-khoz. A sztringeket programozott állandókként használhatja a rendszerekkel való együttműködéshez, ha nem használhat számbavételi típusokat. Ha a profilkészítés azt mutatja, hogy a sztringek nagy mértékben befolyásolják a teljesítményt, keresse meg az olyan metódusokra irányuló hívásokat, mint String, Format, Concat, Split, Join, Substring stb. A StringBuilder használata, hogy elkerüljük a költséget ami egy karakterlánc sok darabból való létrehozásával jár, segít, de még az StringBuilder objektum kiosztása is szűk keresztmetszetté válhat, amely kezelést igényel.
3. példa: sztringműveletek
A C#-fordítóban volt ez a kód, amely egy formázott XML-dokumentum megjegyzésének szövegét írja:
public void WriteFormattedDocComment(string text)
{
string[] lines = text.Split(new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None);
int numLines = lines.Length;
bool skipSpace = true;
if (lines[0].TrimStart().StartsWith("///"))
{
for (int i = 0; i < numLines; i++)
{
string trimmed = lines[i].TrimStart();
if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3]))
{
skipSpace = false;
break;
}
}
int substringStart = skipSpace ? 4 : 3;
for (int i = 0; i < numLines; i++)
WriteLine(lines[i].TrimStart().Substring(substringStart));
}
else { /* ... */ }
Láthatja, hogy ez a kód sok sztring-kezelést végez. A kód kódtár-metódusokkal külön sztringekre osztja fel a sorokat, térközt vág, ellenőrzi, hogy az argumentum text XML-dokumentációs megjegyzés-e, és hogy a sorokból kinyerje az alsztringeket.
A WriteFormattedDocComment első sorában lévő text.Split hívás egy új háromelemű tömböt foglal le argumentumként minden híváskor. A fordítónak minden alkalommal ki kellbocsátania a kódot a tömb lefoglalásához. Ennek az az oka, hogy a fordító nem tudja, hogy a tömböt olyan helyen tárolja-e Split , ahol a tömb más kóddal módosítható, ami hatással lenne a későbbi hívásokra WriteFormattedDocComment. A Split hívása minden sorhoz lefoglal egy sztringet a text-ban, és más memóriát is lefoglal a művelet végrehajtásához.
WriteFormattedDocComment három hívása van a TrimStart metódushoz. Kettő olyan belső hurkokban található, amelyek duplikálják a munkát és az allokációkat. A helyzet rosszabbá tétele érdekében a TrimStart metódus argumentumok nélküli meghívása a sztringeredmény mellett egy üres tömböt (a params paraméterhez) is lefoglal.
Végül van egy hívás a Substring metódushoz, amely általában egy új sztringet foglal le.
Javítás a 3. példában
A korábbi példáktól eltérően a kisebb módosítások nem tudják kijavítani ezeket a foglalásokat. Vissza kell lépnie, meg kell néznie a problémát, és másképp kell megközelítenie. Megfigyelheti például, hogy a függvény argumentuma WriteFormattedDocComment() egy olyan sztring, amely tartalmazza a metódus által igényelt összes információt, így a kód több indexelést is végrehajthat ahelyett, hogy több részleges sztringet kellene kiosztania.
A fordító teljesítményért felelős csapata a következő kóddal oldotta meg ezeket a foglalásokat:
private int IndexOfFirstNonWhiteSpaceChar(string text, int start) {
while (start < text.Length && char.IsWhiteSpace(text[start])) start++;
return start;
}
private bool TrimmedStringStartsWith(string text, int start, string prefix) {
start = IndexOfFirstNonWhiteSpaceChar(text, start);
int len = text.Length - start;
if (len < prefix.Length) return false;
for (int i = 0; i < len; i++)
{
if (prefix[i] != text[start + i]) return false;
}
return true;
}
// etc...
Az első verzió WriteFormattedDocComment() egy tömböt, több alsztringet és egy levágott részstringet foglalt le egy üres params tömbdel együtt. Azt is ellenőrizte, hogy "////". A módosított kód csak indexelést használ, és nem foglal le semmit. Megkeresi az első karaktert, amely nem szóköz, majd karakterenként ellenőrzi, hogy a szöveglánc a "///" karakterrel kezdődik-e. Az új kód a IndexOfFirstNonWhiteSpaceChar helyett a TrimStart használja az első index visszaadására (egy megadott kezdőindex után), ahol nem szóköz karakter fordul elő. A javítás még nem fejeződött be, de láthatja, hogyan alkalmazhat hasonló javításokat egy teljes megoldáshoz. Ha ezt a módszert alkalmazza a kódban, eltávolíthatja az összes foglalást a kódból WriteFormattedDocComment().
4. példa: StringBuilder
Ez a példa egy objektumot StringBuilder használ. Az alábbi függvény generál egy teljes típusnevet az általános típusok számára:
public class Example
{
// Constructs a name like "SomeType<T1, T2, T3>"
public string GenerateFullTypeName(string name, int arity)
{
StringBuilder sb = new StringBuilder();
sb.Append(name);
if (arity != 0)
{
sb.Append("<");
for (int i = 1; i < arity; i++)
{
sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
}
sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
}
return sb.ToString();
}
}
A fókusz azon a soron van, amely létrehoz egy új StringBuilder példányt. A kód sb.ToString() és a StringBuilder implementáció során belső foglalásokat okoz, de ha sztring eredményt szeretne, akkor ezeket a foglalásokat nem tudja szabályozni.
A 4-es példa javítása
Az StringBuilder objektumfoglalás javításához gyorsítótárazza az objektumot. Még egy olyan példány gyorsítótárazása is jelentősen javíthatja a teljesítményt, amelyet később el lehet dobni. Ez a függvény új implementációja, amely kihagyja az összes kódot az új első és utolsó sorok kivételével:
// Constructs a name like "MyType<T1, T2, T3>"
public string GenerateFullTypeName(string name, int arity)
{
StringBuilder sb = AcquireBuilder();
/* Use sb as before */
return GetStringAndReleaseBuilder(sb);
}
A fő részek az új AcquireBuilder() és GetStringAndReleaseBuilder() függvények:
[ThreadStatic]
private static StringBuilder cachedStringBuilder;
private static StringBuilder AcquireBuilder()
{
StringBuilder result = cachedStringBuilder;
if (result == null)
{
return new StringBuilder();
}
result.Clear();
cachedStringBuilder = null;
return result;
}
private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
string result = sb.ToString();
cachedStringBuilder = sb;
return result;
}
Mivel az új fordítók szálkezelést használnak, ezek az implementációk egy szál-statikus mezőt (ThreadStaticAttribute) használnak a StringBuilder gyorsítótárazáshoz, és valószínűleg elhagyhatja a ThreadStatic deklarációt. A szál-statikus mező egyedi értéket tartalmaz minden olyan szálhoz, amely végrehajtja ezt a kódot.
AcquireBuilder() Ha van ilyen, a gyorsítótárazott StringBuilder példányt adja vissza, miután törölte, és null értékre állítja a mezőt vagy a gyorsítótárat. Ellenkező esetben a AcquireBuilder() létrehoz egy új példányt, visszaadja azt, és a mezőt vagy a gyorsítótárat null értékre állítja.
Miután végzett a StringBuilder művelettel, meghívja a GetStringAndReleaseBuilder()-t a sztring eredmény lekérése céljából, menti a StringBuilder példányt a mezőbe vagy gyorsítótárba, majd visszaadja az eredményt. A végrehajtás során újra beírhatja ezt a kódot, és több StringBuilder objektumot is létrehozhat (bár ez ritkán fordul elő). A kód csak a legutóbb kiadott StringBuilder példányt menti későbbi használatra. Ez az egyszerű gyorsítótárazási stratégia jelentősen csökkentette a foglalásokat az új fordítókban. A .NET-keretrendszer és az MSBuild ("MSBuild") részei hasonló technikával javítják a teljesítményt.
Ez az egyszerű gyorsítótárazási stratégia megfelel a jó gyorsítótár-kialakításnak, mert méretkorlátja van. Most azonban több kód van, mint az eredetiben, ami több karbantartási költséget jelent. A gyorsítótárazási stratégiát akkor érdemes alkalmaznia, ha teljesítményproblémát tapasztalt, és a PerfView kimutatta, hogy StringBuilder okozta foglalások jelentős mértékben hozzájárulnak ehhez.
LINQ és lambdas
A Language-Integrated Query (LINQ) a lambda kifejezésekkel együtt egy hatékonyságnövelő funkció példája. A használata azonban jelentős hatással lehet a teljesítményre az idő múlásával, és előfordulhat, hogy újra kell írnia a kódot.
5. példa: Lambda-kifejezések, Lista<T>, és IEnumerable<T>
Ez a példa LINQ és funkcionális stíluskód használatával keres szimbólumot a fordító modelljében egy névsztring alapján:
class Symbol {
public string Name { get; private set; }
/*...*/
}
class Compiler {
private List<Symbol> symbols;
public Symbol FindMatchingSymbol(string name)
{
return symbols.FirstOrDefault(s => s.Name == name);
}
}
Az új fordító és az erre épülő IDE-tulajdonságok rendkívül gyakran hívják a FindMatchingSymbol()-t, és a függvény egyetlen kódsorában számos rejtett lefoglalás található. A foglalások vizsgálatához először ossza fel a függvény egyetlen kódsorát két sorra:
Func<Symbol, bool> predicate = s => s.Name == name;
return symbols.FirstOrDefault(predicate);
Az első sorban a lambda kifejezéss => s.Name == namelezárja a lokális változótname. Ez azt jelenti, hogy amellett, hogy létrehoz egy objektumot azzal a delegálttal , amelyet predicate tart, a kód egy statikus osztályt is lefoglal, hogy tárolja azt a környezetet, amely az name értéket rögzíti. A fordító az alábbihoz hasonló kódot hoz létre:
// Compiler-generated class to hold environment state for lambda
private class Lambda1Environment
{
public string capturedName;
public bool Evaluate(Symbol s)
{
return s.Name == this.capturedName;
}
}
// Expanded Func<Symbol, bool> predicate = s => s.Name == name;
Lambda1Environment l = new Lambda1Environment() { capturedName = name };
var predicate = new Func<Symbol, bool>(l.Evaluate);
A két new foglalás (egy a környezeti osztályhoz és egy a delegátushoz) most már explicit.
Most nézze meg a FirstOrDefault hívását. A System.Collections.Generic.IEnumerable<T> típus bővített metódusa is foglalást von maga után. Mivel FirstOrDefault egy IEnumerable<T> objektumot első argumentumként használ, kibonthatja a hívást a következő kódra (egy kicsit leegyszerűsítve a vitafórumhoz):
// Expanded return symbols.FirstOrDefault(predicate) ...
IEnumerable<Symbol> enumerable = symbols;
IEnumerator<Symbol> enumerator = enumerable.GetEnumerator();
while(enumerator.MoveNext())
{
if (predicate(enumerator.Current))
return enumerator.Current;
}
return default(Symbol);
A symbols változó típusa List<T>. A List<T> gyűjteménytípus megvalósítja a IEnumerable<T>-et, és okosan meghatároz egy enumerátort (IEnumerator<T> interfészt), amelyet a List<T> megvalósít egy struct-vel. Ha osztály helyett struktúrát használ, általában elkerüli a halom allokációkat, ami viszont hatással lehet a szemétgyűjtés teljesítményére gyakorolt hatásra. Az enumerátorokat általában a nyelv foreach ciklusaival használják, amelyek az enumerátor struktúráját használják a hívásveremben. Ha a hívásveremmutatót úgy növeli, hogy helyet adjon egy objektumnak, az nem befolyásolja a GC-t a halomfoglalás módjára.
Kibővített FirstOrDefault hívás esetén a kódnak meg kell hívnia GetEnumerator() egy IEnumerable<T>. Az symbols hozzáadása a enumerable változóhoz, amelynek típusa IEnumerable<Symbol>, elveszíti az információt arról, hogy a tényleges objektum egy List<T>. Ez azt jelenti, hogy amikor a kód lekéri az enumerátortenumerable.GetEnumerator(), a .NET-keretrendszer a visszaadott struktúrát be kell jelölnie, hogy hozzárendelje a enumerator változóhoz.
Javítás az 5. példában
A javítás az, hogy az alábbiak szerint írja FindMatchingSymbol át az egysoros kódsort hat olyan kódsorra, amelyek még mindig tömörek, könnyen olvashatók és érthetőek, és könnyen karbantarthatóak:
public Symbol FindMatchingSymbol(string name)
{
foreach (Symbol s in symbols)
{
if (s.Name == name)
return s;
}
return null;
}
Ez a kód nem használ LINQ kiterjesztéses metódusokat, lambdákat vagy enumerátorokat, és nincs memóriafoglalással. Nincsenek foglalások, mert a fordító láthatja, hogy a symbols gyűjtemény egy List<T> , és az eredményként kapott enumerátort (struktúrát) a megfelelő típusú helyi változóhoz kötheti a boxolás elkerülése érdekében. A függvény eredeti verziója nagyszerű példa volt a C# kifejező erejére és a .NET-keretrendszer termelékenységére. Ez az új és hatékonyabb verzió megőrzi ezeket a tulajdonságokat anélkül, hogy összetett kódot ad hozzá a karbantartáshoz.
Async függvény gyorsítótárazása
A következő példa egy gyakori problémát mutat be, amikor gyorsítótárazott eredményeket próbál használni egy aszinkron metódusban.
6. példa: gyorsítótárazás aszinkron metódusokban
Az új C# és Visual Basic fordítókra épülő Visual Studio IDE-funkciók gyakran lekérnek szintaxisfákat, és a fordítók aszinkron módon használják ezt a Visual Studio válaszkészsége érdekében. A szintaxisfa lekéréséhez az alábbi kód első verziója írható:
class SyntaxTree { /*...*/ }
class Parser { /*...*/
public SyntaxTree Syntax { get; }
public Task ParseSourceCode() { /*...*/ }
}
class Compilation { /*...*/
public async Task<SyntaxTree> GetSyntaxTreeAsync()
{
var parser = new Parser(); // allocation
await parser.ParseSourceCode(); // expensive
return parser.Syntax;
}
}
Láthatja, hogy a hívás GetSyntaxTreeAsync() létrehoz egy Parser példányt, elemzi a kódot, majd visszaad egy Task objektumot, Task<SyntaxTree>. A költséges rész a Parser példány kiosztása és a kód elemzése. A függvény visszaad egy Task értéket, hogy a hívók megvárhassák az elemzési munkát, és felszabadíthassák a UI szálat, hogy reagálni tudjon a felhasználói bemenetre.
Több Visual Studio-funkció is megpróbálhatja ugyanazt a szintaxisfát beszerezni, ezért a következő kódot megírhatja az elemzési eredmény gyorsítótárazásához, hogy időt és foglalásokat takarítson meg. Ez a kód azonban memóriafoglalással jár:
class Compilation { /*...*/
private SyntaxTree cachedResult;
public async Task<SyntaxTree> GetSyntaxTreeAsync()
{
if (this.cachedResult == null)
{
var parser = new Parser(); // allocation
await parser.ParseSourceCode(); // expensive
this.cachedResult = parser.Syntax;
}
return this.cachedResult;
}
}
Láthatja, hogy a gyorsítótárazással rendelkező új kódnak van egy SyntaxTree mezője, amelynek neve cachedResult. Ha ez a mező null értékű, elvégzi a munkát, GetSyntaxTreeAsync() és menti az eredményt a gyorsítótárba.
GetSyntaxTreeAsync() visszaadja az SyntaxTree objektumot. A probléma az, hogy amikor egy async típusú függvénnyel rendelkezik Task<SyntaxTree>, és visszaad egy SyntaxTree típusú értéket, a fordító kódot bocsát ki egy Task allokálására, hogy tárolja az eredményt (a Task<SyntaxTree>.FromResult() használatával). A tevékenység befejezettként van megjelölve, és az eredmény azonnal elérhető. Az új fordítók kódjában olyan gyakran előfordultak már befejezett objektumok, Task hogy a foglalások javítása észrevehetően javította a válaszképességet.
Javítás például 6
A befejezett Task lefoglalás eltávolításához gyorsítótárazza a Feladat objektumot a befejezett eredménnyel:
class Compilation { /*...*/
private Task<SyntaxTree> cachedResult;
public Task<SyntaxTree> GetSyntaxTreeAsync()
{
return this.cachedResult ??
(this.cachedResult = GetSyntaxTreeUncachedAsync());
}
private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync()
{
var parser = new Parser(); // allocation
await parser.ParseSourceCode(); // expensive
return parser.Syntax;
}
}
Ez a kód megváltoztatja a cachedResult típusát Task<SyntaxTree> típusra, és egy async segédfüggvényt alkalmaz, amely az eredeti kódot GetSyntaxTreeAsync()-ból/-ből őrzi meg.
GetSyntaxTreeAsync() már a null egyesítő operátort használja, hogy visszatérjen cachedResult, ha az nem nulla. Ha cachedResult null, akkor GetSyntaxTreeAsync() meghívja GetSyntaxTreeUncachedAsync() és gyorsítótárazza az eredményt. Kérjük, vegye figyelembe, hogy a GetSyntaxTreeAsync() nem várja meg a GetSyntaxTreeUncachedAsync() hívását, ahogyan a kód általában tenné. Ha nem használja a 'await' kifejezést, az azt jelenti, hogy amikor a GetSyntaxTreeUncachedAsync() visszaadja a Task objektumot, a GetSyntaxTreeAsync() azonnal visszaadja a Task. Most a gyorsítótárazott eredmény egy Task, így nincsenek memóriafoglalások a gyorsítótárazott eredmény visszaszolgáltatására.
További szempontok
Íme néhány további pont a sok adatot feldolgozó nagy alkalmazások vagy alkalmazások lehetséges problémáival kapcsolatban.
Szótárak
A szótárakat mindenütt használják sok programban, bár a szótárak nagyon kényelmesek és eredendően hatékonyak. Gyakran azonban helytelenül használják őket. A Visual Studióban és az új fordítókban az elemzés azt mutatja, hogy sok szótár egyetlen elemet tartalmazott, vagy üres volt. Egy üres Dictionary<TKey,TValue> mező tíz mezőt tartalmaz, és 48 bájtot foglal el egy x86-os gépen lévő halomon. A szótárak akkor használhatók, ha egy leképezésre vagy asszociatív adatstruktúrára van szüksége állandó idejű kereséssel. Ha azonban csak néhány elemből áll, sok helyet pazarol egy szótár használatával. Ehelyett például iteratív módon ugyanolyan gyorsan átnézhet egy List<KeyValuePair\<K,V>>. Ha csak adatokkal tölti be a szótárat, majd olvas belőle (ez egy nagyon gyakori minta), akkor az N(log(N)) kereséssel rendelkező rendezett tömbök használata a használt elemek számától függően közel olyan gyors lehet.
Osztályok és struktúrák
Az osztályok és struktúrák így klasszikus tér-idő kompromisszumot biztosítanak az alkalmazások finomhangolásához. Az osztályok 12 bájtnyi többletterhelést jelentenek egy x86-os gépen, még akkor is, ha nincsenek mezőik, de olcsók, mivel csak egy osztálypéldányra mutató mutatót használnak. A struktúrák nem járnak halomfoglalással, ha nincsenek dobozba zárva, de ha nagy struktúrákat ad át függvényargumentumként vagy visszaad értékeket, a processzor időt igényel a struktúrák összes adattagjának atomi műveletként másolásához. Figyelje meg a struktúrákat visszaadó tulajdonságok ismételt hívásait, és gyorsítótárazza a tulajdonság értékét egy helyi változóban, hogy elkerülje a túlzott adatmásolást.
Gyorsítótárak
Gyakori teljesítmény javítási trükk az eredmények gyorsítótárazása. A méretkorlátot vagy ártalmatlanítási szabályzatot nem használó gyorsítótár azonban memóriavesztést okozhat. Nagy mennyiségű adat feldolgozásakor, ha sok memóriát tart a gyorsítótárakban, a szemétgyűjtés felülírhatja a gyorsítótárazott keresések előnyeit.
Ebben a cikkben bemutattuk, hogyan kell tisztában lennie a teljesítmény szűk keresztmetszetének tüneteivel, amelyek hatással lehetnek az alkalmazás válaszkészségére, különösen a nagy mennyiségű adatot feldolgozó nagy rendszerek vagy rendszerek esetében. A gyakori bűnösök közé tartozik a boxolás, a sztringmanipuláció, a LINQ és a lambda, a gyorsítótárazás az aszinkron metódusokban, a gyorsítótárazás méretkorlát vagy ártalmatlanítási szabályzat nélkül, a szótárak nem megfelelő használata és a struktúrák megkerülése. Tartsa szem előtt az alkalmazások finomhangolásának négy tényét:
Ne optimalizáljon idő előtt – legyen hatékony, és finomhangolja az alkalmazást, amikor problémákat észlel.
A profilok nem hazudnak – ha nem mér, csak találgat.
A jó eszközök teszik a különbséget – töltse le a PerfView-t, és próbálja ki.
Minden a kiosztásokról szól – itt töltötte a fordítóplatform csapata a legtöbb időt az új fordítók teljesítményének javítására.