Von Wolken und Drachen

RyuJIT und .NET Native: .NET-Performance
Kommentare

Bei aller Aufregung und dem Hype um Microsofts neue Compilerplattform „Roslyn“ sowie all die Neuerungen, die dadurch in die nächsten Versionen von Visual Studio und dem .NET Framework Einzug halten, übersieht man leicht die Änderungen, die aktuell unter der Haube des .NET Frameworks passieren. Was allerdings ziemlich schade ist, denn Neuerungen wie RyuJIT und vor allem .NET Native muten nahezu revolutionär an, führt man sich die Tradition von .NET als Managed-Code-Plattform vor Augen.

Native Code vs. Managed Code. Zwei Welten, ja zwei Weltanschauungen, prallen hier aufeinander. Welches dieser fundamentalen Paradigmen ist besser? Welches einfacher? Welches performanter? Diese Fragen sind so alt wie die Geschichte der Informatik. Nun ja, zugebenermaßen vielleicht nicht ganz so alt, aber zeitgleich mit dem Aufkommen von Programmiersprachen wie Smalltalk oder Lisp in den Siebzigern, die bereits auf Managed Code und Garbage Collection setzten, wurde eine Diskussion in Gang gesetzt, die bis zum heutigen Tag keine der beiden Seiten eindeutig für sich entscheiden konnte. Auch wenn nach dem Durchbruch von Sprachen wie C#, VB.NET oder JavaScript schon so mancher den Untergang bzw. die baldige Bedeutungslosigkeit traditioneller Native-Code-Sprachen wie C/C++ vorhergesagt hatte, konnte man in den letzten Jahren sowas wie eine C++-Renaissance beobachten, die in erster Linie dem Smartphone- und Mobile-App-Boom geschuldet ist. In gewisser Weise sah man sich auf diesen mobilen Plattformen technologisch um Jahre zurückkatapultiert und die beschränkten Ressourcen machten eine Rückbesinnung auf traditionelle Technologien und Native Code zur Tugend. Zusätzlich bieten Native-Code-Applikationen oft den Vorteil, in sich abgeschlossen zu sein, da keine Abhängigkeiten auf bestimmte Versionen einer Laufzeitumgebung wie .NET vorhanden sind.

Microsofts .NET-Team hat sich dieser Thematik in den letzten Jahren angenommen und mit .NET Native einen Technologiewandel eingeläutet, der die entzweiten Lager der Managed-Code- und Native-Code-Entwickler vereinen und die Vorteile dieser beiden Welten endlich zusammenführen könnte. Doch auch weniger radikale Innovationen aus dem .NET-Entwicklungsteam gibt es zu vermelden, denn mit RyuJIT befindet sich gerade ein brandneuer, verbesserter 64-Bit-JIT-(Just-in-Time-)Compiler in der Entwicklung. Werfen wir also zuerst einen Blick auf die traditionelle Managed-Code-Seite von .NET und die Optimierungen, die ein neuer JIT-Compiler dort bieten kann.

Der Drache RyuJIT

Bereits vor längerer Zeit wurde die Notwendigkeit eines neuen 64-Bit-.NET-JIT-Compilers erkannt. Dieses Erfordernis ergab sich aus der historischen Trennung in 32-Bit-Desktop- sowie 64-Bit-Serversysteme und deren jeweils unterschiedlichen Performanceanforderungen an Applikationen. War es im 32-Bit-Desktopbereich notwendig, Applikationen möglichst schnell starten zu können, so lag im Serverbereich der Fokus auf der Effizienz und der Performance von lang laufenden Prozessen, während die zum Starten benötigte Zeit von eher untergeordneter Bedeutung war (Kasten: „JIT-Qualitätscheck“). Mittlerweile verschwimmen die Grenzen zwischen diesen beiden Welten, und es besteht der Bedarf nach schnell startenden 64-Bit-Desktopanwendungen, aber auch auf Servern ist die Applikationsstartzeit mittlerweile ein wichtiges Kriterium geworden. Der aktuelle 64-Bit-.NET-JIT-Compiler (JIT64) wurde noch unter dem traditionellen Serversystemparadigma entworfen und entwickelt und kann somit modernen Anforderungen an einen JIT-Compiler nur mehr schwer entsprechen. Aus diesem Grund wurde der Ruf nach einer Neuentwicklung laut und mit RyuJIT hat mittlerweile ein neuer 64-Bit-.NET-JIT-Compiler das Licht der Welt erblickt.

JIT-Qualitätscheck Bei der Beurteilung der Leistungsfähigkeit eines JIT-Compilers gilt es, mehrere Faktoren zu beachten. • Durchsatz: Dieses Kriterium gibt Auskunft darüber, wie schnell ein JIT-Compiler „echten“ Maschinencode aus dem plattformunabhängigen Code erzeugen kann. Ganz wesentlich hat dieses Kriterium Einfluss auf die Startzeit einer Anwendung, da jene Zeit, die zum Kompilieren der Anwendung verwendet wird, im JIT-Compiler und nicht im eigentlichen Anwendungscode verbracht wird. Hier hat der klassische .NET-64-Bit-JIT-Compiler aktuell seine größten Defizite, und die erklärten Ziele von RyuJIT liegen in einer Verdoppelung des Durchsatzes. • Codequalität: Dieses Kriterium beschreibt die Schnelligkeit der Ausführung des erzeugten Maschinencodes durch den JIT-Compiler. Auch hier lautet das grundsätzliche Ziel für RyuJIT, bessere Ergebnisse als der aktuelle 64-Bit-JIT-Compiler zu erzielen. Allerdings liegt die Latte für die Codequalität hier bereits sehr hoch, und der Fokus auf den Durchsatz macht es in manchen Fällen schwieriger, komplexe Optimierungen anzustellen. Allerdings unterstützt RyuJIT hardwareseitige Parallelisierung in Form von SIMD, was in geeigneten Applikationen teils dramatische Geschwindigkeitssteigerungen ermöglicht. • Vorhersehbarkeit: Dieses Kriterium beschreibt, wie Durchsatz und Codequalität in verschiedenen Szenarien variieren. Das Ziel hier sind möglichst konsistente Ergebnisse unabhängig vom gestellten Problem (d. h. von der Anwendung, die der JIT-Compiler übersetzen muss). Für das Entwicklungsteam von RyuJIT bedeutet die Erfüllung dieses Qualitätskriteriums ein besseres oder zumindest ebenbürtiges Ergebnis bei sämtlichen Compiler-Benchmarks mit dem aktuellen 64-Bit-JIT-Compiler.

Interessant ist alleine schon die Namensherkunft dieses 64-Bit-JIT-Compilers der neuen Generation. Hierbei handelt es sich um ein Wortspiel, das auf Anhieb wohl nur US-amerikanische Compilerentwickler und Informatikstudenten verstehen werden, die auch der japanischen Sprache mächtig sind. Zu den Standardwerken des Compilerbaus zählt dort nämlich das so genannte „Dragon Book“, also „Drachenbuch“. Das japanische Wort für Drache wiederum lautet „Ryu“, und somit handelt es sich bei RyuJIT also um den „Drachen“ der Just-in-Time-Compiler. Einen „dezenten“ Hinweis auf diese Namensetymologie findet man auch bereits im Installationsdialog von RyuJIT (Abb 1).

Abb. 1: RyuJIT – kein gewöhnlicher Installationsdialog

Bleiben wir gleich beim Thema Installation. Die stets aktuellste Version von RyuJIT (zum Zeitpunkt der Entstehung dieses Artikels CTP4) kann hier heruntergeladen werden. Hier gilt es lediglich zu beachten, dass RyuJIT nur auf Windows-Versionen ab Vista and Windows Server 2008 installiert werden kann. Selbstverständlich muss auch eine 64-Bit-Variante von Windows vorliegen, da es sich eben um einen 64-Bit-Just-in-Time-Compiler handelt. Ansonsten gibt es hier keine weiteren Hürden, und RyuJIT kann gefahrlos einer bereits bestehenden Installation von .NET hinzugefügt werden. Gefahrlos deswegen, weil RyuJIT den vorhandenen 64-Bit-JIT-Compiler nicht ersetzt, sondern erst manuell aktiviert werden muss, will man in den Genuss dessen verbesserter Features kommen.

Möchte man gleich aufs Ganze gehen und den neuen JIT-Compiler systemweit aktivieren, kann dies über folgenden Registry-Eintrag geschehen:

HKLMSOFTWAREMicrosoft.NETFrameworkAltJit
(Default) (REG_SZ) = "*"

Wer etwas vorsichtiger vorgehen möchte, kann RyuJIT auch gezielt für einzelne Applikation aktivieren und testen, indem folgende Umgebungsvariable gesetzt wird:

set COMPLUS_AltJit=*

In der PowerShell lässt sich dasselbe Ergebnis mit folgendem Kommando bewerkstelligen:

$Env:COMPLUS_AltJit="*"

Beide Registrierungsmechanismen veranlassen die .NET-64-Bit-CLR von diesem Zeitpunkt an, RyuJIT anstelle des klassischen JIT-Compilers zu verwenden. Will man diese Einstellung rückgängig machen, da beispielsweise unerwartete Probleme auftauchen (schließlich handelt es sich bei RyuJIT um Pre-Release-Software), kann das durch die Entfernung des Registry-Eintrags bzw. der Umgebungsvariable jederzeit geschehen.

Nachdem RyuJIT nun erfolgreich installiert und aktiviert wurde, wollen wir diesen Just-in-Time-Compiler einem ersten Test unterziehen. Zu diesem Zweck existiert eine ganze Reihe von Benchmarks, die auch vom RyuJIT-Entwicklungsteam herangezogen werden, um den Fortschritt dieses Projekts zu dokumentieren und zu demonstrieren. Ein sehr eindrucksvoller Benchmark zugunsten von RyuJIT ist „FractalPerf“, ein einfaches C#-Programm zur Berechnung von Fraktalen, das hier heruntergeladen werden kann. Dieses Programm wurde auf zwei verschiedenen Systemen (einem Quad-Core-Intel-i7-16-GB-RAM-Entwicklungsrechner sowie ein Dual-Core-Intel-Celeron-8-GB-RAM-Medien-PC) mit nahezu identischen Speed-ups von Faktor 3 getestet (Tabelle 1).

System JIT 64 RyuJIT Speed-up
Entwickler-PC Quad-CoreIntel i7 16 GB RAM 18,165 Sekunden 5,514 Sekunden 3,29
Medien-PC Dual-Core Intel Celeron, 8 GB RAM 61,912 Sekunden 20,357 Sekunden 3,04

Tabelle 1: Ausführungsergebnisse der „FractalPerf“-Benchmarks

Natürlich handelt es sich bei diesem Beispiel um einen sehr günstigen Fall, und es gibt durchaus auch Fälle, in denen der traditionelle JIT-Compiler die Oberhand behält. Die Tendenz allerdings, vor allem was das erklärte Ziel, nämlich die Startzeit von Applikationen betrifft, geht deutlich zugunsten von RyuJIT aus. Wer also heute schon von der verbesserten Leistung dieses neuen Compilers profitieren möchte, kann die aktuellste CTP von RyuJIT ohne großes Risiko einsetzen. Die Installation verursacht keine permanenten Änderungen an einer bestehenden .NET-Installation und kann über einfache Registry-Einträge bzw. Umgebungsvariablen aktiviert und deaktiviert werden.

[ header = Seite 2: Compiler in the Cloud und .NET Native ]

Compiler in the Cloud und .NET Native

Wer das vorhergehende Thema rund um den neuen .NET-JIT-Compiler bereits spannend fand, den wird die .NET-Native-Technologie hoffentlich in helle Begeisterung versetzen. .NET Native hat das ehrgeizige Ziel, Native- und Managed-Code- Entwicklung zu vereinen, um Entwicklern und Benutzern die Vorteile beider Welten zugutekommen zu lassen.

Um es auf den Punkt zu bringen: .NET Native ist eine Technologie, die es ermöglicht, .NET-Applikationen in abgeschlossene Native-Code-Anwendungen zu überführen, die keine Abhängigkeiten auf ein installiertes .NET Framework mehr haben. Es werden nur jene Teile des .NET Frameworks in das fertige Executable übernommen, die auch tatsächlich von der Anwendung benötigt werden, was einen optimierten Memory-Footprint verspricht. Anstelle des CIL-(Common-Intermediate-Language-)Codes erhält man außerdem den fertigen Maschinencode für das Zielsystem (x86, x64 oder ARM), was einen schnelleren Applikationsstart ermöglicht, da der JIT-Compilerschritt vollständig wegfällt. Zusätzlich soll auch noch durch Wiederverwendung des optimierenden C++-Compiler-Backends der fertige Maschinencode verbesserte Performancecharakteristiken aufweisen. Optimierungen in sämtlichen Aspekten der Applikationsausführung – so lautet das erklärte Ziel von .NET Native.

Aktuell ist die .NET-Native-Technologie jedoch nur für Windows-Store-Anwendungen anwendbar und erst als CTP verfügbar. Windows-Store-Apps eignen sich als erster Kandidat für diese Technologie natürlich deshalb besonders gut, weil sie nur auf ein eingeschränktes Set des .NET Frameworks Zugriff haben und die Codeanalyse sowie das Erzeugen von Maschinencode dort etwas überschaubarer ist als bei Applikationen, die auf dem vollständigen .NET Framework aufbauen. Zusätzlich bieten sich diese Apps aufgrund von Microsofts langfristiger „Compiler in the Cloud“-Strategie an, bei der Entwickler ihre Anwendungen im Windows Store lediglich im CIL-Format hinterlegen. Erst dort (also in der Cloud) sorgt der .NET-Native-Compiler dafür, dass diese Anwendungen für die jeweiligen Zielsysteme in Maschinencode übersetzt werden. Auf diese Art und Weise können Updates, sicherheitskritische Patches oder Performanceverbesserungen aktiv vom Windows Store gepusht werden, ohne dass ein Eingreifen des Entwicklers notwendig wäre. Ein ähnlicher Mechanismus findet auch bereits bei Windows-Phone-Apps Anwendung, allerdings handelt es sich dabei noch um keine vollständige Übersetzung der Anwendung in Maschinencode (mehr dazu im Kasten „Altes neu?“).

Altes neu? Wer sich bei .NET Native an einen alten Bekannten in Form von NGen erinnert fühlt oder meint, über diese Technologie im Zusammenhang mit Windows-Phone-Apps schon gestolpert zu sein, liegt nicht ganz falsch. Ein kurzer Überblick soll Klarheit über Unterschiede und Gemeinsamkeiten dieser Technologien bringen:• NGen (Native Image Generator) steht seit der ersten Version von .NET zur Verfügung und dient ebenfalls dazu, maschinenunabhängigen CIL-Code in spezifischen Maschinencode zu übersetzen. Anwendungen können damit „vorkompiliert“ werden, um ein schnelleres Starten zu ermöglichen. Der JIT-Compiler steht zur Laufzeit allerdings immer noch zur Verfügung, wenn beispielsweise Code dynamisch nachgeladen wird. Anwendungen, die NGen verwenden, haben gegenüber reinen CIL-Anwendungen somit einen Geschwindigkeitsvorteil beim Programmstart, benötigen jedoch immer noch ein vollständig installiertes .NET Framework. • Compiler in the Cloud: Diese Technologie wurde erstmals mit Windows-Phone-Anwendungen und dem Windows Phone Store bekannt. Wenngleich Idee und Technologie ähnlich zu .NET Native ist, handelt es sich hierbei lediglich um eine Vorstufe zu .NET Native. Der eigentliche Maschinencode wird nach wie vor erst am Endgerät erzeugt, jedoch entsteht aus dem CIL-Code im Windows Phone Store bereits voroptimierter Maschinenzwischencode (MDIL – Machine Dependent Intermediate Language), der den eigentlichen JIT-Vorgang auf dem Endgerät vereinfacht. Hier kann dieses Thema vertieft werden. • .NET Native ist die radikalste Vorgehensweise unter den angeführten Optimierungstechnologien, da hier zum einen der JIT-Schritt vollständig wegfällt (es wird Maschinencode erzeugt), genauso wie die Abhängigkeit zu einem installierten .NET Framework. Zusätzlich sorgt die Wiederverwendung des optimierenden C++-Compiler-Backends für hochoptimierten Code auf dem entsprechenden Zielsystem.

Wer heute schon einen ersten Eindruck der .NET-Native-Technologie gewinnen möchte, kann dies in der aktuellen Preview (CTP3 zum Zeitpunkt der Entstehung dieses Artikels) von Visual Studio „14“ tun. Auch für Visual Studio 2013 steht .NET Native als separate Erweiterung zur Verfügung, sofern man sich hier für die Preview registriert. Die nachfolgenden Screenshots und Beispiele stammen jedoch aus der Visual Studio „14“ CTP, die übrigens auch als virtuelle Maschine in Microsofts Azure Cloud zur Verfügung steht.

Nach dem Erstellen einer neuen Windows-Store-App ist der erste Schritt und die Grundvoraussetzung zur Verwendung von .NET Native, die Selektion einer spezifischen Zielsystemarchitektur, z. B. x64 (Abb. 2). Anschließend kann im Kontextmenü des Projekts der Eintrag ENABLE FOR .NET NATIVE ausgewählt werden (Abb. 3). Durch das Auswählen dieses Menüpunkts wird die Datei default.rd.xml dem Projekt hinzugefügt. Diese Datei enthält eine Reihe von Metainformationen, die die .NET-Native-Infrastruktur zum Erstellen der Anwendung braucht. Anhand dieser Informationen wird auch entschieden, welche Teile des .NET Frameworks statisch in die Anwendung übernommen werden, um die „Abgeschlossenheit“ der Anwendung zu garantieren. Zusätzlich wird eine statische Codeanalyse auf der Anwendung ausgeführt und ein Kompatibilitätsbericht generiert. Idealerweise sieht dieser Kompatibilitätsbericht wie in Abbildung 4 aus, und .NET Native lässt sich problemlos auf die Applikation anwenden. Mit dem Menüeintrag RUN STATIC ANALYSIS FOR .NET NATIVE im Kontextmenü des Projekts kann dieser Kompatibilitätsbericht jederzeit neu angefordert und erzeugt werden.

Abb. 2: Zuerst muss eine spezifische Prozessorarchitektur (hier x64) ausgewählt werden

Abb. 3: Im Kontextmenü des Projekts „.NET Native“ selektieren

Abb. 4: Ein „idealer“ .NET-Native-Kompatibilitätsbericht

Bevor man die Anwendung nun kompiliert, sollte man sich noch davon überzeugen, dass im Eigenschaftsdialog des Projekts auch tatsächlich das Kontrollkästchen mit der Option COMPILE WITH .NET NATIVE TOOL CHAIN aktiviert ist (Abb. 5). Erst dadurch wird beim Kompilieren der Anwendung der fertige Maschinencode erzeugt. Erkennen lässt sich das auch durch den relativ langen Compile-Vorgang, auf den auch im Ausgabe Fenster hingewiesen wird: „.NET Native Build starting: Several compilation stages will occur. Please be patient as this may take several minutes.“

Abb. 5: Den .NET-Native-Compiler in den Projekteigenschaften aktivieren

Die fertig kompilierte .NET-Native-Applikation lässt sich nun wie gewohnt starten und auch debuggen. Interessant ist hier allerdings ein Blick auf die Modules-Ansicht (Abb. 6). Die verschiedenen Assemblies aus dem .NET Framework, die unsere Anwendung benötigt, werden zwar wie gewohnt aufgelistet, allerdings wird in der Spalte Path der Dateipfad als embedded angegeben. Dies ergibt selbstverständlich Sinn, da wir eine in sich abgeschlossene Applikation debuggen, die keine externen Modulabhängigkeiten auf das .NET Framework mehr aufweist.

Abb. 6: Die Modulpfade werden als „embedded“ angeführt

Auch sonstige .NET-Standardfunktionalitäten wie die Garbage Collection stehen in einer .NET-Native-Applikation selbstverständlich zur Verfügung. Selbst Reflection kann grundsätzlich verwendet werden, auch wenn es natürlich nicht möglich ist, Klassen nachzuladen, die nicht Teil des ursprünglichen Applikationspakets waren. Ansonsten aber kann Reflection wie gewohnt eingesetzt werden, wie die folgenden Codebeispiele demonstrieren. Listing 1 zeigt, wie eine Instanz der Klasse TestClass (Listing 2) mithilfe von Reflection erzeugt und deren public-Methode Greet aufgerufen wird.

private async void Button1_Click(object sender, RoutedEventArgs e)
{
  var t = typeof(TestClass);
  var testClass = (TestClass)Activator.CreateInstance(t);
  // Call public method using reflection.
  var method = t.GetTypeInfo().GetDeclaredMethod("Greet");
  var message = (string)method.Invoke(testClass, null);

  var dlg = new MessageDialog(message);
  await dlg.ShowAsync();
}
public class TestClass
{
  private string _message = "Hi";

  internal string Message
  {
    get
    {
      return _message;
    }
  }

  public string Greet()
  {
    return Message + "!";
  }
}

Auch der Zugriff mittels Reflection auf Properties, im Fall unserer Testklasse die Property Message mit der Sichtbarkeit internal stellt für .NET-Native-Anwendungen kein Problem dar (Listing 3).

private async void Button2_Click(object sender, RoutedEventArgs e)
{
  var t = typeof(TestClass);
  var testClass = (TestClass)Activator.CreateInstance(t);
  // Get internal property using reflection.
  var property = t.GetTypeInfo().GetDeclaredProperty("Message");
  var message = (string)property.GetValue(testClass);

  var dlg = new MessageDialog(message);
  await dlg.ShowAsync();
}

Selbst der Reflection-basierte Zugriff auf Felder mit der Sichtbarkeit private lässt sich mit .NET Native anstandslos bewerkstelligen (Listing 4). Abbildung 7 zeigt die Bildschirmausgabe unserer Beispielanwendung, deren Quellcode hier zur Verfügung steht.

private async void Button3_Click(object sender, RoutedEventArgs e)
{
  var t = typeof(TestClass);
  var testClass = (TestClass)Activator.CreateInstance(t);
  // Get private field using reflection.
  var field = t.GetTypeInfo().GetDeclaredField("_message");
  var message = (string)field.GetValue(testClass);

  var dlg = new MessageDialog(message);
  await dlg.ShowAsync();
}

Abb. 7: Reflection in .NET-Native-Anwendungen

Fazit

RyuJIT und .NET Native sind beides spannende und sehr vielversprechende Technologien. Speziell RyuJIT erweckt den Eindruck, bereits sehr weit gediehen zu sein. Der Autor hat RyuJIT seit CTP1 auf mehreren Systemen aktiviert und kann die Technologie als äußert zuverlässig und stabil beschreiben, da es bisher zu keinerlei unerwartetem Programmverhalten kam. Microsoft hat mit der Modernisierung des 64-Bit-JIT-Compilers jedenfalls den richtigen Schritt getan und eine wichtige technologische Investition in die Zukunft getätigt.

.NET Native mag für manche aufgrund des eingeschränkten Anwendungsbereichs auf Windows-Store-Anwendungen noch nicht ganz so interessant erscheinen. Allerdings ist ein klar abgegrenztes SoftwareÖkosystem, wie der Windows-Store ihn darstellt, der ideale erste Kandidat für eine Technologie wie .NET Native. Die zukünftige Version von ASP.NET mit der abgespeckten Core-CLR-Variante könnte das prädestinierte nächste Einsatzgebiet sein und der Technologie zu mehr Reife verhelfen. Langfristig ermöglichen die so gewonnenen Erfahrungen hoffentlich eine Anwendbarkeit von .NET Native auf das gesamte .NET-Universum; und Diskussionen der Natur „Native versus Managed Code“ gehören endgültig der Vergangenheit an.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -