Die Leistungsvorteile durch NGen

Veröffentlicht: 11. Jul 2006
Von Surupa Biswas

Typischerweise werden Methoden in verwalteten ausführbaren Dateien Just-In-Time (JIT) kompiliert. Der durch den JIT-Compiler erstellte Maschinencode wird bei Beendigung des Prozesses, der die ausführbare Datei ausführt, verworfen, daher muss die Methode beim erneuten Ausführen der Anwendung wieder neu kompiliert werden. Außerdem ist der Code an den ihn erstellenden Prozess gebunden und kann von Prozessen, die dieselbe Anwendung ausführen, nicht gemeinsam verwendet werden.

Auf dieser Seite

 Warum NGen verwenden?
 Verwenden von NGen
 Assemblys und der GAC
 Auswählen von Basisadressen
 Vermeiden von neuen Basisadressen
 NGen und mehrere Anwendungsdomänen
 Verwenden von fester Bindung
 Erneutes Erstellen von NGen-Images
 Schlussbemerkungen
 Der Autor

Diese Eigenschaften der .NET JIT-Kompilierung können zu Leistungsnachteilen führen. Glücklicherweise bietet NGen (Native Image Generation – Erstellen von systemeigenen Images) hier Unterstützung an. NGen erlaubt die Vorkompilierung von MSIL-Anwendungen (Microsoft® Intermediate Language) in Maschinencode vor der Ausführungszeit. Dies führt zu zwei wesentlichen Leistungsvorteilen. Erstens wird die Dauer zum Starten der Anwendung verringert, da eine Kompilierung zur Ausführungszeit vermieden wird. Zweitens wird die Speichernutzung verbessert, da ein gemeinsames Verwenden der Codepages durch mehrere Prozesse unterstützt wird.

Obwohl NGen auf den ersten Blick einer herkömmlichen statischen Back-End-Kompilierung zu entsprechen scheint, ist es tatsächlich etwas ganz anderes. Im Unterschied zu statisch kompilierten Binärdateien werden NGen-Images an die Maschine gebunden, auf denen sie erstellt wurden, und können daher nicht allgemein bereitgestellt werden. Stattdessen muss das Installationsprogramm einer Anwendung Befehle ausführen, um systemeigene Images der jeweiligen Assemblys auf dem Clientcomputer zu erstellen. Ebenfalls abweichend zu herkömmlichen Binärdateien bilden NGen-Images einen Cache. Verwaltete Anwendungen werden auch dann ordnungsgemäß ausgeführt, wenn alle NGen-Images gelöscht wurden. In diesem Fall gibt es selbstverständlich einen Leistungseinbruch, jedoch kein Problem mit der ordnungsgemäßen Ausführung. Auf der anderen Seite kann die ordnungsgemäße Ausführung von verwalteten Anwendungen jedoch fehlschlagen, wenn die MSIL-Assemblys nach der Kompilierung in NGen-Images gelöscht werden.

Warum NGen verwenden?

Durch NGen wird üblicherweise die Warmstartzeit von Anwendungen verbessert, manchmal auch die Kaltstartzeit. Unter Kaltstartzeit ist die notwendige Zeit zum erstmaligen Starten einer Anwendung nach einem Neustart des Computers zu verstehen. Die Warmstartzeit bezieht sich auf die notwendige Zeit zum Starten einer Anwendung, die bereits einmal auf dem Computer ausgeführt wurde (ohne zwischenzeitliche Neustarts des Computers).

Die Kaltstartzeit ist in erster Linie von der Anzahl der Speicherseiten abhängig, die vom Datenträger eingelesen werden müssen. Die Verbesserung der Kaltstartzeit beim Verwenden von NGen kann hauptsächlich darauf zurückgeführt werden, dass auf die beim Kompilieren zu berücksichtigenden MSIL-Seiten zur Ausführungszeit nicht mehr zugegriffen werden muss.

Die Verbesserungen der Warmstartzeit beruhen auf dem Wiederverwenden von Seiten der NGen-Images, die beim vorherigen Ausführen der Anwendung angelegt wurden. Dies führt insbesondere bei umfangreichen Anwendungen mit Benutzeroberfläche auf dem Client zu Vorteilen, bei denen die Startdauer für die Benutzerfreundlichkeit von Bedeutung ist.

NGen verbessert gleichzeitig die gesamte Speichernutzung des Systems, indem verschiedenen Prozessen, die gleiche Assemblys verwenden, das gemeinsame Verwenden der entsprechenden NGen-Images ermöglicht wird, wie in Abbildung 1 dargestellt. Dies kann in Client- und Serverszenarios sehr nützlich sein, in denen die Speicherbeanspruchung gering gehalten werden muss. Ein klassisches Beispiel ist das Terminaldienste-Szenario, in dem eine große Anzahl von Benutzern angemeldet ist und die gleiche Anwendung zur gleichen Zeit ausführt. Wenn Sie Bibliotheken oder andere wieder verwendbare Komponenten erstellen, möchten Sie möglicherweise NGen einsetzen, damit die Ihre Komponenten verwendenden Anwendungen die erstellten Codepages gemeinsam verwenden können.

Gemeinsame Codeverwendung von Prozessen
Abbildung 1: Gemeinsame Codeverwendung von Prozessen

Es ist wichtig festzustellen, dass NGen nicht immer zu einer Verbesserung der Kaltstartzeit von Anwendungen führt, da NGen-Images größer als MSIL-Assemblys sind. Der einzige Weg, um zu überprüfen, ob die Kaltstartzeit und das Workingset durch NGen in bestimmten Szenarios verbessert oder verschlechtert werden, besteht im Messen der entsprechenden Werte. Weitere Informationen zur Leistungsmessung folgen später.

Verwenden von NGen

Die NGen-Vorkompilierung können Sie mit dem Tool ngen.exe durchführen, das mit dem verteilbaren Paket von Microsoft .NET Framework bereitgestellt wird. .NET Framework 2.0 bietet eine vollständig überarbeitete Version dieses Tools und einen neuen NGen-Dienst namens .NET Runtime Optimization (Laufzeitoptimierung), der eine Kompilierung von Assemblys durch NGen im Hintergrund ermöglicht. Auf diese Weise unterstützt NGen eine synchrone und eine asynchrone Kompilierung. Die vollständige Syntax für ngen.exe finden Sie unter .NET Framework Tools (Seite in englischer Sprache).

Alle NGen-Images werden im systemeigenen Imagecache installiert, der sich im Verzeichnis %WINDIR%\assembly befindet. Durch den Befehl "NGen install foo.exe" wird beispielsweise ein systemeigenes Image („native image“) mit dem Namen foo.ni.exe erstellt und im systemeigenen Imagecache installiert. Abbildung 2 stellt den Speicherort des systemeigenen Images für System.Web.dll dar.

Systemeigener Imagecache
Abbildung 2: Systemeigener Imagecache

Beim Verwenden des NGen-Tools können Sie entscheiden, ob Sie Ihre Assemblys synchron (z. B. "NGen install foo.exe") oder asynchron auf einer von drei möglichen Prioritätsebenen (z. B. "NGen install foo.exe /queue:3") kompilieren. Die asynchronen Befehle kehren sofort zurück und werden im Hintergrund durch den NGen-Dienst verarbeitet. Die in der Warteschlange befindlichen Assemblys mit den Prioritätsebenen 1 oder 2 werden so bald wie möglich durch den NGen-Dienst kompiliert (Assemblys der Prioritätsebene 1 werden bevorzugt). Assemblys der Prioritätsebene 3 werden durch den Dienst kompiliert, wenn sich das System im Leerlauf befindet. Sie können zum Erzwingen der Ausführung aller in der Warteschlange befindlichen Einträge auch den Befehlzeilenschalter executeQueuedItems verwenden, wie in Abbildung 3 dargestellt.

Asynchrone NGen-Kompilierung
Abbildung 3: Asynchrone NGen-Kompilierung

Das Verwenden einer synchronen NGen-Kompilierung zur Installationszeit gewährleistet, dass die Anwendung ausschließlich mit NGen-Vorkompilierungen aller Assemblys durch den Endbenutzer gestartet werden kann. Dies führt jedoch zu einer längeren Installationsdauer, da die notwendige Zeit zur Kompilierung hinzukommt. Als Alternative kann das Installationsprogramm der Anwendung asynchrone NGen-Befehle absetzen. Asynchrone NGen-Befehle mit den Prioritätsebenen 1 oder 2 stellen dabei sicher, dass die systemeigenen Images kurze Zeit nach Abschluss der Installation bereitstehen. Es besteht jedoch das Risiko, dass sich das Kompilieren nachteilig auswirkt und die Leistung anderer Anwendungen oder die Antwortzeit des Systems auf Benutzereingaben beeinträchtigt.

Die andere Möglichkeit besteht im Verwenden der asynchronen NGen-Kompilierung mit der Prioritätsebene 3, die weder Computerleistung während der Installation beansprucht noch die Systemleistung beeinträchtigt. Da sich Computer jedoch nach der Installation möglicherweise für längere Zeit nicht im Leerlauf befinden, könnte der Endbenutzer die Anwendung starten und verwenden, bevor der NGen-Dienst die Möglichkeit zum Kompilieren der Assemblys hat. Aus diesem Grund können Sie diesen Kompromiss genau anpassen, indem Sie die Assemblys Ihrer Anwendung in Kategorien aufteilen und diese unterschiedlich vorkompilieren. Für den Start der Anwendung extrem wichtige Assemblys können synchron kompiliert werden, weniger wichtige können mit den Prioritätsebenen 1 und 2 durch den Dienst kompiliert werden, eher selten verwendete Anwendungsbereiche mit Prioritätsebene 3.

Assemblys und der GAC

Um die bestmögliche Leistung mit NGen zu erzielen, müssen alle Assemblys der Anwendung starke Namen verwenden und vor der Kompilierung im GAC (Global Assembly Cache) installiert werden. Verwendet eine Assembly starke Namen, ist jedoch nicht im GAC installiert, so muss der Loader die Signatur der Assembly zur Ladezeit überprüfen. Zum Überprüfen der starken Namenssignatur muss dieser den gesamten Inhalt der MSIL-Assembly analysieren, einen Hashwert berechnen und diesen anschließend mit der Signatur der Assembly vergleichen. Bei diesem Überprüfungsvorgang wird daher jede Seite der MSIL-Assembly vom Datenträger gelesen. Dies führt zu einer Spitze im Workingset der Anwendung, verlängert die Ladedauer und belastet den Datenträgercache des Betriebssystems.

Wurde die Assembly jedoch im GAC installiert, so muss der Loader die starke Namenssignatur nicht zur Ladezeit überprüfen, da die Signatur bereits beim Installieren der Assembly im GAC überprüft wurde. Da der GAC ein sicherer Speicherort ist, der lediglich von einem Administrator geändert werden kann, und die Signatur beim Installieren in den GAC bereits überprüft wurde, verzichtet der Loader auf ein erneutes Überprüfen. Wenn der Loader beim Versuch, eine reine MSIL-Assembly aus dem GAC zu laden, ein passendes NGen-Image entdeckt, wird direkt dieses NGen-Image geladen. In diesem Szenario wird nicht eine einzige Seite der MSIL-Assembly in den Speicher geladen.

Dieser Unterschied im Verhalten führt üblicherweise zu einer deutlichen Abweichung der Startzeit von NGen-kompilierten Anwendungen. Sofern Sie keinen triftigen Grund haben, die Assemblys nicht im GAC zu installieren, wird diese Vorgehensweise dringend empfohlen, um die bestmögliche Leistung mit NGen zu erzielen.

Natürlich entsteht der Aufwand zum Überprüfen der starken Namenssignatur zur Laufzeit in dem Fall, in dem nicht im GAC installiert wird, unabhängig davon, ob die Anwendung JIT- oder NGen-kompiliert ist. Beim Arbeiten mit NGen ist dies jedoch besonders unangenehm, da einige oder auch alle MSIL-Seiten, die zur Überprüfung der Signatur in den Speicher geladen werden, zur Ausführung möglicherweise gar nicht benötigt werden. Informationen zum Signieren und anschließenden Installieren einer Assembly im GAC finden Sie in "How to install an assembly into the Global Assembly Cache in Visual C#" (in englischer Sprache).

Auswählen von Basisadressen

Um eine gute Workingset-Leistung durch NGen zu erreichen, sollten Sie Basisadressen für die Anwendungsassemblys unbedingt sorgfältig auswählen, sodass die NGen-Images nicht kollidieren. Wenn die Anwendung lediglich aus einer einzelnen EXE-Datei und daher einem einzelnen systemeigenen Image besteht, ist das in diesem Abschnitt beschriebene Auswählen von geeigneten Basisadressen nicht erforderlich.

Beim Laden einer ausführbaren Datei, ob verwaltet oder nicht verwaltet, muss das Betriebssystem einen zusammenhängenden Block virtuellen Speichers zuordnen, dessen Größe der ausführbaren Datei entspricht. Der Inhalt der Datei wird anschließend dem Speicherblock zugeordnet. Die Programmimages können Funktionen und Methoden, globale Variablen und Konstanten sowie Verweise darauf enthalten. Viele Verweise bestehen aus absoluten Speicheradressen. Diese Verweise setzen das Laden des Images an einer bestimmten Speicheradresse voraus, die als bevorzugte Basisadresse der ausführbaren Datei bezeichnet wird.

Solange das Betriebssystem in der Lage ist, das Image der ausführbaren Datei an der bevorzugten Basisadresse zu laden, bleiben alle im Image enthaltenen Verweise gültig und können verwendet werden. Überdies kann das Image von mehreren Prozessen gemeinsam verwendet werden. Dies führt typischerweise zu einer wesentlichen Einsparung im Workingset gemeinsam genutzter Komponenten. Wenn der Loader das Modul jedoch nicht an der gewünschten Adresse laden kann (da es zu Überschneidungen mit einem anderen, bereits geladenen oder zugeordneten Modul oder Datenfragment kommen würde), wird das Modul mit einer neuen Basisadresse versehen, also an eine abweichende Adresse geladen. Dies führt dazu, dass alle Adressen im Image der ausführbaren Datei angepasst werden müssen.

Diese Anpassungen gehen zu Lasten des Durchsatzes, da sie zur Ladezeit durchgeführt werden müssen. Auch aus Sicht des Workingsets sind sie aufwändig, da jede Seite in der Binärdatei, die eine Speicherreferenz enthält, in den Speicher geladen und aktualisiert werden muss, selbst wenn die Referenz während der Ausführung nie verwendet wird. Zudem werden alle Seiten durch das Schreiben privat und können nicht mehr von mehreren Prozessen gemeinsam verwendet werden. Daher kommt es zu einer erheblichen Leistungseinbuße, wenn systemeigener Code mit einer neuen Basisadresse versehen wird.

Für mit NGen vorkompilierte verwaltete Anwendungen stellt das Abweichen von Basisadressen eine größere Beeinträchtigung dar als für nicht verwaltete Binärdateien. Die Ursache dafür ist die feste Bindung (Näheres dazu gleich). Wenn eine Assembly A fest an eine Abhängigkeit B gebunden ist und B nicht an der bevorzugten Basisadresse zur Verfügung steht, müssen nicht nur alle Zeiger im NGen-Image von B angepasst werden, sondern auch alle Zeiger in A, die auf einen Typ oder eine Methode in B zeigen. Diese wegen der festen Bindung notwendigen Anpassungen können zu einem erheblichen Anwachsen des Workingsets führen, unter Umständen werden dadurch alle Vorteile von NGen aufgehoben.

Bei JIT-kompiliertem Code gibt es das Problem neuer Basisadressen nicht, da die Adressen zur Laufzeit generiert werden, abhängig von der aktuellen Position des Codes im Speicher. Gleichzeitig ist MSIL weniger von abweichenden Basisadressen betroffen, da MSIL-Verweise eher auf Tokens als auf Adressen basieren. Aus diesem Grund ist ein System beim Verwenden des JIT-Compilers nicht anfällig für Basisadresskonflikte.

Es gibt derzeit keine automatische Unterstützung zum Verwalten von Basisadressen. Beim Erstellen einer Assembly entscheidet der Compiler über einen Vorgabewert, sofern der Entwickler nicht explizit eine bevorzugte Basisadresse angibt. Die ungünstige Folge davon, dem Compiler die Auswahl der Basisadressen zu überlassen, besteht darin, dass alle Binärdateien der Anwendung dieselbe bevorzugte Ladeadresse erhalten.

Vermeiden von neuen Basisadressen

Die einzige Möglichkeit, das beschriebene Zuordnen neuer Basisadressen bei NGen-Images zu vermeiden, besteht im sorgfältigen Zuordnen von Basisadressen zu den Assemblys der Anwendung. Denken Sie beim Auswählen von Basisadressen an den wesentlich größeren Umfang der NGen-Images im Vergleich zu den MSIL-Assemblys: typischerweise sind diese 2,5- bis 3-mal größer als die entsprechenden MSIL-Assemblys – auf x86-Systemen. Auf 64-Bit-Systemen sind sie noch größer.

Im Moment gibt es keine Möglichkeit zum direkten Angeben einer Ladeadresse für ein NGen-Image. Sie können jedoch eine Basisadresse für die MSIL-Assembly angeben. Für eine reine MSIL-Assembly wählt NGen die Basisadresse des systemeigenen Images so, dass sie der Adresse der Assembly entspricht. Das funktioniert ganz gut, solange entweder die Assembly oder das NGen-Image in den Speicher geladen werden muss, nicht jedoch beide. Für eine Assembly im gemischten Modus (eine Assembly, die systemeigenen und verwalteten Code enthält) müssen Assembly und NGen-Image möglicherweise gleichzeitig in den Speicher geladen werden. Für diese Assemblys wählt NGen daher die Basisadresse des NGen-Images so, dass sie der Basisadresse der MSIL-Assembly zuzüglich der Größe der MSIL-Assembly zuzüglich eines Puffers zum Aufrunden auf die nächste Seitengrenze entspricht.

Die 64-Bit-Images von NGen sind größer als die entsprechenden systemeigenen 32-Bit-Images. Beide können aus derselben plattformunabhängigen MSIL-Assembly erstellt werden. Wenn NGen identische Basisadressen für die systemeigenen 32-Bit- und 64-Bit-Images verwendet hat, führt das Auswählen von geeigneten Basisadressen für 32-Bit-Systeme zu Umsetzungen auf 64-Bit-Systemen. Auf der anderen Seite führt das Auswählen von geeigneten Basisadressen für 64-Bit-Systeme zur ineffizienten Nutzung von Adressräumen auf 32-Bit-Systemen. NGen löst dieses Problem, indem für 64-Bit-Images Basisadressen nach der Formel "Offset + N * Basisadresse der MSIL-Assembly" ausgewählt werden. Im Fall von .NET Framework 2.0 ist der Offset auf 0x64180000000 und für x64-Systeme N auf 2 festgelegt (diese Werte können sich in zukünftigen Versionen von .NET Framework ändern). Ein korrektes Auswählen von Basisadressen für 32-Bit-Images sollte üblicherweise dank dieses Algorithmus auch für 64-Bit-Images funktionieren.

Die anschließende Herausforderung für den Entwickler besteht im Auswählen der geeigneten Adressen für die MSIL-Assemblys. Da diese systemeigenen Images ungefähr die dreifache Größe der entsprechenden Assemblys aufweisen, besteht eine Methode der Adresswahl darin, sicherzustellen, dass zwei angrenzende Assemblys A1 und A2 durch mindestens die dreifache Größe von A1 voneinander getrennt werden. Im Fall von Assemblys im gemischten Modus, bei denen MSIL- und NGen-Image gleichzeitig geladen werden müssen, müssen A1 und A2 durch mindestens die vierfache Größe von A1 getrennt werden. Es ist grundsätzlich eine gute Idee, zusätzlichen Platz als Puffer zu berücksichtigen, da Assemblys (und damit die NGen-Images) im Lauf der Zeit möglicherweise durch Dienste oder Lokalisierung wachsen. Gleichzeitig ist der Faktor 3 oder 4 lediglich ein geschätzter Wert, er funktioniert jedoch mit den meisten Assemblys. Der einzige Weg zum Ermitteln des exakten Faktors für eine bestimmte Assembly besteht im Kompilieren der Assembly mit NGen und im Vergleichen der Größe des erstellten Images mit der Größe der MSIL-Assembly. Da NGen selbst die Basisadresse der MSIL-Assembly mit N multipliziert, führt das Auswählen von Adressen, die sicherstellen, dass 32-Bit-NGen-Images sich nicht überlappen, üblicherweise auch dazu, dass systemeigene 64-Bit-Images sich ebenfalls nicht überlappen.

Basisadressen können Assemblys in Visual Studio® 2005 (wie in Abbildung 4 dargestellt) oder durch den Schalter /baseaddress des C#-Compilers zugewiesen werden (weitere Informationen finden Sie in englischer Sprache in "How to: Specify a Base Address for a DLL" und "/baseaddress (Specify Base Address of DLL) (C# Compiler Options)"). Nachdem jeder Assembly eine Basisadresse zugewiesen wurde, sollten die Assemblys im GAC installiert werden. Dann sollte die NGen-Kompilierung durchgeführt und die Anwendung ausgeführt werden, um sicherzustellen, dass tatsächlich keine Kollisionen auftreten. Ein Tool zum Anzeigen von Basisadresskonflikten ist „Process Explorer“ von Sysinternals (in englischer Sprache). Dieses Tool zeigt die bevorzugten und die tatsächlichen Adressen aller in den Speicher geladenen Binärdateien an und hebt die verschobenen hervor.

Festlegen einer Basisadresse in Visual Studio 2005
Abbildung 4: Festlegen einer Basisadresse in Visual Studio 2005

NGen und mehrere Anwendungsdomänen

Beim Verwenden mehrerer Anwendungsdomänen werden Assemblys in der Standardeinstellung domänenspezifisch in die Anwendungsdomänen geladen. Dies führt zu einem Verwenden der einzelnen Assembly ausschließlich im Kontext der jeweiligen Anwendungsdomäne, in die sie geladen wurde. Wenn zwei Anwendungsdomänen dieselbe Assembly domänenspezifisch laden, verwenden beide jeweils eine Kopie des JIT-kompilierten Codes. Wahlweise können Sie Assemblys so festlegen, dass sie domänenunabhängig geladen werden. In diesem Fall werden die Assemblys in eine besondere Anwendungsdomäne mit der Bezeichnung "gemeinsame Domäne" (Shared Domain) geladen. Eine in der gemeinsamen Domäne befindliche Assembly gehört nicht zur Anwendungsdomäne, die sie zu laden versucht: Sie gehört zu allen im Prozess befindlichen Anwendungsdomänen. Diese Assemblys werden erst beim Beenden des Prozesses entladen. Daher kann eine andere Anwendungsdomäne, die dieselbe domänenunabhängige Assembly laden muss, einfach die bereits in der gemeinsamen Domäne geladene Kopie verwenden.

Um eine gute Leistung mit NGen und Anwendungen mit mehreren Anwendungsdomänen zu erreichen, sollten alle Assemblys als domänenunabhängig geladen werden. Wenn eine Assembly mit einem NGen-Image in eine Anwendungsdomäne als domänenspezifisch geladen wird, kann das NGen-Image nur beim erstmaligen Laden der Assembly verwendet werden. Nachfolgende Ladeversuche derselben Assembly in abweichenden Anwendungsdomänen führen zu einer JIT-Kompilierung des Codes. Nach dem domänenspezifischen Laden des NGen-Images initialisiert der Loader Datenstrukturen im NGen-Image, als Folge führt dieser Vorgang zu einer festen Bindung des Images an die Anwendungsdomäne. Nach diesem Vorgang kann der Code nicht mehr gemeinsam verwendet werden.

Ein anderer Ansatz zum Verwenden der JIT-Kompilierung besteht im Laden verschiedener Kopien des systemeigenen Images in jeder Anwendungsdomäne, die die entsprechende Assembly domänenspezifisch lädt. Dieser Ansatz erfordert jedoch ein Laden der einzelnen Kopien des NGen-Images (mit Ausnahme der ersten Kopie) an eine Adresse, die von der bevorzugten Basisadresse abweicht. Als Folge dessen müssen alle Adressen dieser Kopien des NGen-Images angepasst werden.

Das domänenspezifische Laden einer Assembly, die generische Instanzierungen enthält, kann zur JIT-Kompilierung führen, selbst wenn der Instanzierungscode im NGen-Image gespeichert ist und die Assembly möglicherweise erstmalig in der Anwendungsdomäne geladen wird. Das System erfordert das domänenunabhängige Instanzieren domänenunabhängiger, generischer Typen, damit die Instanzierung gemeinsam von Anwendungsdomänen verwendet werden kann. Als Beispiel soll eine Assembly A eine generische Instanzierung mit der Bezeichnung GI enthalten, die die Wertetypen T1 und T2 verwendet. Wenn T1 und T2 in Assemblys definiert sind, die domänenunabhängig geladen werden, wird GI als domänenunabhängige, generische Instanzierung betrachtet. Wenn nun eine Anwendungsdomäne die Assembly A domänenspezifisch laden möchte, muss das System diese Instanzierung JIT-kompilieren, obwohl der entsprechende Code im NGen-Image von A enthalten ist.

Wie ich bereits erwähnte, kann die JIT-Kompilierung in den beschriebenen Szenarios vermieden werden, wenn die Assemblys domänenunabhängig geladen werden. Es gibt jedoch auch einige Nachteile. Domänenunabhängigkeit ist nicht ohne Zugeständnisse zu erreichen. Erstens kann eine domänenunabhängige Assembly nicht entladen werden – selbst dann nicht, wenn alle Anwendungsdomänen, die diese Assembly verwenden, entladen wurden. Zweitens ist mit NGen kompilierter Code beim Zugriff auf statische Variablen langsamer als JIT-kompilierter Code. Die Ursache dafür liegt darin, dass jede Anwendungsdomäne ihre eigene Kopie der statischen Variable an einer anderen Speicheradresse speichert. Da derselbe mit NGen kompilierte Code von jeder Anwendungsdomäne ausgeführt wird, muss der Code zunächst die Anwendungsdomäne ermitteln, in deren Kontext er ausgeführt wird, und kann erst anschließend die zugehörige statische Variable laden oder speichern. Dieser zusätzliche Aufwand kann zu einer Beeinträchtigung der Durchsatzleistung mit C++-Code führen, der intensiv statische Variablen verwendet. Die Auswirkungen auf üblichen Anwendungscode (im Gegensatz zu portierten herkömmlichen Benchmarks) sind jedoch eher gering. Sie sollten die Leistung in domänenspezifischen und domänenunabhängigen Szenarios messen und prüfen, ob eine Verringerung des Durchsatzes vorliegt.

Eine Assembly kann domänenunabhängig geladen werden, wenn das Attribut LoaderOptimizationAttribute (Seite in englischer Sprache) in der Main-Methode der Assembly auf MultiDomain oder MultiDomainHost festgelegt wird. In der Standardeinstellung des Systems wird lediglich mscorlib.dll von allen Anwendungsdomänen gemeinsam verwendet. Wenn Sie dieses Attribut in Assembly A auf MultiDomain festlegen, kann A gemeinsam verwendet werden. Wenn Sie MultiDomainHost verwenden, kann A gemeinsam verwendet werden, jedoch nur dann, wenn A aus dem GAC geladen wird. Im letzten Fall werden Assemblys, die nicht aus dem GAC geladen werden, nicht gemeinsam verwendet und können daher jederzeit entladen werden. Unter den von mir skizzierten Leistungsgesichtspunkten sollten die meisten Assemblys im GAC installiert werden, daher sollte kaum ein Unterschied zwischen MultiDomain und MultiDomainHost auftreten.

Verwenden von fester Bindung

Die Durchsatzleistung von NGen-kompiliertem Code ist schlechter als die von JIT-kompiliertem Code. Dies ist eines der am häufigsten in der Literatur verwendeten Argumente gegen den Einsatz von NGen. In den Versionen 1.0 und 1.1 von .NET Framework liegt der Durchsatz von NGen-kompiliertem Code durchschnittlich 5 bis 10 Prozent unter der JIT-kompilierten Entsprechung. .NET Framework 2.0 enthält Verbesserungen für NGen, um den Abstand in der Durchsatzleistung zu verkleinern. Die Verbesserung bezieht sich in erster Linie auf ein neues Feature mit der Bezeichnung "feste Bindung". In .NET Framework 2.0 werden in der Standardeinstellung alle NGen-Images fest an die NGen-Images von mscorlib.dll und system.dll gebunden. Bereits alleine diese Änderung hat den Durchsatz von NGen-kompiliertem Code mit dem von JIT-kompiliertem Code vergleichbar gemacht. Sie können auch Ihre Assemblys fest an bestimmte Abhängigkeiten binden, um die Leistung weiter zu steigern.

Hauptsächlich eine Ursache ist verantwortlich für den geringeren Durchsatz von NGen-kompiliertem Code im Vergleich zu JIT-kompiliertem Code: assemblyübergreifende Verweise. In JIT-kompiliertem Code können assemblyübergreifende Verweise als direkte Aufrufe oder Sprünge implementiert werden, da die genauen Adressen der Verweise zur Laufzeit bekannt sind. In statisch kompiliertem Code müssen assemblyübergreifende Verweise jedoch eine Sprungtabelle durchlaufen, die zur Laufzeit mit den korrekten Adressen durch Ausführen eines Pre-Stub einer Methode gefüllt wird. Der Pre-Stub der Methode stellt neben anderen Dingen sicher, dass die systemeigenen Images der Assemblys, auf die diese Methode verweist, vor dem Ausführen der Methode in den Speicher geladen wird. Der Pre-Stub muss nur beim erstmaligen Aufruf der Methode ausgeführt werden, nachfolgende Aufrufe umgehen ihn. Bei jedem Aufruf der Methode müssen assemblyübergreifende Verweise jedoch einen Dereferenzierungsvorgang durchlaufen. Dies ist grundsätzlich die Ursache für den Durchsatzunterschied von 5 bis 10 Prozent von NGen-kompiliertem Code im Vergleich zu JIT-kompiliertem Code.

In .NET Framework 2.0 wurde ein neues Konzept eingeführt: feste oder weiche Bindung von Abhängigkeiten. Die weiche Bindung von Abhängigkeiten ist die gewohnte Art, das Verhalten entspricht meinen bisherigen Ausführungen. Eine feste Bindung von Abhängigkeiten bezieht sich auf "vertrauenswürdige" Abhängigkeiten in dem Sinn, dass sie immer oder nahezu immer geladen sind. Wenn eine Assembly A eine feste Bindung zu einer Abhängigkeit B aufweist, verweist der Code im NGen-Image von A direkt auf Typen und Methoden im NGen-Image von B, da das Laden des Images von B an der bevorzugten Basisadresse vorausgesetzt wird. Daher hat eine feste Bindung den Vorteil, den Aufwand einer Dereferenzierung überflüssig zu machen, der gewöhnlich bei NGen-kompiliertem Code für assemblyübergreifende Verweise notwendig ist. Dies kann die Durchsatzleistung erheblich steigern. In den meisten Fällen wird das Workingset ebenfalls verbessert, da die Tabellenseiten zur Anpassung nicht mehr benötigt werden.

Die feste Bindung hat jedoch auch ein paar Nachteile. Erstens müssen alle fest gebundenen Abhängigkeiten einer Assembly in den Speicher geladen werden, bevor die Assembly selbst geladen werden kann. Viele dieser Assemblys werden erst viel später benötigt (möglicherweise sogar nie), aus Sicht des Workingsets könnte es daher viel ökonomischer sein, diese Assemblys verzögert zu laden. Zweitens müssen bei einer Verschiebung einer fest gebundenen Abhängigkeit die NGen-Images beider Assemblys angepasst werden. Wenn daher zahlreiche Assemblys fest an eine bestimmte Assembly (Abhängigkeit) gebunden sind und diese Assembly eine neue Basisadresse erhält, hebt der erforderliche Anpassungsaufwand normalerweise den durch die Verwendung von NGen erzielten Gewinn wieder auf.

Aus diesem zweiten Grund müssen Sie die Auswahl der bevorzugten Ladeadresse einer Assembly sorgfältig durchführen, bevor Sie andere Assemblys fest an diese binden. So wurde beispielsweise die Basisadresse für mscorlib.dll sehr sorgfältig ausgewählt, um das Risiko einer neuen Basisadresse zu minimieren. In den meisten Szenarios verbessert die feste Bindung aller Assemblys an mscorlib.dll den Durchsatz und führt zu keiner Zunahme des Workingsets. Um den Durchsatz weiter zu steigern können Sie Attribute Ihrer Assemblys so festlegen, dass diese fest an ihre .NET Framework- oder andere Abhängigkeiten gebunden werden. Eine Assembly A kann beispielsweise fest an eine Abhängigkeit gebunden werden, indem das DependencyAttribute auf A oder das DefaultDependencyAttribute auf B festgelegt wird. Wenn in einer Assembly A häufig Aufrufe von Assembly B ausgeführt werden, kann der Entwickler das DependencyAttribute im Code auf folgende Weise für A festlegen, damit eine feste Bindung von B erfolgt:

[assembly: DependencyAttribute("AssemblyB", LoadHint.Always)]

Wenn andererseits alle Assemblys der Anwendung zahlreiche Aufrufe von Assembly B durchführen, kann der Entwickler stattdessen das DefaultDependencyAttribute im Code von B folgendermaßen festlegen, damit alle Assemblys fest an B gebunden werden:

[assembly: DefaultDependencyAttribute(LoadHint.Always)]

Erneutes Erstellen von NGen-Images

Eigentlich berichte ich hier nicht über eine gute Speicherausnutzung durch NGen oder das Erreichen eines guten Durchsatzes. Mein Anliegen ist das Sicherstellen, dass NGen-Images zu jeder Zeit verfügbar sind. Um sicherzustellen, dass NGen-Images jederzeit verfügbar sind, müssen sie erneut erstellt werden, wenn sie ungültig werden.

Es gibt zahlreiche Ursachen für das Ungültigwerden von NGen-Images nach deren Erstellung. Das systemeigene Image einer Assembly A kann beispielsweise durch Aktualisierungen von A ungültig werden, durch Aktualisierungen einer der A betreffenden Abhängigkeiten, Änderungen der Sicherheitsrichtlinien nach der Erstellung (z. B. das Ändern der Richtlinie von voller zu teilweiser Vertrauenswürdigkeit), Änderungen der Bindungsentscheidungen (z. B. Hinzufügen einer Anwendungskonfigurationsdatei mit Bindungsänderungen) usw. Diese Szenarios zum Ungültigwerden beziehen sich auf die derzeitige Arbeitsweise von NGen. So können systemeigene Images Anpassungen für assemblyübergreifende Verweise enthalten (wie weiter oben beschrieben), die als Metadatentokens beschrieben sind. Wird eine Assembly A erneut kompiliert, ändern sich deren Metadatentokens, und damit werden alle systemeigenen Images ungültig, die die bisherigen Tokens der Assembly A verwendet haben. Für fest gebundene Abhängigkeiten gibt es keine Anpassungen, stattdessen werden direkte Adressen verwendet. Diese direkten Adressen werden natürlich ebenfalls nach einem erneuten Kompilieren der Abhängigkeit ungültig.

Das Ändern der Sicherheitsrichtlinien wirkt sich aus, da NGen beim Erstellen von systemeigenen Images die zu dieser Zeit gültigen Sicherheitsrichtlinien verwendet, um Verknüpfungs- und Ableitungsanfragen aufzulösen. Werden die Richtlinien zu einem späteren Zeitpunkt geändert, müssen diese Anfragen erneut ausgewertet werden, der Code im NGen-Image wird damit ungültig.

Der häufigste Grund zum Ungültigwerden eines NGen-Images besteht darin, dass die zugehörige Assembly oder eine ihrer Abhängigkeiten aktualisiert wurde. Daher müssen nach jedem bereitgestellten Patch einer Assembly die NGen-Images für alle Assemblys, die von dieser abhängig sind, und das NGen-Image für die Assembly selbst (sofern sie vorhanden ist) erneut erstellt werden.

NGen-Images können mit dem update-Befehl von ngen.exe neu erstellt werden. Das Ausführen dieses Befehls führt zum Auflisten aller systemeigenen Images auf dem Computer, die erneut kompiliert werden müssen, zum erneuten Kompilieren, zum Installieren im systemeigenen Imagecache und zum Löschen der ungültigen Images. Jeder Patch für .NET Framework 2.0 führt am Ende den update-Befehl aus, um so sicherzustellen, dass sich alle systemeigenen Images auf dem neuesten Stand befinden – einschließlich der Images, die von Assemblys von Drittanbietern erstellt wurden. Zukünftig wird der update-Befehl auch immer dann ausgeführt werden, wenn eine neue Version von .NET Framework auf dem Computer installiert oder eine vorhandene Version deinstalliert wird. NGen-Images bleiben verfügbar, auch wenn eine Installation oder Deinstallation zu einem Rollback von Anwendungen oder einer Anpassung an eine andere Version des Frameworks führt. In diesem Sinne ist das Tool ngen.exe mit mehreren Versionen kompatibel. Wenn Sie beispielsweise Version N von ngen.exe mit einer Anwendung ausführen, die für Version N-1 von .NET Framework konfiguriert ist, werden tatsächlich systemeigene Images der Version N-1 erstellt.

Sie sollten immer an das erneute Erstellen von NGen-Images denken, wenn Sie Patches für Ihre Anwendungen einsetzen. Der update-Befehl hat eine sehr einfache Syntax: "NGen update" oder "NGen update /queue" (für synchrone bzw. asynchrone NGen-Aufrufe). Tatsächlich können die meisten Probleme von Endbenutzern mit NGen-Images durch ein einfaches Eingeben von "NGen update" in die Befehlszeile behoben werden. Gewöhnlich sollten Endbenutzer jedoch ngen.exe nicht für zufällig auf ihrem System befindliche verwaltete Assemblys einsetzen, wenn sie keinen triftigen Grund dafür haben. Das Hinzufügen von neuen systemeigenen Images zum systemeigenen Imagecache führt zu zusätzlichem Speicherplatzverbrauch und zusätzlicher CPU-Last beim erneuten Kompilieren der Images nach Aktualisierungen.

Schlussbemerkungen

Bevor ich meine Ausführungen zu NGen abschließe, möchte ich darauf hinweisen, dass ein Erstellen von NGen-Images für alle Assemblys einer Anwendung und das abschließende Behandeln aller Abhängigkeiten keineswegs sicherstellt, dass der JIT-Compiler (mscorjit.dll) zur Laufzeit nicht geladen wird. Das Laden von mscorjit.dll in den Speicher beansprucht ungefähr 200 KB zusätzlich zum Gesamtspeicherbedarf auf einem x86-System (der JIT-Compiler wird von allen verwalteten Anwendungen gemeinsam verwendet). Von größerer Bedeutung beim Laden des JIT-Compilers ist jedoch, dass dies immer auch bedeutet, dass Code dynamisch kompiliert wird. Dadurch wird wiederum auf MSIL-Seiten zugegriffen, werden dem Workingset der Anwendung private Codepages hinzugefügt usw. Es ist daher eine gute Idee, zu überprüfen, ob mscorjit.dll nach dem Erstellen der NGen-Images beim Ausführen Ihrer Anwendung in den Prozess geladen wurde. Zu diesem Zweck können Sie einfach einen Debugger einsetzen oder den Assembly Binding Log Viewer (Seite in englischer Sprache) verwenden.

Letzterer ermöglicht, wie in Abbildung 5 dargestellt, das Protokollieren aller Bindungen auf dem Datenträger und das Anzeigen der Liste aller geladenen NGen-Images (jeder Protokolleintrag erläutert den Lade- und Überprüfungsvorgang, wie in Abbildung 6 dargestellt). Wenn die Liste aller systemeigenen Images leer ist oder bestimmte Abhängigkeiten fehlen, wurde MSIL JIT-kompiliert. Sie können auch den neuen JitCompilationStart-Assistent für verwaltetes Debuggen verwenden und damit überprüfen, ob JIT-Kompilierungen beim Ausführen der Anwendung aufgetreten sind. Weitere Informationen zum Assistenten für verwaltetes Debuggen finden Sie im Artikel „Let The CLR Find Bugs For You With Managed Debugging Assistants“ (in englischer Sprache).

Protokollanzeige
Abbildung 5: Protokollanzeige

Wurde eine JIT-Kompilierung festgestellt, sollten Sie zunächst überprüfen, ob Abhängigkeiten vorhanden sind, die nicht mit NGen kompiliert werden konnten, da sie nicht gefunden wurden. Eine gute Möglichkeit, dies zu überprüfen, besteht im Analysieren der Ausgabe von ngen.exe auf möglicherweise aufgetretene Fehler oder Warnungen. Auch wenn keine Fehler oder Warnungen ausgegeben werden, kann der JIT-Compiler dennoch aufgrund der Verwendung bestimmter APIs aufgerufen werden. APIs, die MSIL oder Quellcode dynamisch erzeugen (z. B. Reflection.Emit, System.Text.RegularExpressions.RegexOptions.Compiled, System.CodeDom, System.Xml.Serialization, Type.MakeGenericType, MethodInfo.MakeGenericMethod), erfordern eine JIT-Kompilierung. Auch die Verwendung bestimmter Loader-APIs, z. B. Assembly.LoadFrom, erfordert eine JIT-Kompilierung, da Assembly.LoadFrom bei jedem Aufruf eine andere Assembly zurückgeben kann.

Wenn Sie die Verwendung von NGen in Betracht ziehen, sollten Sie die Größe des Workingsets Ihrer Anwendung mit und ohne NGen ermitteln. Um eine korrekte Ermittlung der Verbesserungen bei der Speichernutzung durch NGen durchzuführen, sollten Sie zunächst das Installationsprogramm für die Anwendung erstellen, das die geeigneten Assemblys im GAC und im systemeigenen Imagecache installiert. Vergleichen Sie dies anschließend mit der Konfiguration, in der die Assemblys dem GAC hinzugefügt werden, nicht jedoch dem systemeigenen Imagecache. Das einfache Kompilieren der Assemblys mit NGen und Betrachten des Workingsets mit und ohne NGen-Image einer bestimmten Assembly ergibt unter Umständen keinen korrekten Eindruck.

Letztendlich ist das Abwägen der Vor- und Nachteile ratsam, bevor Sie sich zum Verwenden von NGen entschließen. Zu den Vorteilen gehören verbesserte gemeinsame Nutzung, schnellere Startzeiten und geringerer Speicherbedarf. Zu den Nachteilen gehören zusätzlicher Speicherplatzbedarf und der Aufwand, die Images auf dem aktuellen Stand zu halten.

Der Autor

Surupa Biswas arbeitet als Program Manager im CLR-Team von Microsoft. Sie arbeitet am Back-End-Compiler der Laufzeitumgebung mit dem Fokus auf Vorkompilierungstechnologien. Senden Sie Fragen und Kommentare in englischer Sprache an  clrinout@microsoft.com.

Aus der Ausgabe Mai 2006 des MSDN Magazine. Dieser Artikel enthält auch Links zu englischsprachigen Seiten.


Anzeigen: