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 szintaxiskiegészítési listákat, a hibákra vonatkozó hullámokat, a paramétertippeket, a kódhibá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. Akkor is érdemes tesztelnie a teljesítményt, és elemeznie kell a kódot, ha problémákat tapasztal.
2. tény: Ha nem mér, akkor azt feltételezi, hogy
A profilok és a mérések nem hazudnak. A profilok azt mutatják, hogy a processzor teljes mértékben be van-e töltve, vagy hogy le van-e tiltva a lemez I/O-ján. 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 szigorú megközelítésével elkerülheti az időt 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. Rögzítheti a teljesítményhez kapcsolódó eseménykövetést Windows (ETW)-eseményekhez, és egyszerűen megtekintheti alkalmazásonként, folyamatonként, veremenként és szálonkénti információkké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 végzett szinte minden munka során elkerülte a foglalásokat és kezelte 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 foglalások és példák
Az ebben a szakaszban szereplő példakifejezések rejtett foglalásokkal rendelkeznek, amelyek kicsinek tűnnek. Ha azonban egy nagy alkalmazás elég alkalommal hajtja végre a kifejezéseket, több száz megabájtot, akár gigabájtot is okozhat a foglalások számára. 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 boxolást, de néha előfordul, amikor a legkevésbé számít rá.
Ha a PerfView-ban szeretné látni a boxingot, nyisson meg egy nyomkövetést, és tekintse meg a GC Heap Alloc Stackset az alkalmazás folyamatnév alatt (ne feledje, a PerfView-jelentések az összes folyamatról). Ha a foglalásokhoz hasonló System.Int32 és System.Char alatti típusok jelennek meg, akkor az értéktípusokat kell megadnia. 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 hívás string.Format megoldja a Format(String, Object, Object) túlterhelést.
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. Részleges javítás az, hogy meghívja id.ToString() és size.ToString() átadja az összes sztringet (amelyek objektumok) a string.Format hívásnak. A hívás ToString() leküld egy sztringet, de ez a kiosztás mindenképpen megtörténik.string.Format
Úgy gondolhatja, hogy ez az alapszintű hívás string.Format csak sztringösszefűzés, ezért ezt a kódot írhatja inkább:
var s = id.ToString() + ':' + size.ToString();
Ez a kódsor azonban egy boxing-foglalást vezet be, mert az a Concat(Object, Object, Object). A .NET-keretrendszer meg kell adnia a meghívandó karakterkonstanstConcat
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: enumerálá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 finom. A PerfView ezt boxingként GetHashCode() jelenti, mert a metódus implementálási okokból az enumerálási típus mögöttes ábrázolását adja meg. 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 egyszerűen elkerülheti, ha a hívás GetHashCode()előtt a mögöttes ábrázolást adhatja meg:
((int)color).GetHashCode()
Az enumerálási típusok ökölvívá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 dobozolási költségeket, de csak az alkalmazás profilkészítése és a gyakori 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 String , mint Formata , Concat, Split, Join, Substringstb. Ha StringBuilder elkerüli, hogy egy sztringet sok darabból hozzon létre, segít, de még az StringBuilder objektum kiosztása is szűk keresztmetszetté válhat, amelyet kezelnie kell.
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 hívás az WriteFormattedDocComment első sorban text.Splitegy ú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 hívás Split egy sztringet is lefoglal minden sorhoz, text é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 a lefoglalásokat. 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 karakter szerint ellenőrzi, hogy a sztring a "///" karakterrel kezdődik-e. Az új kód ahelyettIndexOfFirstNonWhiteSpaceChar, TrimStart hogy az első indexet adja vissza (egy megadott kezdőindex után), ahol nem szóköz karakter jelenik meg. 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 a végrehajtáson belül sb.ToString() foglalást StringBuilder és belső foglalásokat okoz, de a sztring eredményének megadásakor nem szabályozhatja ezeket a foglalásokat.
Javítás például 4
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, ha egyetlen példányt is 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() a 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 attribútumot) használnak a StringBuildergyorsítótárazáshoz, és valószínűleg lemondhat a ThreadStatic deklarációról. 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 létrehoz egy új példányt, AcquireBuilder() és visszaadja azt, és null értékre állítja a mezőt vagy a gyorsítótárat.
Amikor végzett, StringBuilder meghívja GetStringAndReleaseBuilder() a sztring eredményének lekérését, mentse a példányt StringBuilder a mezőbe vagy a gyorsítótárba, majd adja vissza 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 csak akkor érdemes alkalmaznia, ha teljesítményproblémát észlelt, és a PerfView kimutatta, hogy StringBuilder a foglalások jelentős mértékben járulnak hozzá.
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: Lambdas, T< lista>é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 arra épülő IDE-élmények nagyon gyakran hívnak FindMatchingSymbol() , é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 == name záródik. Ez azt jelenti, hogy a kód a megtartott delegált predicatekiosztása mellett egy statikus osztályt is lefoglal a környezet tárolásához, amely az értékeket namerö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 meghatalmazotthoz) explicit.
Most nézze meg a hívás.FirstOrDefault A típus ezen bővítménymetódusa System.Collections.Generic.IEnumerable<T> 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ény típusa implementál IEnumerable<T> és okosan definiál egy enumerátort (IEnumerator<T> interfészt), amely List<T> egy struct. Ha osztály helyett struktúrát használ, általában elkerüli a halomfoglalásokat, ami viszont hatással lehet a szemétgyűjtés teljesítményére. Az enumerátorokat általában a nyelv hurokjával foreach használják, amely az enumerátor struktúráját használja a hívásverem visszaadása során. 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.
Kibontott FirstOrDefault hívás esetén a kódnak be kell hívnia GetEnumerator() egy IEnumerable<T>. A típusváltozóhoz symbolsenumerable való IEnumerable<Symbol> hozzárendelés elveszíti azt az információt, 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-bővítménymetszeteket, lambdákat vagy enumerátorokat, és nincs foglalása. 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 metódus 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 Parserpéldányt, elemzi a kódot, majd visszaad egy Task objektumot. Task<SyntaxTree> A költséges rész a példány kiosztása Parser é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 felhasználói szálat, hogy reagáljanak 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 lefoglalá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.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 ha van egy async típusfüggvénye Task<SyntaxTree>, és egy típusértéket SyntaxTreead vissza, a fordító kódot bocsát ki az eredmény tárolására (a használatával Task<SyntaxTree>.FromResult()). 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 típust cachedResult , Task<SyntaxTree> és egy async segédfüggvényt alkalmaz, amely az eredeti kódot GetSyntaxTreeAsync()tartalmazza.
GetSyntaxTreeAsync() Most a null szenesítő operátort használja a visszatéréshez cachedResult , ha az nem null. Ha cachedResult null, akkor GetSyntaxTreeAsync() meghívja GetSyntaxTreeUncachedAsync() és gyorsítótárazza az eredményt. Figyelje meg, hogy GetSyntaxTreeAsync() nem várja meg a hívás, GetSyntaxTreeUncachedAsync() mint a kód általában. Ha nem használja a várakozást, az azt jelenti, hogy amikor GetSyntaxTreeUncachedAsync() visszaadja az objektumot Task , GetSyntaxTreeAsync() azonnal visszaadja a Task. Most a gyorsítótárazott eredmény egy Task, így nincsenek foglalások a gyorsítótárazott eredmény visszaadásához.
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
Szótárak használják mindenütt sok program, és bár szótárak nagyon kényelmes és eredendően hatékony. 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 is végignézhet egy List<KeyValuePair\<K,V>>olyan gyorsan. 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 bekeretezve, de ha nagy struktúrákat ad át függvényargumentumként vagy értékeket ad vissza, a processzor időt vesz igénybe a struktúrák összes adattagjának atomi 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énybeli 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 – azt találgatja, hogy nem mér-e.
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.