(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Schreiben von leistungsstarken verwalteten Anwendungen - Ein Leitfaden

Veröffentlicht: 11. Aug 2003 | Aktualisiert: 28. Jun 2004
Von Gregor Noriskin

Download
Auf dieser Seite

Jonglieren - eine Metapher für die Softwareentwicklung Jonglieren - eine Metapher für die Softwareentwicklung
Die Common Language Runtime von .NET Die Common Language Runtime von .NET
Verwaltete Daten und der Garbage Collector Verwaltete Daten und der Garbage Collector
Zuweisungsprofile Zuweisungsprofile
Die Profilerstellungs-API und CLR Profiler Die Profilerstellungs-API und CLR Profiler
Hosten des Server-GC Hosten des Server-GC
Beendigung Beendigung
Das "Dispose"-Muster Das "Dispose"-Muster
Anmerkung zu schwachen Verweisen Anmerkung zu schwachen Verweisen
Verwalteter Code und CLR JIT Verwalteter Code und CLR JIT
Werttypen Werttypen
Ausnahmebehandlung Ausnahmebehandlung
Threading und Synchronisierung Threading und Synchronisierung
Reflektion Reflektion
Spätes Binden Spätes Binden
Sicherheit Sicherheit
COM-Interoperabilität und Plattformaufrufe COM-Interoperabilität und Plattformaufrufe
Leistungsindikatoren Leistungsindikatoren
Weitere Tools Weitere Tools
Schlussfolgerung Schlussfolgerung
Ressourcen Ressourcen

Jonglieren - eine Metapher für die Softwareentwicklung

Jonglieren ist eine passende Metapher, um den Prozess der Softwareentwicklung zu beschreiben. Für das Jonglieren werden normalerweise mindestens drei Gegenstände benötigt, wobei der Anzahl der Objekte natürlich keine Grenzen gesetzt sind. Wenn Sie das erste Mal jonglieren, werden Sie feststellen, dass Sie jeden einzelnen Ball mit den Augen verfolgen, wenn Sie ihn auffangen und werfen. Als Fortgeschrittener beginnen Sie, sich auf den Fluss der Bälle anstatt auf jeden einzelnen Ball zu konzentrieren. Als Jonglierprofi können Sie sich dann wieder auf einen einzelnen Ball konzentrieren und diesen zum Beispiel auf Ihrer Nase balancieren, während Sie mit den anderen Bällen weiterjonglieren. Sie wissen dann intuitiv, wo sich die Bälle befinden, und bewegen Ihre Hände in die richtige Position, um sie aufzufangen und wieder zu werfen. Wie lässt sich dies nun mit dem Entwickeln von Software vergleichen?

Bei der Softwareentwicklung erfordern unterschiedliche Rollen das Jonglieren von jeweils drei verschiedenen Aufgaben: Programm- und Projekt-Manager jonglieren Funktionen, Ressourcen und Zeit, Softwareentwickler jonglieren Fehlerfreiheit, Leistung und Sicherheit. Man kann natürlich immer versuchen, noch mehr Objekte zu jonglieren, aber Jonglierschüler werden Ihnen bestätigen, dass jeder weitere Ball die Schwierigkeit, alle Bälle in der Luft zu halten, exponentiell erhöht. Technisch gesehen sind Sie kein wirklicher Jongleur, wenn Sie mit weniger als drei Bällen arbeiten. Wenn Sie als Softwareentwickler nicht die Fehlerfreiheit, Leistung und Sicherheit des von Ihnen geschriebenen Codes beachten, könnte man auch sagen, dass Sie Ihren Job nicht richtig machen. Wenn Sie die Fehlerfreiheit, Leistung und Sicherheit im Auge behalten, werden Sie feststellen, dass Sie sich anfangs jeweils auf einen der drei Aspekte konzentrieren müssen. Durch tägliche Übung ist dies schließlich nicht mehr notwendig, da alle drei zu einem integralen Bestandteil Ihrer Arbeitsweise geworden sind. Wenn Sie die drei Aspekte vollständig beherrschen, können Sie intuitiv Kompromisse eingehen und Ihre Bemühungen entsprechend konzentrieren. Wie beim Jonglieren macht auch hier die Übung den Meister.

Das Schreiben von leistungsstarkem Code erfordert das Festlegen von Zielen, Messungen und ein Verständnis der Zielplattform. Wenn Sie nicht wissen, wie schnell Ihr Code arbeiten muss, woher wissen Sie dann, wann Sie fertig sind? Wenn Sie Ihren Code nicht messen und Profile davon erstellen, wie wissen Sie dann, wann Sie Ihre Ziele erreicht haben oder warum Sie dies nicht schaffen? Wenn Sie die Zielplattform nicht verstehen, wie wissen Sie dann, was Sie optimieren müssen, falls Sie Ihre Ziele nicht erreichen? Diese Prinzipien gelten, ungeachtet der angestrebten Zielplattform, für jede Entwicklung von leistungsstarkem Code. Ein Artikel über das Schreiben von leistungsstarkem Code, der diese drei Aspekte nicht berücksichtigt, ist unvollständig. Obwohl die drei Aspekte alle gleich wichtig sind, konzentrieren wir uns in diesem Artikel auf die letzten beiden, da sie für das Schreiben von leistungsstarken Anwendungen von Bedeutung sind, deren Zielplattform Microsoft® .NET Framework ist.

Für das Schreiben von leistungsstarkem Code für jede beliebige Plattform gelten folgende grundlegende Prinzipien:

  • Festlegen eines Leistungsziels

  • Messen, messen und dann noch einmal messen

  • Verständnis der Hardware- und Softwareplattformen, für die Ihre Anwendung entwickelt wird

Die Common Language Runtime von .NET

Das Herzstück von .NET Framework ist die Common Language Runtime (CLR). Die CLR bietet alle Laufzeitdienste für Ihren Code: JIT-Kompilierung (Just-In-Time), Speicherverwaltung, Sicherheit und eine Vielzahl weiterer Dienste. Die CLR wurde für hohe Leistung entwickelt. Es gibt Wege, diese Leistungskraft zu nutzen oder zu mindern.

Dieser Artikel soll Ihnen einen Überblick über die Leistung der Common Language Runtime geben, die für die Leistung von verwaltetem Code optimale Vorgehensweise identifizieren und Ihnen zeigen, wie Sie die Leistung Ihrer verwalteten Anwendung messen. Er enthält jedoch keine umfassenden Erläuterungen zu den Leistungsmerkmalen von .NET Framework. Für diesen Artikel beinhaltet die Definition von Leistung neben dem Durchsatz, der Skalierbarkeit und der Startzeit auch die Speichernutzung.

Verwaltete Daten und der Garbage Collector

Eine der Hauptsorgen von Entwicklern beim Einsatz von verwaltetem Code in leistungskritischen Anwendungen sind die Kosten für die CLR-Speicherverwaltung, die vom Garbage Collector (GC) durchgeführt wird. Die Speicherverwaltungskosten setzen sich aus den Zuweisungskosten für den Speicher, der mit einer Typinstanz verknüpft ist, den Kosten für das Verwalten dieses Speichers während der Lebensdauer der Instanz und den Kosten für das Freigeben dieses Speichers zusammen, wenn er nicht mehr benötigt wird.

Eine verwaltete Zuweisung ist normalerweise recht kostengünstig und benötigt in den meisten Fällen weniger Zeit als malloc oder new in C/C++. Grund hierfür ist, dass die CLR keine Liste durchsuchen muss, um den nächsten verfügbaren und zusammenhängenden Speicherblock zu finden, dessen Größe für das neue Objekt ausreicht. Sie verfügen über einen Zeiger auf den nächsten freien Speicherplatz. Man kann sich verwaltete Heapzuweisungen als "stackähnlich" vorstellen. Eine Zuweisung kann zu einer Auflistung führen, wenn der GC Speicher freigeben muss, um das neue Objekt zuweisen zu können. In diesem Fall ist die Zuweisung teurer als malloc oder new. Verankerte Objekte können die Zuweisungskosten ebenfalls beeinflussen. Verankerte Objekte sind Objekte, die der GC während einer Auflistung auf Anweisung hin nicht verschieben darf. Grund hierfür ist normalerweise, dass die Adresse des Objekts an eine systemeigene API übergeben wurde.

Im Gegensatz zu malloc oder new zieht das Verwalten von Speicher während der Lebensdauer eines Objekts Kosten nach sich. Der CLR-GC ist "generationsbezogen", d.h., es wird nicht immer der gesamte Heap aufgelistet. Der GC muss aber dennoch wissen, ob sich im verbleibenden Heap aktive Objekte befinden, die Stämme für Objekte in dem Teil des Heaps unterhalten, der aufgelistet wird. Eine Verwaltung von Speicher mit Objekten, die über Verweise auf Objekte in jüngeren Generationen verfügen, führt im Verlauf der Lebensdauer dieser Objekte zu hohen Kosten.

Der GC ist ein "Mark-And-Sweep"-Generationen-Garbage Collector. Der verwaltete Heap enthält drei Generationen. Generation 0 enthält alle neuen Objekte, Generation 1 etwas langlebigere Objekte und Generation 2 alle sehr langlebigen Objekte. Der GC listet den kleinstmöglichen Abschnitt des Heaps auf, der genügend Speicher freigibt, damit die Anwendung fortfahren kann. Bei der Auflistung einer Generation werden auch alle jüngeren Generationen mit aufgeführt. Das heißt, eine Auflistung von Generation 1 enthält auch Generation 0. Die Größe von Generation 0 wird dynamisch an die Größe des Prozessorcaches und die Zuweisungsrate der Anwendung angepasst. Die Auflistung dauert im Regelfall weniger als 10 Millisekunden. Die Größe von Generation 1 wird gemäß der Zuweisungsrate der Anwendung dynamisch angepasst. Ihre Auflistung dauert normalerweise zwischen 10 und 30 Millisekunden. Die Größe von Generation 2 sowie die für ihre Auflistung benötigte Zeit hängt vom Zuweisungsprofil Ihrer Anwendung ab. Die Leistungskosten für die Verwaltung Ihres Anwendungsspeichers werden hauptsächlich von dieser Generation 2-Auflistung bestimmt.

HINWEIS Der GC führt eine Selbstabstimmung durch und passt sich den Speicheranforderungen der Anwendung an. Ein programmtechnisches Aufrufen eines GC verhindert meist diese Selbstanpassung. Wenn Sie dem GC durch Aufrufen von GC.Collect "helfen" wollen, kann dies die Leistung Ihrer Anwendung vermindern.

Der GC kann aktive Objekte während einer Auflistung verschieben. Wenn diese Objekte groß sind, ist ihre Verschiebung sehr kostenintensiv. Derartige Objekte werden daher einem speziellen Bereich des Heaps, dem Large Object Heap, zugewiesen. Der Large Object Heap wird zwar aufgelistet, aber nicht komprimiert, und große Objekte werden beispielsweise nicht verschoben. Als große Objekte gelten Objekte mit mehr als 80 KB. Beachten Sie, dass sich dies in zukünftigen Versionen der CLR ändern kann. Wenn der Large Object Heap aufgelistet werden muss, wird eine vollständige Auflistung erzwungen. Die Auflistung des Large Object Heap erfolgt dabei während Auflistungen von Generation 2. Die Zuweisungs- und Deaktivierungsrate von Objekten im Large Object Heap kann die Leistungskosten für die Verwaltung Ihres Anwendungsspeichers beträchtlich beeinflussen.

Zuweisungsprofile

Das allgemeine Zuweisungsprofil einer verwalteten Anwendung bestimmt, wie hart der Garbage Collector arbeiten muss, um den mit der Anwendung verbundenen Speicher zu verwalten. Je mehr der GC an der Speicherverwaltung arbeitet, desto höher ist die von ihm benötigte Anzahl an CPU-Zyklen, und umso geringer die Zeit, die die CPU auf die Ausführung des Anwendungscodes verwendet. Das Zuweisungsprofil setzt sich aus der Anzahl der zugewiesenen Objekte, der Größe dieser Objekte und ihrer Lebensdauer zusammen. Sie reduzieren die GC-Auslastung am einfachsten, indem Sie weniger Objekte zuweisen. Anwendungen, die für Erweiterbarkeit, Modularität und Wiederverwendung entwickelt werden und objektorientierte Entwurfsverfahren verwenden, resultieren fast immer in einer vermehrten Anzahl von Zuweisungen. Abstraktion und "Eleganz" führen zu Leistungseinbußen.

Bei einem GC-freundlichen Zuweisungsprofil werden zu Beginn der Anwendung einige Objekte zugewiesen, die dann für die Lebensdauer der Anwendung bestehen bleiben. Alle andere Objekte sind kurzlebig. Langlebige Objekte enthalten nur wenige oder gar keine Verweise auf kurzlebige Objekte. Je mehr das Zuweisungsprofil von diesem Vorbild abweicht, desto härter muss der GC arbeiten, um den Anwendungsspeicher zu verwalten.

Bei einem GC-unfreundlichen Zuweisungsprofil überdauern viele Objekte bis zur Generation 2 und werden dann deaktiviert, oder es werden dem Large Object Heap viele kurzlebige Objekte zugewiesen. Die Verwaltung von Objekten, die lange genug aktiv sind, um in die Generation 2 zu gelangen, dann aber deaktiviert werden, ist am kostenintensivsten. Wie bereits erwähnt, werden die Kosten für die Auflistung während einer Garbage Collection auch durch Objekte in älteren Generationen erhöht, die Verweise auf Objekte in jüngeren Generationen enthalten.

Ein typisches, reales Zuweisungsprofil liegt zwischen den beiden oben erwähnten Zuweisungsprofilen. Eine wichtige Metrik für Ihr Zuweisungsprofil ist der Prozentsatz der gesamten CPU-Zeit, der für die Garbage Collection aufgewendet wird. Sie erhalten diese Zahl vom Leistungsindikator .NET CLR Memory: % Time in GC. Wenn der Mittelwert dieses Indikators über 30 % liegt, sollten Sie Ihr Zuweisungsprofil genauer unter die Lupe nehmen. Das heißt nicht zwangsläufig, dass Ihr Zuweisungsprofil nicht OK ist. Es gibt einige speicherintensive Anwendungen, bei denen ein derartiger GC-Prozentsatz notwendig und auch angebracht ist. Wenn Sie auf Leistungsprobleme stoßen, sollten Sie sich diesen Indikator als Erstes ansehen. Er kann ihnen sofort zeigen, ob Ihr Zuweisungsprofil Teil des Problems ist.

HINWEIS Wenn der Leistungsindikator .NET CLR Memory: % Time in GC anzeigt, dass Ihre Anwendung durchschnittlich über 30 % der Zeit für Garbage Collection aufwendet, sollten Sie sich Ihr Zuweisungsprofil genauer ansehen.

HINWEIS Eine GC-freundliche Anwendung enthält bedeutend mehr Generation 0- als Generation 2-Auflistungen. Sie können dieses Verhältnis durch Vergleichen der Leistungsindikatoren NET CLR Memory: # Gen 0 Collections und NET CLR Memory: # Gen 2 Collections ermitteln.

Die 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. CLR Profiler ist ein nicht unterstütztes Tool zur Erstellung von Zuweisungsprofilen, das vom CLR-Produktteam geschrieben wurde und diese Profilerstellungs-API verwendet. Mit CLR Profiler können Entwickler die Zuweisungsprofile ihrer verwalteten Anwendungen anzeigen.

highperfmanagedapps_01.gif

Abbildung 1 CLR Profiler- Hauptfenster

CLR Profiler enthält mehrere, sehr nützliche Ansichten des Zuweisungsprofils, einschließlich eines Histogramms der zugewiesenen Typen, Zuweisungs- und Aufrufkurven, eine Zeitleiste, die die GCs verschiedener Generationen und den aus diesen Auflistungen resultierenden Status des verwalteten Heaps anzeigt, sowie eine Aufrufverzeichnisstruktur, die die Zuweisungen pro Methode und Assemblyladungen aufführt.

highperfmanagedapps_02.gif

Abbildung 2 CLR Profiler - Zuweisungskurven

HINWEIS Genaue Angaben zur Verwendung von CLR Profiler finden Sie in der entsprechenden Infodatei, die ebenfalls in der ZIP-Datei enthalten ist.

Beachten Sie, dass CLR Profiler einen hohen Leistungsoverhead besitzt und die Leistungsmerkmale Ihrer Anwendung deutlich ändert. Auftretende Belastungsfehler verschwinden wahrscheinlich, wenn Sie Ihre Anwendung mit CLR Profiler ausführen.

Hosten des Server-GC

Es gibt zwei verschiedene Garbage Collectors für die CLR: einen Workstation-GC und einen Server-GC. Der Workstation-GC wird von Konsolenanwendungen und Windows Forms-Anwendungen gehostet, der Server-GC von ASP.NET. Der Server-GC ist für Durchsatz und Multiprozessorskalierbarkeit optimiert. Er hält für die gesamte Dauer einer Auflistung alle Threads (einschließlich der Mark- und Sweep-Phasen) an, die verwalteten Code ausführen. Die Garbage Collection wird bei allen dem Prozess zur Verfügung stehenden CPUs auf dedizierten, CPU-affinierten Threads mit hoher Priorität parallel ausgeführt. Wenn Threads während einer Garbage Collection systemeigenen Code ausführen, werden sie nur dann angehalten, wenn der systemeigene Aufruf zurückgegeben wird. Wenn Sie eine Serveranwendung für Multiprozessorgeräte entwickeln, sollten Sie auf jeden Fall den Server-GC verwenden. Wenn Ihre Anwendung nicht von ASP.NET gehostet wird, müssen Sie eine systemeigene Anwendung schreiben, die die CLR explizit hostet.

HINWEIS Hosten Sie den Server-GC, wenn Sie skalierbare Serveranwendungen entwickeln. Siehe Implement a Custom Common Language Runtime Host for Your Managed App (in Englisch).

Der Workstation-GC ist für niedrige Latenz optimiert, die normalerweise für Clientanwendungen benötigt wird. Während einer Garbage Collection soll in einer Clientanwendung natürlich keine merkliche Pause auftreten, denn die Clientleistung wird normalerweise nicht am reinen Durchsatz sondern an der wahrgenommenen Leistung gemessen. Der Workstation-GC führt gleichzeitige Garbage Collections durch. Das heißt, er führt die Mark-Phase durch, während der verwaltete Code noch läuft. Der GC hält Threads, die verwalteten Code ausführen, nur dann an, wenn er die Sweep-Phase durchführen muss. Beim Workstation-GC wird die Garbage Collection nur auf einem Thread und daher auch nur auf einer CPU ausgeführt.

Beendigung

Die CLR enthält einen Mechanismus, der automatisch eine Bereinigung durchführt, bevor der mit der Instanz eines Typs verknüpfte Speicher freigegeben wird. Dieser Mechanismus wird "Beendigung" (Finalization) genannt. Durch die Beendigung werden für gewöhnlich systemeigene Ressourcen freigegeben, in diesem Fall Datenbankverbindungen oder Betriebssystemhandles, die von einem Objekt verwendet werden.

Der Beendigungsmechanismus ist eine kostspielige Funktion, die die GC-Auslastung noch erhöht. Der GC verfolgt die Objekte, die beendet werden müssen, in einer Finalizable-Warteschlange. Wenn der GC während einer Auflistung auf ein Objekt stößt, das nicht mehr aktiv ist und beendet werden muss, dann wird der Eintrag für dieses Objekt von der Finalizable- in die FRachable-Warteschlange verschoben. Die Beendigung wird auf einem separaten Thread, dem Finalizerthread, durchgeführt. Da während der Ausführung des Finalizers u.U. der vollständige Status des Objekts benötigt wird, wird das Objekt selbst sowie alle Objekte, auf die es zeigt, zur nächsten Generation heraufgestuft. Der mit dem Objekt oder der Objektkurve verknüpfte Speicher wird erst während der nachfolgenden Garbage Collection freigegeben.

Ressourcen, die freigegeben werden müssen, sollten in ein möglichst kleines Finalizable-Objekt gewrappt werden. Wenn Ihre Klasse beispielsweise Verweise auf verwaltete und nicht verwaltete Ressourcen benötigt, sollten Sie die nicht verwalteten Ressourcen in eine neue Finalizable-Klasse wrappen und diese Klasse zu einem Member Ihrer Klasse machen. Bei der übergeordneten Klasse sollte es sich nicht um eine Finalizable-Klasse handeln. Das bedeutet, dass nur die Klasse heraufgestuft wird, die die nicht verwalteten Ressourcen enthält (es wird davon ausgegangen, dass die Klasse mit den nicht verwalteten Ressourcen keinen Verweis auf die übergeordnete Klasse besitzt). Sie sollten darüber hinaus beachten, dass es nur einen einzigen Beendigungsthread gibt. Wenn ein Finalizer eine Blockierung dieses Threads verursacht, werden die nachfolgenden Finalizer nicht aufgerufen, die Ressourcen werden nicht freigegeben und es kommt zu Anwendungslücken.

HINWEIS Finalizer sollten möglichst einfach gestaltet sein und niemals Blockierungen verursachen.
HINWEIS Nur die Wrapper-Klasse um nicht verwaltete Objekte, die eine Bereinigung benötigen, sollte eine Finalizable-Klasse sein.

Betrachten Sie die Beendigung als eine Alternative zur Verweiszählung. Ein Objekt, das Verweiszählung durchführt, verfolgt, wie viele andere Objekte Verweise auf es selbst besitzen (was zu einigen recht bekannten Problemen führen kann), damit es seine Ressourcen freigeben kann, sobald die Verweisanzahl auf Null gefallen ist. Die CLR implementiert keine Verweiszählung und benötigt daher einen Mechanismus, der Ressourcen dann automatisch freigibt, wenn keine Verweise mehr auf das Objekt vorliegen. Dieser Mechanismus ist die Beendigung. Eine Beendigung ist normalerweise nur dann erforderlich, wenn die Lebensdauer eines zu bereinigenden Objekts nicht genau bekannt ist.

Das "Dispose"-Muster

Wenn die Lebensdauer eines Objekts genau bekannt ist, sollten die nicht verwalteten Ressourcen, die mit dem Objekt verknüpft sind, umgehend freigegeben werden. Man spricht in diesem Fall vom "Entfernen" (Disposing) des Objekts. Das Dispose-Muster wird über die IDisposable-Schnittstelle implementiert (obwohl Sie dies auch leicht selbst durchführen können). Wenn Sie die sofortige Entfernung für Ihre Klasse einrichten möchten, sodass Instanzen Ihrer Klasse entfernbar sind, muss Ihr Objekt die IDisposable-Schnittstelle implementieren und eine Implementierung für die Dispose-Methode zur Verfügung gestellt werden. Sie rufen in der Dispose-Methode denselben Bereinigungscode auf, den auch der Finalizer enthält, und melden dem GC durch Aufrufen der GC.SuppressFinalization-Methode, dass er das Objekt nicht mehr beenden muss. Es ist empfehlenswert, sowohl von der Dispose-Methode als auch vom Finalizer eine allgemeine Beendigungsfunktion aufrufen zu lassen, damit nur eine Version des Bereinigungscodes verwaltet werden muss. Wenn die Semantik des Objekts eine Close-Methode logischer als eine Dispose-Methode werden lässt, sollten Sie auch eine Close-Methode implementieren (in diesem Fall wird eine Datenbankverbindung oder ein Socket auf logische Weise "geschlossen"). Die Close-Methode kann einfach die Dispose-Methode aufrufen.

Für Klassen mit einem Finalizer sollte immer eine Dispose-Methode zur Verfügung gestellt werden, da man nie sicher sein kann, wie diese Klassen verwendet werden und ob beispielsweise ihre Lebensdauer explizit bekannt sein wird oder nicht. Wenn eine von Ihnen verwendete Klasse das Dispose-Muster implementiert und Sie genau wissen, wann Sie das Objekt nicht mehr benötigen, sollten Sie auf jeden Fall die Dispose-Methode aufrufen.

HINWEIS Stellen Sie für alle Finalizable-Klassen eine Dispose-Methode zur Verfügung.
HINWEIS Unterdrücken Sie die Beendigung 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 die Dispose-Methode auf.

C# bietet eine sehr bequeme Möglichkeit, Objekte automatisch zu entfernen. Mithilfe des using-Schlüsselwortes können Sie einen Codeblock identifizieren, nach dem die Dispose-Methode für eine Anzahl zu entfernender Objekte aufgerufen wird.

Das "using"-Schlüsselwort in C#

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

Anmerkung zu schwachen Verweisen

Jeder Verweis auf ein Objekt, das sich auf dem Stack, in einem Register, in einem anderen Objekt oder in einem der anderen GC-Stämme befindet, hält das Objekt während einer Garbage Collection aktiv. Dies ist normalerweise wünschenswert, da Ihre Anwendung dieses Objekt gewöhnlich noch benötigt. Es gibt jedoch Fälle, in denen Sie zwar einen Verweis auf ein Objekt wünschen, aber die Lebensdauer dieses Objekts nicht beeinflussen möchten. Dafür bietet CLR einen als "schwache Verweise" (Weak References) bezeichneten Mechanismus. Jeder starke Verweis (beispielsweise ein Verweis, der den Stamm für ein Objekt unterhält) kann zu einem schwachen Verweis geändert werden. Ein Beispiel für die Verwendung von schwachen Verweisen wäre der Fall, dass Sie ein externes Cursorobjekt erstellen möchten, das eine Datenstruktur durchlaufen kann, aber die Lebensdauer des Objekts nicht beeinflussen soll. Schwache Verweise sind auch angebracht, wenn Sie einen Cache erstellen möchten, der bei Speicherauslastung (beispielsweise während einer Garbage Collection) gelöscht wird.

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 CLR JIT

Verwaltete Assemblys, die die Verteilungseinheit für verwalteten Code darstellen, besitzen eine prozessorunabhängige Sprache namens Microsoft Intermediate Language (MSIL oder IL). Der CLR JIT (Just-In-Time) kompiliert die IL in optimierte, systemeigene X86-Anweisungen. Der JIT ist ein optimierender Compiler. Da die Kompilierung jedoch zur Laufzeit und nur beim ersten Aufruf einer Methode durchgeführt wird, muss das Verhältnis zwischen der vom Compiler ausgeführten Anzahl an Optimierungen und der für die Kompilierung aufgewendeten Zeit stimmen. Dies ist bei Serveranwendungen normalerweise nicht sehr wichtig, da die Startzeit und das Reaktionsvermögen für gewöhnlich nicht von Bedeutung sind. Für Clientanwendungen ist dies jedoch kritisch. Beachten Sie, dass Sie die Startzeit verkürzen können, indem Sie die Kompilierung zum Zeitpunkt der Installation mithilfe von NGEN.exe durchführen.

Viele der von JIT vorgenommenen Optimierungen verfügen nicht über verknüpfte programmtechnische Muster (so können Sie beispielsweise keine explizite Codierung dafür durchführen). Es gibt eine gewisse Anzahl an Optimierungen, bei denen dies möglich ist. Im nächsten Abschnitt werden einige davon genauer erläutert.

HINWEIS Verkürzen Sie die Startzeit für Clientanwendungen, indem Sie Ihre Anwendung zum Zeitpunkt der Installation mithilfe des NGEN.exe-Dienstprogramms kompilieren.

Methodeninlining
Methodenaufrufe bergen gewisse Kosten in sich: Argumente müssen dem Stack hinzugefügt oder in Registern gespeichert werden, der Methodenprolog und -epilog muss ausgeführt werden usw. Die Kosten für diese Aufrufe können bei bestimmten Methoden vermieden werden, indem der Textkörper der aufgerufenen Methode in den Textkörper des Aufrufers verschoben wird. Dies wird "Methodeninlining" genannt. Der JIT wendet mehrere Heuristiken an, um zu entscheiden, ob bei einer Methode ein Inlining durchgeführt werden soll. Es folgt eine Liste der wichtigsten Heuristiken (diese ist jedoch nicht vollständig):

  • Bei Methoden, die mehr als 32 Byte von IL beanspruchen, wird kein Inlining durchgeführt.

  • Bei virtuellen Funktionen wird kein Inlining durchgeführt.

  • Bei Methoden mit komplexer Flusssteuerung wird kein Inlining durchgeführt. Unter einer komplexen Flusssteuerung versteht man jede Steuerung außer if/then/else. In diesem Fall switch oder while.

  • Bei Methoden, die Ausnahmebehandlungsblöcke enthalten, wird kein Inlining durchgeführt. Methoden, die Ausnahmen ausgeben, qualifizieren sich dennoch für Inlining.

  • Wenn es sich bei einem der formalen Argumente der Methode um eine Struktur handelt, wird bei der Methode kein Inlining durchgeführt.

Sie sollten sich ein eindeutiges Codieren für diese Heuristiken gut überlegen, da sie sich in zukünftigen Versionen des JIT ändern können. Gehen Sie bei der Fehlerfreiheit der Methode keine Kompromisse ein, nur um ein Inlining der Methode sicherzustellen. Interessanterweise garantieren die inline- und __inline-Schlüsselwörter in C++ (im Gegensatz zu __forceinline) nicht, dass der Compiler bei einer Methode wirklich ein Inlining durchführt.

Methoden zum Abrufen und Festlegen von Eigenschaften eignen sich normalerweise gut für das Inlining, da sie lediglich private Datenmember initialisieren.

HINWEIS Gehen Sie bei der Fehlerfreiheit einer Methode keine Kompromisse ein, um ein Inlining sicherzustellen.

Eliminieren der Bereichsüberprüfung
Einer der vielen Vorteile von verwaltetem Code ist die automatische Bereichsüberprüfung (Range Check). Jedes Mal, wenn Sie mithilfe von array[index]-Semantik auf ein Array zugreifen, gibt der JIT eine Überprüfung aus, um sicherzustellen, dass der Index innerhalb der Arraygrenzen liegt. Diese Überprüfungen können sehr kostenintensiv sein, wenn sie bei Schleifen mit einer großen Anzahl an Iterationen und nur wenigen auszuführenden Anweisungen pro Iteration durchgeführt werden. In manchen Fällen erkennt der JIT, dass diese Bereichsüberprüfungen unnötig sind, eliminiert sie aus dem Inhalt der Schleife und überprüft die Schleife nur ein Mal vor Beginn der Ausführung. C# enthält ein programmtechnisches Muster, das eine Eliminierung dieser Bereichsüberprüfungen sicherstellt: Führen Sie einen expliziten Test hinsichtlich der Länge des Arrays in der for-Anweisung durch. Beachten Sie hierbei, dass geringe Abweichungen von diesem Muster bereits dazu führen, dass die Überprüfung nicht eliminiert und dem Index in diesem Fall ein Wert hinzugefügt wird.

Eliminieren der Bereichsüberprü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 beim Durchsuchen von großen, verzweigten Arrays auffällig, da die Bereichsüberprüfung der inneren und äußeren Schleife eliminiert wird.

Optimierungen, die eine Verfolgung der Variablenverwendung erfordern
Bei mehreren Optimierungen des JIT-Compilers ist es erforderlich, dass der JIT die Verwendung formaler Argumente und lokaler Variablen verfolgt (beispielsweise hinsichtlich ihrer ersten und letzten Verwendung im Textkörper der Methode). In den Versionen 1.0 und 1.1 der CLR ist die Gesamtzahl der Variablen, deren Verwendung der JIT verfolgen kann, auf 64 begrenzt. Ein Beispiel für eine Optimierung, die eine Verfolgung der Verwendung erfordert, ist die Registrierung. Bei der Registrierung werden Variablen in Prozessorregistern und nicht auf dem Stackrahmen gespeichert (beispielsweise im RAM). Der Zugriff auf die in Registern gespeicherten Variablen erfolgt wesentlich schneller als bei den auf Stackrahmen gespeicherten Variablen, selbst wenn sich die Variable auf dem Rahmen im Prozessorcache befinden sollte. Es können nur 64 Variablen registriert werden. Alle anderen werden dem Stack hinzugefügt. Neben der Registrierung gibt es noch weitere Optimierungen, die eine Verfolgung der Verwendung erfordern. Die Zahl der formalen Argumente und lokalen Variablen für eine Methode sollte unter 64 liegen, um eine maximale Anzahl an JIT-Optimierungen zu gewährleisten. Beachten Sie jedoch, dass sich diese Zahl in zukünftigen Versionen der CLR ändern kann.

HINWEIS Methoden sollten kurz gehalten sein. Dafür gibt es mehrere Gründe, wie z.B. die für Methodeninlining, Registrierung und JIT benötigte Zeit.

Weitere JIT-Optimierungen
Der JIT-Compiler führt noch eine Vielzahl weiterer Optimierungen durch: Konstanten- und Kopierpropagation, invariantes Schleifenhoisting und einiges mehr. Sie müssen für diese Optimierungen keine expliziten Programmiermuster verwenden.

Warum sehe ich diese Optimierungen nicht in Visual Studio?
Wenn Sie in Visual Studio eine Anwendung durch Auswählen der Option Starten im Menü Debuggen oder durch Drücken von F5 starten, werden alle JIT-Optimierungen ungeachtet der Tatsache deaktiviert, ob es sich bei der von Ihnen erstellten Anwendung um eine Release- oder Debugversion handelt. Wenn eine verwaltete Anwendung über einen Debugger gestartet wird, gibt der JIT nicht optimierte x86-Anweisungen aus, selbst wenn es sich nicht um einen Debugbuild der Anwendung handelt. Wenn der JIT optimierten Code ausgeben soll, müssen Sie die Anwendung entweder von Windows Explorer aus starten oder in Visual Studio STRG+F5 drücken. Verwenden Sie cordbg.exe, wenn Sie die optimierte Disassembly anzeigen und mit dem nicht optimierten Code vergleichen möchten.

HINWEIS Sie können mithilfe von cordbg.exe die Disassembly von optimiertem und nicht optimiertem Code anzeigen, der vom JIT ausgegeben wurde. Starten Sie die Anwendung mit cordbg.exe, und legen Sie dann den JIT-Modus durch folgende Eingabe fest:

(cordbg) mode JitOptimizations 1

JIT's will produce optimized code

(cordbg) mode JitOptimizations 0

Der JIT gibt dann debugfähigen (nicht optimierten) Code aus.

Werttypen

Die CLR stellt zwei verschiedene Typsätze bereit, Verweistypen und Werttypen. Verweistypen werden stets auf dem verwalteten Heap zugewiesen und (wie der Name schon sagt) nach Verweis übergeben. Werttypen werden auf dem Stack oder inline als Teil eines Objekts auf dem Heap zugewiesen und standardmäßig nach Wert übergeben, obwohl dies auch nach Verweis erfolgen kann. Werttypen lassen sich sehr kostengünstig zuweisen und als Argumente übergeben, solange sie klein und einfach gehalten sind. Ein gutes Beispiel für eine passende Verwendung des Werttyps wäre ein Point-Werttyp mit einer x- und einer y-Koordinate.

"Point"-Werttyp

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

Werttypen können auch wie Objekte behandelt werden. So ist es beispielsweise möglich, Objektmethoden auf ihnen aufzurufen, sie in Objekte umzuwandeln oder dorthin zu übergeben, wo ein Objekt erwartet wird. In diesen Fällen wird der Werttyp jedoch mittels eines als Boxing ("Verpacken") bezeichneten Prozesses in einen Verweistyp konvertiert. Wenn ein Werttyp verpackt wird, wird auf dem verwalteten Heap ein neues Objekt zugewiesen und der Wert in das neue Objekt kopiert. Dies ist ein kostspieliger Vorgang, der das durch den Einsatz von Werttypen gewonnene Leistungsplus wieder reduzieren oder völlig zunichte machen kann. Wenn der verpackte Typ implizit oder explizit wieder in einen Werttyp umgewandelt wird, ist er entpackt.

"Box"-/"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 die ToString-Methode u.U. überschreiben. Wenn Sie diese Methode nicht überschreiben, führen Aufrufe von ToString auf Ihrem Werttyp zu einem Boxing des Typs. Dies gilt auch für die anderen Methoden, die von System.Object abgeleitet werden. In diesem Fall ist dies Equals, obwohl ToString wahrscheinlich die am häufigsten aufgerufene Methode ist. Wenn Sie wissen möchten, ob und wann Ihr Werttyp verpackt wird, können Sie in MSIL mithilfe des ildasm.exe-Dienstprogramms nach der box-Anweisung suchen (siehe Codeausschnitt oben).

Überschreiben der "ToString()"-Methode in C# zur Vermeidung 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 bei der Erstellung von Auflistungen (beispielsweise einer ArrayList vom Typ float), dass jedes Objekt beim Hinzufügen zur Auflistung verpackt wird. Sie sollten für Ihren Werttyp die Verwendung eines Arrays oder die Erstellung einer benutzerdefinierten Auflistungsklasse in Betracht ziehen.

Implizites Boxing bei der 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

Die Verwendung von Fehlerbedingungen als normale Flussteuerung ist weit verbreitet. Wenn Sie in diesem Fall versuchen, einer Active Directory-Instanz einen Benutzer programmtechnisch hinzuzufügen, und E_ADS_OBJECT_EXISTS HRESULT zurückgegeben wird, wissen Sie, dass dieser Benutzer bereits im Verzeichnis existiert. Sie können das Verzeichnis jedoch auch nach dem Benutzer durchsuchen und ihn nur dann hinzufügen, wenn Sie ihn nicht finden.

Eine derartige Verwendung von Fehlern für die normale Flusssteuerung wird im Kontext der CLR als Antimuster für die Leistung gesehen. Die Fehlerbehandlung in der CLR erfolgt mittels strukturierter Ausnahmebehandlung. Verwaltete Ausnahmen sind sehr kostengünstig, solange Sie sie nicht ausgeben. Wenn eine Ausnahme ausgegeben wird, erfordert dies einen Stackwalk in der CLR, um einen passenden Ausnahmehandler für die ausgegebene Ausnahme zu finden. Stackwalking ist ein sehr kostspieliger Vorgang. Ausnahmen sollten, wie ihr Name bereits sagt, nur ausnahmsweise oder unter unerwarteten Umständen verwendet werden.

HINWEIS Bei leistungskritischen Methoden ist es u.U. ratsam, für erwartete Ergebnisse ein aufgelistetes Ergebnis zurückzugeben, anstatt eine Ausnahme auszugeben.
HINWEIS Es gibt eine Reihe von .NET-CLR-Ausnahmeleistungsindikatoren, die Ihnen mitteilen, wie viele Ausnahmen in Ihrer Anwendung ausgegeben werden.
HINWEIS Wenn Sie VB.NET verwenden, sollten Sie Ausnahmen anstelle von On Error Goto einsetzen. Das error-Objekt verursacht hier nur unnötige Kosten.

Threading und Synchronisierung

Die CLR stellt zahlreiche Threading- und Synchronisierungsfunktionen einschließlich der Möglichkeit bereit, eigene Threads, einen Threadpool sowie verschiedene Synchronisierungsprimitiven zu erstellen. Bevor Sie die Threadingunterstützung in der CLR nutzen, sollten Sie sich jedoch Ihre Verwendung von Threads genau ansehen. Beachten Sie, dass das Hinzufügen von Threads Ihren Durchsatz reduzieren anstatt verbessern kann und mit Sicherheit die Speichernutzung erhöht. Bei Serveranwendungen für Multiprozessorgeräte kann das Hinzufügen von Threads den Durchsatz durch parallele Ausführung beträchtlich verbessern (obwohl dies von der Anzahl der Sperrkonflikte, wie beispielsweise einer Serialisierung der Ausführung, abhängt). Bei Clientanwendungen kann das Hinzufügen eines Threads zum Anzeigen von Aktivität und/oder Fortschritt (bei geringen Durchsatzeinbußen) zu einer Verbesserung der wahrgenommenen Leistung führen.

Wenn die Threads Ihrer Anwendung weder auf eine bestimmte Aufgabe spezialisiert noch mit einem besonderen Status verknüpft sind, sollten Sie eine Verwendung des Threadpools in Betracht ziehen. Wenn Sie den Win32-Threadpool schon einmal verwendet haben, wird Ihnen der CLR-Threadpool vertraut vorkommen. Pro verwaltetem Prozess gibt es eine einzelne Threadpoolinstanz. Der Threadpool trifft intelligente Entscheidungen hinsichtlich der zu erstellenden Threadanzahl und passt sich entsprechend der Geräteauslastung an.

Threading und Synchronisierung sind eng miteinander verbunden. Alle mittels Multithreading in Ihrer Anwendung gewonnen Durchsatzsteigerungen können durch schlecht geschriebene Synchronisierungslogik wieder zunichte gemacht werden. Die Granularität von Sperren kann den Durchsatz Ihrer Anwendung insgesamt stark beeinflussen. Grund dafür sind die Kosten, die durch die Erstellung und Verwaltung der Sperre entstehen, sowie die Tatsache, dass Sperren die Ausführung potenziell serialisieren können. Um Ihnen diesen Punkt zu verdeutlichen, versuchen wir als Beispiel, einer Verzeichnisstruktur einen Knoten hinzuzufügen. Wenn es sich bei der Verzeichnisstruktur beispielsweise um eine freigegebene Datenstruktur handelt, müssen mehrere Threads während der Ausführung der Anwendung darauf zugreifen können. Das bedeutet, dass Sie den Zugriff auf diese Verzeichnisstruktur synchronisieren müssen. Sie können die gesamte Verzeichnisstruktur sperren, während Sie einen Knoten hinzufügen, denn dann entstehen Ihnen nur Kosten für das Erstellen einer einzigen Sperre. Andere Threads jedoch, die währenddessen versuchen, auf die Verzeichnisstruktur zuzugreifen, blockieren wahrscheinlich. Dies wäre ein Beispiel für eine grobkörnige Sperre. Sie haben aber auch die Möglichkeit, beim Durchlaufen der Verzeichnisstruktur jeden Knoten einzeln zu sperren. Allerdings kumulieren Sie dann Kosten für jede Knotensperre. Diese Vorgehensweise hat jedoch den Vorteil, dass die anderen Threads nur dann blockieren, wenn sie versuchen, genau auf den von Ihnen gesperrten Knoten zuzugreifen. Dies wäre ein Beispiel für eine feinkörnige Sperre. Sie erhalten eine passendere Sperrgranularität, indem Sie nur den Strukturzweig sperren, an dem Sie gerade arbeiten. Beachten Sie, dass Sie in diesem Beispiel wahrscheinlich eine freigegebene Sperre (RWLock) verwenden würden, da mehrere Leser zur selben Zeit Zugriff haben sollten.

Die einfachste und leistungsstärkste Möglichkeit, synchronisierte Vorgänge durchzuführen, ist der Einsatz der System.Threading.Interlocked-Klasse. Die Interlocked-Klasse stellt eine Reihe von atomaren Operationen auf niedriger Ebene bereit: Increment, 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 wohl am häufigsten verwendete Synchronisierungsmechanismus ist Monitor oder ein kritischer Abschnitt. Eine Monitor-Sperre kann entweder direkt oder über das lock-Schlüsselwort in C# eingesetzt werden. Das lock-Schlüsselwort synchronisiert den Zugriff des angegebenen Objekts auf einen bestimmten Codeblock. Eine Monitor-Sperre, auf die nur selten zugegriffen wird, ist hinsichtlich der Leistung recht kostengünstig. Allerdings steigen die Kosten, wenn sie häufig frequentiert wird.

Das "lock"-Schlüsselwort in C#

//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

Die RWLock-Sperre bietet einen freigegebenen Sperrmechanismus: "Leser" können so die Sperre beispielsweise mit anderen "Lesern" teilen. Ein "Schreiber" kann dies jedoch nicht. In den Fällen, bei denen diese Vorgehensweise anwendbar ist, kann die RWLock-Sperre im Vergleich zu einer Monitor-Sperre besseren Durchsatz gewähren, da Letztere nur jeweils einem einzelnen Leser oder Schreiber den Zugriff auf die Sperre gestattet. Der System.Threading-Namespace enthält auch die Mutex-Klasse. Eine Mutex ist eine Synchronisierungsprimitive, die prozessübergreifende Synchronisierung zulässt. Beachten Sie hierbei jedoch, dass dies wesentlich kostspieliger als ein kritischer Abschnitt ist und nur dann verwendet werden sollte, wenn eine prozessübergreifende Synchronisierung erforderlich ist.

Reflektion

Reflektion ist ein von der CLR bereitgestellter Mechanismus, mit dem Sie Typinformationen programmtechnisch zur Laufzeit abrufen können. Die Reflektion ist in hohem Maße von den Metadaten abhängig, die in verwalteten Assemblys eingebettet sind. Viele Reflektions-APIs erfordern ein Durchsuchen und Analysieren der Metadaten, was sehr kostenintensiv ist.

Die Reflektions-APIs können in drei Leistungsgruppen eingeteilt werden: Typvergleich, Memberaufzählung und Memberaufruf. Jede dieser Gruppen wird ständig teurer. Typvergleiche (in diesem Fall typeof in C#, GetType, is, IsInstanceOfType usw.) sind die kostengünstigsten Reflektions-APIs, auch wenn sie auf keinen Fall billig sind. Mit Memberauflistungen können Sie u.a. die Methoden, Eigenschaften, Felder, Ereignisse sowie die Konstruktoren einer Klasse programmtechnisch analysieren. Ein Verwendungsbeispiel hierfür wären Entwurfszeitszenarios, wo in diesem Fall die Eigenschaften der benutzerdefinierten Websteuerelemente für den Eigenschaftenbrowser in Visual Studio aufgelistet werden. Die teuersten Reflektions-APIs sind diejenigen, die das dynamische Aufrufen oder Ausgeben von Members einer Klasse, JIT sowie das Ausführen einer Methode ermöglichen. Es gibt natürlich auch spät gebundene Szenarios, bei denen ein dynamisches Laden von Assemblys, Instanziieren von Typen und Methodenaufrufe durchgeführt werden müssen. Allerdings erfordert diese lose Kopplung explizite Leistungskompromisse. Im Allgemeinen sollten Sie Reflektions-APIs in leistungsempfindlichen Codepfaden vermeiden. Beachten Sie hierbei jedoch Folgendes: Auch wenn Sie die Reflektion nicht direkt einsetzen, kann sie dennoch von einer verwendeten API angewandt werden. Achten Sie daher immer auf eine transitive Verwendung von Reflektions-APIs.

Spätes Binden

Spät gebundene Aufrufe sind ein Beispiel für eine Funktion, die die Reflektion versteckt einsetzt. Sowohl Visual Basic.NET als auch JScript.NET unterstützen spät gebundene Aufrufe. So müssen Sie eine Variable vor ihrer Verwendung beispielsweise nicht deklarieren. Spät gebundene Objekte sind eigentlich Typobjekte, und die Reflektion wird dazu verwendet, das Objekt zur Laufzeit in den korrekten Typ umzuwandeln. Ein spät gebundener Aufruf ist wesentlich langsamer als ein direkter Aufruf. Sie sollten diese Aufrufart in leistungskritischen Codepfaden vermeiden, wenn Sie eine spät gebundene Verhaltensweise nicht explizit benötigen.

HINWEIS Wenn Sie VB.NET verwenden und eine späte Bindung nicht unbedingt benötigen, können Sie den Compiler anweisen, diese zu verhindern. Dazu müssen Sie Option Explicit On und Option Strict On am Anfang Ihrer Quelldateien einfügen. Diese Optionen zwingen Sie, Ihre Variablen zu deklarieren und stark zu typisieren, und deaktivieren implizites Umwandeln.

Sicherheit

Sicherheit ist ein notwendiger und integraler Bestandteil der CLR, der gewisse Kosten mit sich bringt. Sollte der Code vollkommen vertrauenswürdig und die Sicherheitsrichtlinien standardmäßig aktiviert sein, dann wirkt sich die Sicherheit geringfügig auf den Durchsatz und die Startzeit Ihrer Anwendung aus. Teilweise vertrauenswürdiger Code (beispielsweise Code aus dem Internet oder dem Intranet) oder ein Einschränken der Gewährungseinstellungen des Arbeitsplatzes erhöhen die Leistungskosten für die Sicherheit.

COM-Interoperabilität und Plattformaufrufe

Systemeigene APIs werden dem verwalteten Code durch COM-Interoperabilität (COM-Interop) und Plattformaufrufe (P/Invoke) auf fast transparente Weise offen gelegt. Das Aufrufen der meisten systemeigenen APIs erfordert normalerweise keinen speziellen Code, höchstens ein Paar Mausklicks. Wie Sie vielleicht schon vermuten, birgt das Aufrufen systemeigenen Codes von verwaltetem Code aus (und umgekehrt) gewisse Kosten. Die Kosten bestehen aus einem festen Preis für die Übergänge zwischen systemeigenem und verwaltetem Code und variablen Preisen für das Marshalling von u.U. benötigten Argumenten und Rückgabewerten. Der feste Kostenbeitrag für COM-Interop und P/Invoke ist gering: für gewöhnlich weniger als 50 Anweisungen. Die Kosten für das Marshalling von und in verwaltete Typen hängen davon ab, wie unterschiedlich die Darstellungen auf beiden Seiten der Grenze sind. Typen, die eine recht umfangreiche Transformation benötigen, sind kostenintensiver. So sind zum Beispiel alle Zeichenfolgen in der CLR Unicode-Zeichenfolgen. Wenn Sie nun 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, obwohl ein systemeigenes Ganzzahlarray erwartet wird, ist kein Marshalling notwendig.

Da das Aufrufen von systemeigenem Code mit Leistungskosten verbunden ist, sollten Sie sicherstellen, dass diese Kosten gerechtfertigt sind. Wenn Sie einen systemeigenen Aufruf durchführen, sollte die von ihm verrichtete Arbeit die mit dem Durchführen des Aufrufs verbundenen Kosten rechtfertigen. Die Methoden sollten kompakt sein und nicht viele Einzelaufrufe enthalten. Die Kosten für einen systemeigenen Aufruf lassen sich am besten durch Messen der Leistung einer systemeigenen Methode, die keine Argumente annimmt und über keinen Rückgabewert verfügt, und dann durch Messen der Leistung der systemeigenen Methode feststellen, die Sie aufrufen möchten. Der Leistungsunterschied zwischen den beiden Methoden vermittelt Ihnen einen Eindruck von den Marshallingkosten.

HINWEIS Erstellen Sie keine Einzelaufrufe, sondern kompakte COM-Interop- und P/Invoke-Aufrufe, und stellen Sie sicher, dass die Kosten für den Aufruf durch die von dem Aufruf durchgeführte Arbeit gerechtfertig werden.

Beachten Sie, dass mit verwalteten Threads keine Threadingmodelle verknüpft sind. Wenn Sie einen COM-Interop-Aufruf durchführen möchten, muss der Thread, auf dem der Aufruf vorgenommen wird, für das korrekte COM-Threadingmodell initialisiert werden. Dies wird normalerweise mithilfe von MTAThreadAttribute und STAThreadAttribute durchgeführt (obwohl es auch programmtechnisch vorgenommen werden kann).

Leistungsindikatoren

Es gibt eine Reihe von Windows-Leistungsindikatoren für die CLR von .NET. Diese Leistungsindikatoren sollte ein Entwickler als erstes zu Rate ziehen, wenn er auf ein Leistungsproblem stößt oder versucht, die Leistungsmerkmale einer verwalteten Anwendung zu identifizieren. Einige der Indikatoren für die Speicherverwaltung und für Ausnahmen wurden in diesem Artikel bereits erwähnt. Es gibt Leistungsindikatoren für fast jeden Aspekt der CLR und .NET Framework. Diese Indikatoren sind stets verfügbar und greifen nicht in das System ein, sie haben geringe Overheads und ändern die Leistungsmerkmale Ihrer Anwendung nicht.

Weitere Tools

Neben den Leistungsindikatoren und CLR Profiler sollten Sie auch einen konventionellen Profiler einsetzen, um die zeitintensivsten und am häufigsten aufgerufenen Methoden in Ihrer Anwendung zu bestimmen. Denn diese Methoden müssen Sie zuerst optimieren. Es gibt eine Reihe von kommerziellen Profilern, die verwalteten Code unterstützen, wie beispielsweise DevPartner Studio Professional Edition 7.0 von Compuware und VTuneT Performance Analyzer 7.0 von Intel®. Compuware bietet darüber hinaus mit DevPartner Profiler Community Edition auch einen kostenlosen Profiler für verwalteten Code an.

Schlussfolgerung

In diesem Artikel wurde die Leistung der CLR und von .NET Framework nur ansatzweise untersucht. Es gibt noch viele weitere Aspekte der CLR- und .NET Framework-Architektur, die die Leistung Ihrer Anwendung beeinflussen. Ich kann Entwicklern nur empfehlen, keine Erwartungen hinsichtlich der Leistung der Zielplattform der Anwendung und der verwendeten APIs vorauszusetzen. Messen Sie alles!

Ich wünsche Ihnen viel Spaß beim Jonglieren.

Ressourcen


Anzeigen:
© 2014 Microsoft