Freigeben über


Schreiben High-Performance verwalteter Anwendungen: Eine Einführung

 

Gregor Noriskin
Microsoft CLR Performance Team

Juni 2003

Gilt für:
   Microsoft® .NET Framework

Zusammenfassung: Erfahren Sie mehr über die Common Language Runtime der .NET Framework aus Leistungssicht. Erfahren Sie, wie Sie bewährte Methoden für die Leistung von verwaltetem Code identifizieren und die Leistung Ihrer verwalteten Anwendung messen. (19 gedruckte Seiten)

Laden Sie den CLR Profiler herunter. (330 KB)

Inhalte

Jonglieren als Metapher für die Softwareentwicklung
The .NET Common Language Runtime
Verwaltete Daten und der Garbage Collector
Zuordnungsprofile
Profilerstellungs-API und CLR Profiler
Hosten der Server-GC
Abschluss
Das Dispose-Muster
Hinweis zu schwachen Verweisen
Verwalteter Code und das CLR-JIT
Werttypen
Ausnahmebehandlung
Threading und Synchronisierung
Spiegelung
Späte Bindung
Sicherheit
COM-Interop und Plattformaufruf
Leistungsindikatoren
Sonstige Tools
Zusammenfassung
Ressourcen

Jonglieren als Metapher für die Softwareentwicklung

Jonglieren ist eine großartige Metapher für die Beschreibung des Softwareentwicklungsprozesses. Das Jonglieren erfordert in der Regel mindestens drei Elemente, obwohl es keine Obergrenze für die Anzahl der Elemente gibt, die Sie versuchen können, zu jonglieren. Wenn Sie lernen, wie man jonglieren kann, stellen Sie fest, dass Sie jeden Ball einzeln watch, während Sie sie fangen und werfen. Wenn Sie vorankommen, beginnen Sie, sich auf den Fluss der Bälle zu konzentrieren, im Gegensatz zu jedem einzelnen Ball. Wenn Sie das Jonglieren beherrscht haben, können Sie sich wieder auf einen einzelnen Ball konzentrieren, diesen Ball auf Der Nase ausbalancieren, während Sie die anderen weiter jonglieren. Sie wissen intuitiv, wo die Bälle liegen und können Ihre Hand an die richtige Stelle setzen, um sie zu fangen und zu werfen. Wie ist das also mit der Softwareentwicklung?

Unterschiedliche Rollen im Softwareentwicklungsprozess jonglieren unterschiedliche "Trinitäten"; Projekt- und Programmmanager jonglieren Features, Ressourcen und Zeit, und Softwareentwickler jonglieren Korrektheit, Leistung und Sicherheit. Man kann immer versuchen, mehr Gegenstände zu jonglieren, aber wie jeder Schüler des Jonglierens nachweisen kann, macht es das Hinzufügen eines einzelnen Balls exponentiell schwieriger, die Bälle in der Luft zu halten. Technisch gesehen, wenn Sie weniger als drei Bälle jonglieren, jonglieren Sie überhaupt nicht. Wenn Sie als Softwareentwickler nicht die Richtigkeit, Leistung und Sicherheit des codes berücksichtigen, den Sie schreiben, kann der Fall gemacht werden, dass Sie Ihre Arbeit nicht erledigen. Wenn Sie zunächst über Korrektheit, Leistung und Sicherheit nachdenken, müssen Sie sich auf einen Aspekt konzentrieren. Wenn sie Teil Ihrer täglichen Praxis werden, werden Sie feststellen, dass Sie sich nicht auf einen bestimmten Aspekt konzentrieren müssen, sie werden einfach Teil ihrer Arbeitsweise sein. Wenn Sie sie gemeistert haben, werden Sie in der Lage sein, intuitiv Kompromisse zu treffen und Ihre Bemühungen entsprechend zu konzentrieren. Und wie beim Jonglieren ist Übung der Schlüssel.

Das Schreiben von hochleistungsfähigem Code hat eine eigene Dreifaltigkeit; Festlegen von Zielen, Messen und Verstehen der Zielplattform. Wenn Sie nicht wissen, wie schnell Ihr Code sein muss, wie werden Sie wissen, wann Sie fertig sind? Wenn Sie Ihren Code nicht messen und profilieren, woher wissen Sie, wann Sie Ihre Ziele erreicht haben oder warum Sie Ihre Ziele nicht erreichen? Wenn Sie die Plattform, auf die Sie abzielen, nicht verstehen, wie Sie wissen, was Sie optimieren müssen, falls Sie Ihre Ziele nicht erreichen? Diese Prinzipien gelten für die Entwicklung von hochleistungsfähigem Code im Allgemeinen, unabhängig davon, auf welche Plattform Sie abzielen. Kein Artikel zum Schreiben von hochleistungsfähigem Code wäre vollständig, ohne diese Dreifaltigkeit zu erwähnen. Obwohl alle drei gleich wichtig sind, konzentriert sich dieser Artikel auf die beiden letzten Aspekte, da sie für das Schreiben von Hochleistungsanwendungen gelten, die auf die Microsoft®-.NET Framework ausgerichtet sind.

Die grundlegenden Prinzipien für das Schreiben von hochleistungsfähigem Code auf jeder Plattform sind:

  1. Festlegen von Leistungszielen
  2. Messen, Messen und anschließendes Messen
  3. Verstehen der Hardware- und Softwareplattformen, auf die Ihre Anwendung ausgerichtet ist

The .NET Common Language Runtime

Der Kern des .NET Framework ist die Common Language Runtime (CLR). Die CLR stellt alle Laufzeitdienste für Ihren Code bereit. Just-In-Time-Kompilierung, Speicherverwaltung, Sicherheit und eine Reihe weiterer Dienste. Die CLR wurde für eine hohe Leistung konzipiert. Das heißt, es gibt Möglichkeiten, diese Leistung zu nutzen und sie zu behindern.

Das Ziel dieses Artikels besteht darin, einen Überblick über die Common Language Runtime aus Leistungssicht zu geben, bewährte Methoden für die Leistung von verwaltetem Code zu identifizieren und zu zeigen, wie Sie die Leistung Ihrer verwalteten Anwendung messen können. Dieser Artikel ist keine erschöpfende Erläuterung der Leistungsmerkmale der .NET Framework. Für die Zwecke dieses Artikels werde ich die Leistung definieren, um Durchsatz, Skalierbarkeit, Startzeit und Speicherauslastung einzuschließen.

Verwaltete Daten und der Garbage Collector

Eines der Hauptsorgen der Entwickler bei der Verwendung von verwaltetem Code in leistungskritischen Anwendungen sind die Kosten der Speicherverwaltung der CLR, die vom Garbage Collector (GC) durchgeführt wird. Die Kosten für die Speicherverwaltung sind eine Funktion der Speicherzuordnungskosten, die einem instance eines Typs zugeordnet sind, den Kosten für die Verwaltung dieses Arbeitsspeichers über die Lebensdauer des instance und den Kosten für die Freigabe dieses Arbeitsspeichers, wenn er nicht mehr benötigt wird.

Eine verwaltete Zuordnung ist in der Regel sehr günstig; in den meisten Fällen nimmt weniger Zeit in Anspruch als ein C/C++ malloc oder new. Dies liegt daran, dass die CLR keine freie Liste scannen muss, um den nächsten verfügbaren zusammenhängenden Speicherblock zu finden, der groß genug ist, um das neue Objekt aufzunehmen. Es behält einen Zeiger auf die nächste freie Position im Arbeitsspeicher bei. Man kann sich verwaltete Heapzuordnungen als "stapelartig" vorstellen. Eine Zuordnung kann zu einer Auflistung führen, wenn die GC Arbeitsspeicher freigeben muss, um das neue Objekt zuzuweisen. In diesem Fall ist die Zuordnung teurer als ein malloc oder new. Objekte, die angeheftet sind, können sich auch auf die Zuordnungskosten auswirken. Angeheftete Objekte sind Objekte, die der GC angewiesen wurde, während einer Auflistung nicht zu verschieben, in der Regel, weil die Adresse des Objekts an eine native API übergeben wurde.

Im Gegensatz zu einem malloc oder newfallen Kosten an, die mit der Verwaltung des Arbeitsspeichers über die Lebensdauer eines Objekts verbunden sind. Die CLR GC ist generational, was bedeutet, dass nicht immer der gesamte Heap erfasst wird. Der GC muss jedoch weiterhin wissen, ob live-Objekte in den restlichen Heapstammobjekten im Teil des gesammelten Heaps vorhanden sind. Speicher, der Objekte enthält, die Verweise auf Objekte in jüngeren Generationen enthalten, ist teuer, während der Lebensdauer der Objekte zu verwalten.

Der GC ist eine Generationsmarkierung und ein Sweep Garbage Collector. Der verwaltete Heap enthält drei Generationen; Generation 0 enthält alle neuen Objekte, Generation 1 enthält etwas langlebige Objekte und Generation 2 enthält langlebige Objekte. Die GC sammelt den kleinsten Abschnitt des Heaps, der möglich ist, um genügend Arbeitsspeicher freizugeben, damit die Anwendung fortgesetzt werden kann. Die Sammlung einer Generation umfasst die Sammlung aller jüngeren Generationen, in diesem Fall sammelt eine Generation 1-Sammlung auch Generation 0. Generation 0 wird dynamisch entsprechend der Größe des Prozessorcaches und der Zuordnungsrate der Anwendung angepasst und benötigt in der Regel weniger als 10 Millisekunden. Generation 1 wird dynamisch entsprechend der Zuordnungsrate der Anwendung dimensioniert und dauert in der Regel zwischen 10 und 30 Millisekunden. Die Größe der Generation 2 hängt vom Zuordnungsprofil Ihrer Anwendung sowie vom Zeitaufwand für die Erfassung ab. Es sind diese Sammlungen der Generation 2, die die Leistungskosten für die Verwaltung des Arbeitsspeichers Ihrer Anwendungen am stärksten beeinflussen.

HINWEIS Die GC ist selbstoptimiert und passt sich entsprechend den Arbeitsspeicheranforderungen der Anwendungen an. In den meisten Fällen behindert das programmgesteuerte Aufrufen einer GC diese Optimierung. "Hilfe" für den GC durch Aufrufen von GC. Sammeln verbessert die Leistung Ihrer Anwendungen höchstwahrscheinlich nicht.

Die GC kann Liveobjekte während einer Auflistung verschieben. Wenn diese Objekte groß sind, sind die Kosten für das Verschieben hoch, sodass diese Objekte in einem speziellen Bereich des Heaps zugeordnet werden, der als Großer Objektheap bezeichnet wird. Der Große Objektheap wird gesammelt, aber nicht komprimiert, z. B. werden große Objekte nicht verschoben. Große Objekte sind Objekte, die größer als 80 KB sind. Beachten Sie, dass sich dies in zukünftigen Versionen der CLR ändern kann. Wenn der Heap für große Objekte gesammelt werden muss, erzwingt erzwingt eine vollständige Auflistung, und der Heap für große Objekte wird während gen 2-Auflistungen gesammelt. Die Zuordnungs- und Todesrate von Objekten im Heap "Große Objekte" kann sich erheblich auf die Leistungskosten für die Verwaltung des Anwendungsspeichers auswirken.

Zuordnungsprofile

Das Gesamtzuordnungsprofil einer verwalteten Anwendung definiert, wie hart der Garbage Collector arbeiten muss, um den der Anwendung zugeordneten Arbeitsspeicher zu verwalten. Je schwieriger der GC zum Verwalten des Arbeitsspeichers arbeiten muss, desto größer ist die Anzahl der CPU-Zyklen, die der GC benötigt, und desto weniger Zeit wird die CPU für die Ausführung des Anwendungscodes aufwenden. Das Zuordnungsprofil ist eine Funktion der Anzahl der zugeordneten Objekte, der Größe dieser Objekte und ihrer Lebensdauer. Die naheliegendste Möglichkeit, den GC-Druck zu entlasten, besteht darin, einfach weniger Objekte zuzuweisen. Anwendungen, die auf Erweiterbarkeit, Modularität und Wiederverwendung mit objektorientierten Entwurfstechniken ausgelegt sind, werden fast immer zu einer erhöhten Anzahl von Zuordnungen führen. Es gibt eine Leistungsstrafe für Abstraktion und "Eleganz".

Ein GC-freundliches Zuordnungsprofil enthält einige Objekte, die am Anfang der Anwendung zugeordnet sind und dann für die Lebensdauer der Anwendung überleben, und alle anderen Objekte sind kurzlebig. Langlebige Objekte enthalten nur wenige oder keine Verweise auf kurzlebige Objekte. Da das Zuordnungsprofil von diesem abweicht, muss der GC härter arbeiten, um den Anwendungsspeicher zu verwalten.

Ein GC-unfreundliches Zuordnungsprofil weist viele Objekte auf, die bis zur 2. Generation überleben und dann sterben, oder es werden viele kurzlebige Objekte im Heap "Großes Objekt" zugeordnet. Objekte, die lange genug überleben, um in die 2. Generation zu gelangen und dann zu sterben, sind die teuersten zu verwalten. Wie bereits erwähnt, erhöhen auch Objekte in älteren Generationen, die Verweise auf Objekte in jüngeren Generationen während eines GC enthalten, die Kosten der Sammlung.

Ein typisches reales Zuordnungsprofil liegt irgendwo zwischen den beiden oben genannten Zuordnungsprofilen. Eine wichtige Metrik Ihres Zuordnungsprofils ist der Prozentsatz der gesamten CPU-Zeit, die in GC aufgewendet wird. Sie können diese Zahl aus dem .NET CLR-Arbeitsspeicher abrufen: % Time in GC-Leistungsindikator . Wenn der Mittelwert dieses Zählers über 30 % liegt, sollten Sie sich Ihr Zuordnungsprofil wahrscheinlich genauer ansehen. Dies bedeutet nicht unbedingt, dass Ihr Zuordnungsprofil "schlecht" ist; es gibt einige speicherintensive Anwendungen, bei denen diese Gc-Ebene erforderlich und angemessen ist. Dieser Leistungsindikator sollte das erste Sein, das Sie sich ansehen, wenn Leistungsprobleme auftreten. Es sollte sofort angezeigt werden, ob Ihr Zuordnungsprofil Teil des Problems ist.

HINWEIS Wenn der Leistungsindikator .NET CLR Memory: % Time in GC angibt, dass Ihre Anwendung durchschnittlich mehr als 30 % ihrer Zeit in GC aufwendet, sollten Sie sich Ihr Zuordnungsprofil genauer ansehen.

HINWEIS Eine GC-freundliche Anwendung verfügt über deutlich mehr Sammlungen der Generation 0 als die 2. Generation. Dieses Verhältnis kann durch Vergleich der Leistungsindikatoren NET CLR Memory: # Gen0 Collections und NET CLR Memory: # Gen2 Collections ermittelt werden.

Profilerstellungs-API und CLR-Profiler

Die CLR enthält eine leistungsstarke Profilerstellungs-API, die es Dritten ermöglicht, benutzerdefinierte Profiler für verwaltete Anwendungen zu schreiben. Der CLR Profiler ist ein nicht unterstütztes Zuordnungsprofilerstellungstool, das vom CLR-Produktteam geschrieben wurde und diese Profilerstellungs-API verwendet. Mit dem CLR-Profiler können Entwickler das Zuordnungsprofil ihrer verwalteten Anwendungen anzeigen.

Abbildung 1 Hauptfenster des CLR-Profilers

Der CLR-Profiler enthält eine Reihe sehr nützlicher Ansichten des Zuordnungsprofils, darunter ein Histogramm der zugeordneten Typen, Zuordnungs- und Aufrufdiagramme, eine Zeitlinie, die GCs verschiedener Generationen und den resultierenden Zustand des verwalteten Heaps nach diesen Sammlungen anzeigt, sowie eine Aufrufstruktur, die Zuordnungen und Assemblylasten pro Methode anzeigt.

Abbildung 2: Zuordnungsdiagramm für CLR-Profiler

HINWEIS Ausführliche Informationen zur Verwendung des CLR-Profilers finden Sie in der Infodatei, die in der ZIP-Datei enthalten ist.

Beachten Sie, dass der CLR Profiler einen hohen Leistungsaufwand aufweist und die Leistungsmerkmale Ihrer Anwendung erheblich ändert. Auftretende Stressfehler werden wahrscheinlich verschwinden, wenn Sie Ihre Anwendung mit dem CLR Profiler ausführen.

Hosten der Server-GC

Für die CLR stehen zwei verschiedene Garbage Collectors zur Verfügung: eine Arbeitsstations-GC und eine Server-GC. Konsolen- und Windows Forms anwendungen hosten die Arbeitsstations-GC, und ASP.NET hostet die Server-GC. Die Server-GC ist für Durchsatz und Skalierbarkeit mit mehreren Prozessoren optimiert. Die Server-GC hält alle Threads an, die verwalteten Code ausführen, während der gesamten Dauer einer Sammlung, einschließlich der Mark- und Sweep-Phase, und GC erfolgt parallel auf allen CPU-Prozessoren, die für den Prozess verfügbar sind, in dedizierten Threads mit hoher CPU-Affinität. Wenn Threads nativen Code während einer GC ausführen, werden diese Threads nur angehalten, wenn der native Aufruf zurückgibt. Wenn Sie eine Serveranwendung erstellen, die auf Multiprozessorcomputern ausgeführt wird, wird dringend empfohlen, die Server-GC zu verwenden. Wenn Ihre Anwendung in nicht von ASP.NET gehostet wird, müssen Sie eine native Anwendung schreiben, die die CLR explizit hostet.

HINWEIS Wenn Sie skalierbare Serveranwendungen erstellen, hosten Sie die Server-GC. Weitere Informationen finden Sie unter Implementieren eines benutzerdefinierten Common Language Runtime-Hosts für Ihre verwaltete App.

Die Arbeitsstations-GC ist für niedrige Latenzen optimiert, die in der Regel für Clientanwendungen erforderlich ist. Man möchte keine spürbare Pause in einer Clientanwendung während eines GC, da die Clientleistung in der Regel nicht durch rohen Durchsatz, sondern durch die wahrgenommene Leistung gemessen wird. Die Arbeitsstations-GC führt gleichzeitig gc aus, was bedeutet, dass die Markierungsphase ausgeführt wird, während verwalteter Code noch ausgeführt wird. Der GC hält Threads, die verwalteten Code ausführen, nur an, wenn die Sweepphase ausgeführt werden muss. In der Arbeitsstations-GC erfolgt GC nur für einen Thread und daher nur für eine CPU.

Abschluss

Die CLR bietet einen Mechanismus, bei dem sauber automatisch erfolgt, bevor der Speicher, der einem instance eines Typs zugeordnet ist, freigegeben wird. Dieser Mechanismus wird als Finalization bezeichnet. In der Regel wird Finalization verwendet, um native Ressourcen freizugeben, in diesem Fall Datenbankverbindungen- oder Betriebssystemhandles, die von einem Objekt verwendet werden.

Die Finalisierung ist ein teures Feature und erhöht den Druck, der auf den GC ausgeübt wird. Der GC verfolgt die Objekte, die eine Finalisierung erfordern, in einer finalisierbaren Warteschlange nach. Wenn der GC während einer Auflistung ein Objekt findet, das nicht mehr aktiv ist, aber eine Finalisierung erfordert, wird der Eintrag dieses Objekts in der Finalizable Queue in die FReachable-Warteschlange verschoben. Die Finalisierung erfolgt in einem separaten Thread namens Finalizer Thread. Da der gesamte Zustand des Objekts während der Ausführung des Finalizers möglicherweise erforderlich ist, werden das Objekt und alle Objekte, auf die es verweist, zur nächsten Generation heraufgestuft. Der dem Objekt zugeordnete Arbeitsspeicher oder das Diagramm von Objekten wird nur während der folgenden Gc freigegeben.

Ressourcen, die freigegeben werden müssen, sollten in ein möglichst kleines finalisierbares Objekt eingeschlossen werden. wenn instance ihre Klasse Verweise auf verwaltete und nicht verwaltete Ressourcen erfordert, sollten Sie die nicht verwalteten Ressourcen in eine neue Finalizable-Klasse umschließen und diese Klasse zu einem Mitglied Ihrer Klasse machen. Die übergeordnete Klasse sollte nicht finalisierbar sein. Dies bedeutet, dass nur die Klasse, die die nicht verwalteten Ressourcen enthält, höhergestuft wird (vorausgesetzt, Sie enthalten keinen Verweis auf die übergeordnete Klasse in der Klasse, die die nicht verwalteten Ressourcen enthält). Zu beachten ist auch, dass es nur einen Finalization Thread gibt. Wenn ein Finalizer bewirkt, dass dieser Thread blockiert wird, werden die nachfolgenden Finalizer nicht aufgerufen, Ressourcen werden nicht freigegeben, und Ihre Anwendung wird auslaufen.

HINWEIS Finalizer sollten so einfach wie möglich gehalten werden und niemals blockieren.

HINWEIS Machen Sie nur die Wrapperklasse für nicht verwaltete Objekte fertig, die bereinigungswürdig sind.

Die Finalisierung kann als Alternative zum Referenzzählen betrachtet werden. Ein Objekt, das die Verweiszählung implementiert, verfolgt, wie viele andere Objekte Verweise darauf haben (was zu einigen sehr bekannten Problemen führen kann), sodass es seine Ressourcen freigeben kann, wenn die Verweisanzahl 0 ist. Die CLR implementiert keine Verweiszählung, sodass sie einen Mechanismus bereitstellen muss, um Ressourcen automatisch freizugeben, wenn keine Verweise mehr auf das Objekt gespeichert werden. Finalisierung ist dieser Mechanismus. Die Finalisierung ist in der Regel nur dann erforderlich, wenn die Lebensdauer eines Objekts, das sauber erfordert, nicht explizit bekannt ist.

Das Dispose-Muster

Falls die Lebensdauer des Objekts explizit bekannt ist, sollten die nicht verwalteten Ressourcen, die einem Objekt zugeordnet sind, eifrig freigegeben werden. Dies wird als "Disposing" des -Objekts bezeichnet. Das Dispose-Muster wird über die IDisposable-Schnittstelle implementiert (obwohl die Implementierung selbst trivial wäre). Wenn Sie die eifrige Finalisierung für Ihre Klasse verfügbar machen möchten, z. B. Instanzen Ihrer Klasse verwerfbar machen möchten, müssen Sie von Ihrem Objekt die IDisposable-Schnittstelle implementieren und eine Implementierung für die Dispose-Methode bereitstellen. In der Dispose-Methode rufen Sie den gleichen Bereinigungscode auf, der sich im Finalizer befindet, und informieren den GC, dass das Objekt nicht mehr abgeschlossen werden muss, indem Sie den GC aufrufen. SuppressFinalization-Methode . Es empfiehlt sich, dass sowohl die Dispose-Methode als auch der Finalizer eine gemeinsame Endisierungsfunktion aufrufen, sodass nur eine Version des sauber-Up-Codes beibehalten werden muss. Wenn die Semantik des Objekts so ist, dass eine Close-Methode logischer als eine Dispose-Methode ist, sollte auch eine Close-Methode implementiert werden. in diesem Fall ist eine Datenbankverbindung oder ein Socket logisch "geschlossen". Close kann einfach die Dispose-Methode aufrufen.

Es empfiehlt sich immer, eine Dispose-Methode für Klassen mit einem Finalizer bereitzustellen. man kann nie sicher sein, wie diese Klasse für instance verwendet wird, ob ihre Lebensdauer explizit bekannt ist oder nicht. Wenn eine Klasse, die Sie verwenden, das Dispose-Muster implementiert und Sie explizit wissen, wann Sie mit dem Objekt fertig sind, rufen Sie unbedingt Dispose auf.

HINWEIS Stellen Sie eine Dispose-Methode für alle Klassen bereit, die finalisierbar sind.

HINWEIS Unterdrücken Sie die Finalisierung in Ihrer Dispose-Methode .

HINWEIS Rufen Sie eine allgemeine Bereinigungsfunktion auf.

HINWEIS Wenn ein von Ihnen verwendetes Objekt IDisposable implementiert und Sie wissen, dass das Objekt nicht mehr benötigt wird, rufen Sie Dispose auf.

C# bietet eine sehr bequeme Möglichkeit, Objekte automatisch zu entsorgen. Mit using der Schlüsselwort (keyword) können Sie einen Codeblock identifizieren, nach dem Dispose für eine Reihe von einwegbaren Objekten aufgerufen wird.

C# verwendet Schlüsselwort (keyword)

using(DisposableType T)
{
   //Do some work with T
}
//T.Dispose() is called automatically

Hinweis zu schwachen Verweisen

Jeder Verweis auf ein Objekt, das sich auf dem Stapel, in einem Register, in einem anderen Objekt oder in einem der anderen GC Roots befindet, hält ein Objekt während eines GC am Leben. Dies ist in der Regel eine sehr gute Sache, wenn man bedenkt, dass dies in der Regel bedeutet, dass Ihre Anwendung mit diesem Objekt nicht fertig ist. Es gibt jedoch Fälle, in denen Sie einen Verweis auf ein Objekt haben möchten, dessen Lebensdauer aber nicht beeinflussen möchten. In diesen Fällen stellt die CLR einen Mechanismus namens Schwache Verweise bereit, um genau dies zu tun. Jeder starke Verweis – für instance ein Verweis, der ein Objekt verwurzelt – kann in einen schwachen Verweis umgewandelt werden. Ein Beispiel für schwache Verweise ist, wenn Sie ein externes Cursorobjekt erstellen möchten, das eine Datenstruktur durchlaufen kann, aber die Lebensdauer des Objekts nicht beeinflussen sollte. Ein weiteres Beispiel ist, wenn Sie einen Cache erstellen möchten, der bei Arbeitsspeicherauslastung geleert wird. für instance, wenn eine GC auftritt.

Erstellen eines schwachen Verweises in C#

MyRefType mrt = new MyRefType();
//...

//Create weak reference
WeakReference wr = new WeakReference(mrt); 
mrt = null; //object is no longer rooted
//...

//Has object been collected?
if(wr.IsAlive)
{
   //Get a strong reference to the object
   mrt = wr.Target;
   //object is rooted and can be used again
}
else
{
   //recreate the object
   mrt = new MyRefType();
}

Verwalteter Code und der CLR-JIT

Verwaltete Assemblys, die die Verteilungseinheit für verwalteten Code sind, enthalten eine prozessorunabhängige Sprache namens Microsoft Intermediate Language (MSIL oder IL). Der CLR Just-In-Time (JIT) kompiliert die IL in optimierte native X86-Anweisungen. Der JIT ist ein optimierender Compiler, aber da die Kompilierung zur Laufzeit erfolgt und nur beim ersten Aufruf einer Methode erfolgt, muss die Anzahl der Optimierungen, die sie ausführt, mit der Zeit ausgeglichen werden, die für die Kompilierung benötigt wird. In der Regel ist dies für Serveranwendungen nicht wichtig, da die Startzeit und Die Reaktionsfähigkeit im Allgemeinen kein Problem ist, aber für Clientanwendungen wichtig ist. Beachten Sie, dass die Startzeit verbessert werden kann, indem Sie die Kompilierung zur Installationszeit mithilfe von NGEN.exe durchführen.

Viele der vom JIT durchgeführten Optimierungen weisen keine programmgesteuerten Muster auf. Für instance können Sie sie nicht explizit programmieren, aber es gibt eine Zahl, die dies tut. Im nächsten Abschnitt werden einige dieser Optimierungen erläutert.

HINWEIS Verbessern Sie die Startzeit von Clientanwendungen, indem Sie Ihre Anwendung zum Zeitpunkt der Installation kompilieren, indem Sie das Hilfsprogramm NGEN.exe verwenden.

Methodeninlining

Mit Methodenaufrufen fallen Kosten an. Argumente müssen auf den Stapel gepusht oder in Registern gespeichert werden, der Methodenprolog und der Epilog müssen ausgeführt werden usw. Die Kosten dieser Aufrufe können für bestimmte Methoden vermieden werden, indem der Methodentext der aufgerufenen Methode einfach in den Text des Aufrufers verschoben wird. Dies wird als Method In-lining bezeichnet. Der JIT verwendet eine Reihe von Heuristiken, um zu entscheiden, ob eine Methode inlineiert werden soll. Im Folgenden finden Sie eine Liste der wichtigeren (beachten Sie, dass dies nicht erschöpfend ist):

  • Methoden, die größer als 32 Byte il sind, werden nicht inlineiert.
  • Virtuelle Funktionen sind nicht inlineiert.
  • Methoden, die über eine komplexe Flusssteuerung verfügen, werden nicht inlineiert. Bei der komplexen Flusssteuerung handelt es sich um eine andere Flusssteuerung als if/then/else; in diesem Fall oder switchwhile.
  • Methoden, die Ausnahmebehandlungsblöcke enthalten, sind nicht inlined, obwohl Methoden, die Ausnahmen auslösen, weiterhin für das Inlining geeignet sind.
  • Wenn es sich bei einem der formalen Argumente der Methode um Strukturen handelt, wird die Methode nicht inlineiert.

Ich würde sorgfältig erwägen, explizit für diese Heuristiken zu programmieren, da sie sich in zukünftigen Versionen des JIT ändern könnten. Gefährden Sie nicht die Richtigkeit der Methode, um zu gewährleisten, dass sie inline ist. Es ist interessant zu beachten, dass die inline Schlüsselwörter und __inline in C++ nicht garantieren, dass der Compiler eine Methode inline eingibt (obwohl __forceinline dies der Fall ist).

Property get- und set-Methoden sind im Allgemeinen gute Kandidaten für das Inlining, da sie in der Regel nur private Datenmember initialisieren.

**HINT ** Gefährden Sie nicht die Richtigkeit einer Methode, um das Inlining zu gewährleisten.

Entfernung der Bereichsprüfung

Einer der vielen Vorteile von verwaltetem Code ist die automatische Bereichsüberprüfung. Jedes Mal, wenn Sie mithilfe der Array[Index]-Semantik auf ein Array zugreifen, gibt der JIT eine Überprüfung aus, um sicherzustellen, dass sich der Index in den Grenzen des Arrays befindet. Im Kontext von Schleifen mit einer großen Anzahl von Iterationen und einer geringen Anzahl von Anweisungen, die pro Iteration ausgeführt werden, können diese Bereichsüberprüfungen teuer sein. Es gibt Fälle, in denen der JIT erkennt, dass diese Bereichsüberprüfungen unnötig sind, und die Überprüfung aus dem Textkörper der Schleife entfernt, indem er nur einmal überprüft wird, bevor die Ausführung der Schleife beginnt. In C# gibt es ein programmgesteuertes Muster, um sicherzustellen, dass diese Bereichsüberprüfungen eliminiert werden: Explizites Testen der Länge des Arrays in der "for"-Anweisung. Beachten Sie, dass subtile Abweichungen von diesem Muster dazu führen, dass die Überprüfung nicht beseitigt wird, und in diesem Fall wird dem Index ein Wert hinzugefügt.

Entfernung der Bereichsprüfung in C#

//Range check will be eliminated
for(int i = 0; i < myArray.Length; i++) 
{
   Console.WriteLine(myArray[i].ToString());
}

//Range check will NOT be eliminated
for(int i = 0; i < myArray.Length + y; i++) 
{ 
   Console.WriteLine(myArray[i+x].ToString());
}

Die Optimierung ist besonders bei der Suche nach großen gezackten Arrays nach instance spürbar, da sowohl die Bereichsprüfung der inneren als auch der äußeren Schleife entfällt.

Optimierungen, die die Nachverfolgung der variablen Nutzung erfordern

Eine Reihe von JIT-Compileroptimierungen erfordert, dass der JIT die Verwendung formaler Argumente und lokaler Variablen nachverfolgt. Beispielsweise, wann sie zum ersten Mal verwendet werden und wann sie zuletzt im Text der -Methode verwendet werden. In Version 1.0 und 1.1 der CLR gibt es eine Beschränkung von 64 für die Gesamtzahl der Variablen, für die die JIT die Nutzung nachverfolgt. Ein Beispiel für eine Optimierung, die eine Nutzungsnachverfolgung erfordert, ist Enregistration. Die Registrierung erfolgt, wenn Variablen in Prozessorregistern und nicht im Stapelframe gespeichert werden, z. B. im RAM. Der Zugriff auf die registrierten Variablen ist erheblich schneller als im Stapelframe, auch wenn sich die Variable im Frame im Prozessorcache befindet. Nur 64 Variablen werden für die Registrierung berücksichtigt; alle anderen Variablen werden auf den Stapel gepusht. Es gibt andere Optimierungen als Enregistration, die von der Nutzungsnachverfolgung abhängen. Die Anzahl der formalen Argumente und lokalen Argumente für eine Methode sollte unter 64 gehalten werden, um die maximale Anzahl von JIT-Optimierungen sicherzustellen. Beachten Sie, dass sich diese Nummer bei zukünftigen Versionen der CLR ändern kann.

HINWEIS Halten Sie Methoden kurz. Es gibt eine Reihe von Gründen dafür, einschließlich Methodeninlining, Registrierung und JIT-Dauer.

Andere JIT-Optimierungen

Der JIT-Compiler führt eine Reihe weiterer Optimierungen durch: Konstante und Kopierweitergabe, invariantes Schleifen-Hoisting und mehrere andere. Es gibt keine expliziten Programmiermuster, die Sie verwenden müssen, um diese Optimierungen zu erhalten. sie sind kostenlos.

Warum werden diese Optimierungen in Visual Studio nicht angezeigt?

Wenn Sie start über das Menü Debuggen verwenden oder F5 drücken, um eine Anwendung in Visual Studio zu starten, unabhängig davon, ob Sie eine Release- oder Debugversion erstellt haben, werden alle JIT-Optimierungen deaktiviert. Wenn eine verwaltete Anwendung von einem Debugger gestartet wird, auch wenn es sich nicht um einen Debugbuild der Anwendung handelt, gibt der JIT nicht optimierte x86-Anweisungen aus. Wenn Sie möchten, dass der JIT optimierten Code ausgibt, starten Sie die Anwendung über die Windows-Explorer, oder verwenden Sie STRG+F5 in Visual Studio. Wenn Sie die optimierte Disassemblierung anzeigen und mit dem nicht optimierten Code vergleichen möchten, können Sie cordbg.exe verwenden.

HINWEIS Verwenden Sie cordbg.exe, um die Disassemblierung sowohl des optimierten als auch des nicht optimierten Codes anzuzeigen, der vom JIT ausgegeben wird. Nachdem Sie die Anwendung mit cordbg.exe gestartet haben, können Sie den JIT-Modus festlegen, indem Sie Folgendes eingeben:

(cordbg) mode JitOptimizations 1
JIT's will produce optimized code

(cordbg) mode JitOptimizations 0

JITs erzeugen debugfähigen (nicht optimierten) Code.

Werttypen

Die CLR macht zwei unterschiedliche Typensätze verfügbar: Verweistypen und Werttypen. Verweistypen werden immer auf dem verwalteten Heap zugeordnet und als Verweis übergeben (wie der Name schon sagt). Werttypen werden im Stapel oder inline als Teil eines Objekts auf dem Heap zugeordnet und standardmäßig als Wert übergeben, obwohl Sie sie auch als Verweis übergeben können. Werttypen sind sehr billig zuzuordnen, und wenn sie klein und einfach gehalten werden, sind sie billig, als Argumente zu übergeben. Ein gutes Beispiel für eine geeignete Verwendung von Werttypen wäre ein Point-Werttyp, der eine x- und y-Koordinate enthält.

Punktwerttyp

struct Point
{
   public int x;
   public int y;
   
   //
}

Werttypen können auch als Objekte behandelt werden. für instance können Objektmethoden aufgerufen werden, sie können in ein Objekt umgewandelt oder an der Stelle übergeben werden, an der ein Objekt erwartet wird. In diesem Fall wird der Werttyp jedoch über einen Prozess namens Boxing in einen Verweistyp konvertiert. Wenn ein Werttyp Boxed ist, wird dem verwalteten Heap ein neues Objekt zugeordnet, und der Wert wird in das neue Objekt kopiert. Dies ist ein kostspieliger Vorgang, der die durch die Verwendung von Werttypen gewonnene Leistung verringern oder vollständig negieren kann. Wenn der Boxed-Typ implizit oder explizit in einen Werttyp umgewandelt wird, lautet er Unboxed.

Feld-/Unbox-Werttyp

C#:

int BoxUnboxValueType()
{
   int i = 10;
   object o = (object)i; //i is Boxed
   return (int)o + 3; //i is Unboxed
}

MSIL:

.method private hidebysig instance int32
        BoxUnboxValueType() cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals init (int32 V_0,
           object V_1)
  IL_0000:  ldc.i4.s   10
  IL_0002:  stloc.0
  IL_0003:  ldloc.0
  IL_0004:  box        [mscorlib]System.Int32
  IL_0009:  stloc.1
  IL_000a:  ldloc.1
  IL_000b:  unbox      [mscorlib]System.Int32
  IL_0010:  ldind.i4
  IL_0011:  ldc.i4.3
  IL_0012:  add
  IL_0013:  ret
} // end of method Class1::BoxUnboxValueType

Wenn Sie benutzerdefinierte Werttypen (Struktur in C#) implementieren, sollten Sie erwägen, die ToString-Methode zu überschreiben. Wenn Sie diese Methode nicht überschreiben, führen Aufrufe von ToString für Ihren Werttyp dazu, dass der Typ boxed wird. Dies gilt auch für die anderen Methoden, die von System.Object geerbt werden, in diesem Fall Equals, obwohl ToString wahrscheinlich die am häufigsten aufgerufene Methode ist. Wenn Sie wissen möchten, ob und wann Ihr Werttyp boxed ist, können Sie mithilfe des box Hilfsprogramms ildasm.exe (wie im obigen Codeausschnitt) nach der Anweisung in der MSIL suchen.

Überschreiben der ToString()-Methode in C# zur Verhinderung von Boxing

struct Point
{
   public int x;
   public int y;

   //This will prevent type being boxed when ToString is called
   public override string ToString()
   {
      return x.ToString() + "," + y.ToString();
   }
}

Beachten Sie, dass beim Erstellen von Sammlungen – z. B. eine ArrayList von float – jedes Element boxed wird, wenn es der Auflistung hinzugefügt wird. Sie sollten erwägen, ein Array zu verwenden oder eine benutzerdefinierte Sammlungsklasse für Ihren Werttyp zu erstellen.

Implizites Boxen bei Verwendung von Auflistungsklassen in C#

ArrayList al = new ArrayList();
al.Add(42.0F); //Implicitly Boxed becuase Add() takes object
float f = (float)al[0]; //Unboxed

Ausnahmebehandlung

Es ist üblich, Fehlerbedingungen als normale Flusssteuerung zu verwenden. In diesem Fall können Sie beim Versuch, einem Active Directory-instance programmgesteuert einen Benutzer hinzuzufügen, einfach versuchen, den Benutzer hinzuzufügen. Wenn ein E_ADS_OBJECT_EXISTS HRESULT zurückgegeben wird, wissen Sie, dass er bereits im Verzeichnis vorhanden ist. Alternativ können Sie das Verzeichnis nach dem Benutzer durchsuchen und den Benutzer dann nur hinzufügen, wenn die Suche fehlschlägt.

Diese Verwendung von Fehlern für die normale Flusssteuerung ist ein Leistungs-Antimuster im Kontext der CLR. Die Fehlerbehandlung in der CLR erfolgt mit strukturierter Ausnahmebehandlung. Verwaltete Ausnahmen sind sehr günstig, bis Sie sie auslösen. Wenn in der CLR eine Ausnahme ausgelöst wird, ist ein Stapellauf erforderlich, um einen geeigneten Ausnahmehandler für die ausgelöste Ausnahme zu finden. Stapellauf ist ein teurer Vorgang. Ausnahmen sollten verwendet werden, wie ihr Name schon sagt; in Ausnahmefällen oder unerwarteten Umständen.

**HINT **Erwägen Sie die Rückgabe eines enumerierten Ergebnisses für erwartete Ergebnisse, anstatt eine Ausnahme für leistungskritische Methoden auszulösen.

**HINT **Es gibt eine Reihe von Leistungsindikatoren für .NET CLR-Ausnahmen, die Ihnen mitteilen, wie viele Ausnahmen in Ihrer Anwendung ausgelöst werden.

**HINT **Wenn Sie VB.NET verwenden, verwenden Sie Ausnahmen anstelle On Error Gotovon ; das Fehlerobjekt ist eine unnötige Kosten.

Threading und Synchronisierung

Die CLR macht umfangreiche Threading- und Synchronisierungsfeatures verfügbar, einschließlich der Möglichkeit, eigene Threads, einen Threadpool und verschiedene Synchronisierungsgrundtypen zu erstellen. Bevor Sie die Threadingunterstützung in der CLR nutzen, sollten Sie die Verwendung von Threads sorgfältig in Betracht ziehen. Beachten Sie, dass das Hinzufügen von Threads ihren Durchsatz tatsächlich reduzieren kann, anstatt ihn zu erhöhen, und Sie können sicher sein, dass die Speicherauslastung erhöht wird. In Serveranwendungen, die auf Computern mit mehreren Prozessoren ausgeführt werden, kann das Hinzufügen von Threads den Durchsatz durch Parallelisierung der Ausführung erheblich verbessern (obwohl dies davon abhängt, wie viel Sperrenkonflikte auftreten, z. B. die Serialisierung der Ausführung), und in Clientanwendungen kann das Hinzufügen eines Threads zum Anzeigen von Aktivität und/oder Fortschritt die wahrgenommene Leistung (bei geringen Durchsatzkosten) verbessern.

Wenn die Threads in Ihrer Anwendung nicht auf eine bestimmte Aufgabe spezialisiert sind oder ihnen ein spezieller Zustand zugeordnet ist, sollten Sie die Verwendung des Threadpools in Betracht ziehen. Wenn Sie den Win32-Threadpool in der Vergangenheit verwendet haben, ist Ihnen der Threadpool der CLR sehr vertraut. Es gibt eine einzelne instance des Threadpools pro verwaltetem Prozess. Der Threadpool ist hinsichtlich der Anzahl der von ihm erstellten Threads intelligent und optimiert sich entsprechend der Auslastung auf dem Computer.

Threading kann nicht diskutiert werden, ohne die Synchronisierung zu diskutieren. Alle Durchsatzgewinne, die multithreading Ihrer Anwendung bieten können, können durch schlecht geschriebene Synchronisierungslogik negiert werden. Die Granularität von Sperren kann sich erheblich auf den Gesamtdurchsatz Ihrer Anwendung auswirken, sowohl aufgrund der Kosten für das Erstellen und Verwalten der Sperre als auch aufgrund der Tatsache, dass Sperren die Ausführung potenziell serialisieren können. Ich verwende das Beispiel des Versuchs, einer Struktur einen Knoten hinzuzufügen, um diesen Punkt zu veranschaulichen. Wenn es sich bei der Struktur um eine freigegebene Datenstruktur handelt, benötigen für instance während der Ausführung der Anwendung mehrere Threads Zugriff darauf, und Sie müssen den Zugriff auf die Struktur synchronisieren. Sie können die gesamte Struktur sperren, während Sie einen Knoten hinzufügen. Dies bedeutet, dass nur die Kosten für das Erstellen einer einzelnen Sperre anfallen, aber andere Threads, die versuchen, auf die Struktur zuzugreifen, werden wahrscheinlich blockiert. Dies wäre ein Beispiel für eine grobkörnige Sperre. Alternativ können Sie jeden Knoten während der Durchquerung der Struktur sperren. Dies würde bedeuten, dass ihnen die Kosten für das Erstellen einer Sperre pro Knoten entstehen, aber andere Threads würden nicht blockiert, es sei denn, sie versuchten, auf den bestimmten Knoten zuzugreifen, den Sie gesperrt hatten. Dies ist ein Beispiel für eine differenzierte Sperre. Wahrscheinlich wäre es eine geeignetere Granularität der Sperre, nur die Unterstruktur zu sperren, in der Sie arbeiten. Beachten Sie, dass Sie in diesem Beispiel wahrscheinlich eine freigegebene Sperre (RWLock) verwenden würden, da mehrere Leser gleichzeitig Zugriff erhalten sollten.

Die einfachste und leistungsstärkste Methode für synchronisierte Vorgänge ist die Verwendung der System.Threading.Interlocked-Klasse. Die Interlocked-Klasse macht eine Reihe von atomaren Vorgängen auf niedriger Ebene verfügbar: Inkrement, Decrement, Exchange und CompareExchange.

Verwenden der System.Threading.Interlocked-Klasse in C#

using System.Threading;
//...
public class MyClass
{
   void MyClass() //Constructor
   {
      //Increment a global instance counter atomically
      Interlocked.Increment(ref MyClassInstanceCounter);
   }

   ~MyClass() //Finalizer
   {
      //Decrement a global instance counter atomically
      Interlocked.Decrement(ref MyClassInstanceCounter);
      //... 
   }
   //...
}

Der wahrscheinlich am häufigsten verwendete Synchronisierungsmechanismus ist der Abschnitt Überwachen oder kritisch. Eine Monitorsperre kann direkt oder mithilfe der lock Schlüsselwort (keyword) in C# verwendet werden. Die lock Schlüsselwort (keyword) synchronisiert den Zugriff für das angegebene Objekt mit einem bestimmten Codeblock. Eine Monitorsperre, die ziemlich leicht umkämpft ist, ist aus Leistungssicht relativ günstig, wird aber teurer, wenn sie stark umkämpft ist.

C#-Schlüsselwort (keyword)

//Thread will attempt to obtain the lock
//and block until it does
lock(mySharedObject)
{
   //A thread will only be able to execute the code
   //within this block if it holds the lock
}//Thread releases the lock

Der RWLock bietet einen gemeinsamen Sperrmechanismus: Beispielsweise können "Leser" die Sperre mit anderen "Readern" teilen, ein "Writer" jedoch nicht. In den Fällen, in denen dies anwendbar ist, kann der RWLock zu einem besseren Durchsatz führen als die Verwendung eines Monitors, wodurch nur ein einzelner Reader oder Writer die Sperre gleichzeitig erhält. Der System.Threading-Namespace enthält auch die Mutex-Klasse. Ein Mutex ist ein Synchronisierungsgrundtyp, der eine prozessübergreifende Synchronisierung ermöglicht. Beachten Sie, dass dies erheblich teurer ist als ein kritischer Abschnitt und nur dann verwendet werden sollte, wenn eine prozessübergreifende Synchronisierung erforderlich ist.

Spiegelung

Reflektion ist ein von der CLR bereitgestellter Mechanismus, mit dem Sie Typinformationen programmgesteuert zur Laufzeit abrufen können. Die Reflektion hängt stark von Metadaten ab, die in verwaltete Assemblys eingebettet sind. Viele Reflektions-APIs erfordern die Suche und Analyse der Metadaten, was kostspielige Vorgänge sind.

Die Reflektions-APIs können in drei Leistungsbuckets gruppiert werden. Typvergleich, Memberenumeration und Memberaufruf. Jeder dieser Buckets wird immer teurer. Typvergleichsvorgänge ( in diesem Fall typeof in C#, GetType, IsInstanceOfType usw.) sind die billigsten reflektions-APIs, obwohl sie keineswegs billig sind. Memberenumerzählungen ermöglichen es Ihnen, die Methoden, Eigenschaften, Felder, Ereignisse, Konstruktoren usw. einer Klasse programmgesteuert zu untersuchen. Ein Beispiel dafür, wo diese verwendet werden können, ist in Entwurfszeitszenarien, in diesem Fall das Aufzählen von Eigenschaften von Zollwebsteuerelementen für den Eigenschaftenbrowser in Visual Studio. Die teuersten Reflektions-APIs sind diejenigen, mit denen Sie die Member einer Klasse dynamisch aufrufen oder dynamisch JIT ausgeben und eine Methode ausführen können. Es gibt sicherlich spät begrenzte Szenarien, in denen dynamisches Laden von Assemblys, Instanziierungen von Typen und Methodenaufrufe erforderlich sind, aber diese lose Kopplung erfordert einen expliziten Leistungskonflikt. Im Allgemeinen sollten die Reflektions-APIs in leistungsabhängigen Codepfaden vermieden werden. Beachten Sie, dass Sie die Reflektion zwar nicht direkt verwenden, aber von einer API, die Sie verwenden, diese möglicherweise verwenden. Beachten Sie daher auch die transitive Verwendung der Reflektions-APIs.

Späte Bindung

Spät gebundene Aufrufe sind ein Beispiel für ein Feature, das Reflektion im Cover verwendet. Visuelle Basic.NET und JScript.NET unterstützen spät gebundene Aufrufe. Für instance müssen Sie eine Variable vor ihrer Verwendung nicht deklarieren. Spät gebundene Objekte sind tatsächlich vom Typ objekt, und Reflektion wird verwendet, um das Objekt zur Laufzeit in den richtigen Typ zu konvertieren. Ein spät gebundener Aufruf ist um Größenordnungen langsamer als ein direkter Aufruf. Es sei denn, Sie benötigen ein spät gebundenes Verhalten, sollten Sie dessen Verwendung in leistungskritischen Codepfaden vermeiden.

HINWEIS Wenn Sie VB.NET verwenden und nicht explizit eine späte Bindung benötigen, können Sie den Compiler anweisen, dies nicht zuzulassen, indem Sie und Option Explicit OnOption Strict On oben in Die Quelldateien einfügen. Diese Optionen zwingen Sie, Ihre Variablen zu deklarieren und stark einzugeben, und deaktivieren die implizite Umwandlung.

Sicherheit

Sicherheit ist ein notwendiger und integraler Bestandteil der CLR und verursacht damit leistungsrelevante Kosten. Falls der Code vollständig vertrauenswürdig ist und die Sicherheitsrichtlinie die Standardeinstellung ist, sollte die Sicherheit geringfügige Auswirkungen auf den Durchsatz und die Startzeit Ihrer Anwendung haben. Teilweise vertrauenswürdiger Code – z. B. Code aus dem Internet oder der Intranetzone – oder das Einschränken des MyComputer Grant Set erhöhen die Sicherheitsleistung.

COM-Interop und Plattformaufruf

COM-Interop und Plattformaufruf machen native APIs für verwalteten Code nahezu transparent verfügbar. Das Aufrufen der meisten nativen APIs erfordert in der Regel keinen speziellen Code, kann jedoch einige Mausklicks erfordern. Wie Sie vielleicht erwarten, fallen beim Aufrufen von nativem Code aus verwaltetem Code Kosten an und umgekehrt. Es gibt zwei Komponenten für diese Kosten: feste Kosten, die mit der Durchführung der Übergänge zwischen nativem und verwaltetem Code verbunden sind, und variable Kosten, die mit einem Marshalling von Argumenten und Rückgabewerten verbunden sind, die möglicherweise erforderlich sind. Der feste Beitrag zu den Kosten für COM-Interop und P/Invoke ist gering: in der Regel weniger als 50 Anweisungen. Die Kosten für das Marshallen zu und von verwalteten Typen hängen davon ab, wie unterschiedlich die Darstellungen auf beiden Seiten der Grenze sind. Typen, die einen erheblichen Transformationsaufwand erfordern, sind teurer. Beispielsweise sind alle Zeichenfolgen in der CLR Unicode-Zeichenfolgen. Wenn Sie eine Win32-API über P/Invoke aufrufen, die ein ANSI-Zeichenarray erwartet, muss jedes Zeichen in der Zeichenfolge eingeschränkt werden. Wenn jedoch ein verwaltetes Ganzzahlarray übergeben wird, bei dem ein natives Ganzzahlarray erwartet wird, ist kein Marshalling erforderlich.

Da beim Aufrufen von nativem Code Leistungskosten anfallen, sollten Sie sicherstellen, dass die Kosten gerechtfertigt sind. Wenn Sie einen nativen Aufruf tätigen möchten, stellen Sie sicher, dass die Arbeit, die der native Aufruf ausführt, die Leistungskosten im Zusammenhang mit dem Ausführen des Anrufs rechtfertigt– halten Sie methoden "chunky" statt "chatty". Eine gute Möglichkeit, die Kosten eines nativen Aufrufs zu messen, besteht darin, die Leistung einer nativen Methode zu messen, die keine Argumente akzeptiert und keinen Rückgabewert aufweist, und dann die Leistung der nativen Methode zu messen, die Sie aufrufen möchten. Die Differenz gibt Ihnen einen Hinweis auf die Marshallingkosten.

HINWEIS Führen Sie "Chunky"-COM-Interop- und P/Invoke-Aufrufe im Gegensatz zu "Chatty"-Anrufen aus, und stellen Sie sicher, dass die Kosten für den Anruf durch den Arbeitsaufwand, den der Anruf ausführt, gerechtfertigt sind.

Beachten Sie, dass verwalteten Threads keine Threadingmodelle zugeordnet sind. Wenn Sie einen COM-Interop-Aufruf durchführen möchten, müssen Sie sicherstellen, dass der Thread, für den der Aufruf ausgeführt wird, mit dem richtigen COM-Threadingmodell initialisiert wird. Dies erfolgt in der Regel mithilfe von MTAThreadAttribute und STAThreadAttribute (obwohl dies auch programmgesteuert erfolgen kann).

Leistungsindikatoren

Eine Reihe von Windows-Leistungsindikatoren werden für die .NET CLR verfügbar gemacht. Diese Leistungsindikatoren sollten bei der ersten Diagnose eines Leistungsproblems oder beim Versuch, die Leistungsmerkmale einer verwalteten Anwendung zu identifizieren, die Waffe der Wahl eines Entwicklers sein. Ich habe bereits einige der Indikatoren erwähnt, die sich auf die Speicherverwaltung und Ausnahmen beziehen. Es gibt Leistungsindikatoren für fast jeden Aspekt der CLR und .NET Framework. Diese Leistungsindikatoren sind immer verfügbar und nicht invasiv; Sie haben einen geringen Mehraufwand und ändern nicht die Leistungsmerkmale Ihrer Anwendung.

Sonstige Tools

Abgesehen von den Leistungsindikatoren und dem CLR Profiler sollten Sie einen herkömmlichen Profiler verwenden, um festzustellen, welche Methoden in Ihrer Anwendung am meisten Zeit in Anspruch nehmen und am häufigsten aufgerufen werden. Dies sind die Methoden, die Sie zuerst optimieren. Es stehen eine Reihe von kommerziellen Profilern zur Verfügung, die verwalteten Code unterstützen, einschließlich DevPartner Studio Professional Edition 7.0 von Compuware und VTune™ Leistungsanalyse 7.0 von Intel®. Compuware erstellt auch einen kostenlosen Profiler für verwalteten Code namens DevPartner Profiler Community Edition.

Zusammenfassung

Dieser Artikel beginnt mit der Untersuchung der CLR und der .NET Framework aus Leistungssicht. Es gibt viele andere Aspekte der Architektur der CLR und der .NET Framework, die sich auf die Leistung Ihrer Anwendung auswirken. Die beste Anleitung, die ich jedem Entwickler geben kann, besteht darin, keine Annahmen über die Leistung der Plattform zu treffen, auf die Ihre Anwendung abzielt, und der von Ihnen verwendeten APIs. Messen Sie alles!

Glückliches Jonglieren.

Ressourcen