Die Performance von C#-Programmen

Die Programme der Megos sind heute im Prinzip sehr schnell, denn sie basieren auf C++-Code, arbeiten nicht exzessive mit dynamisch alloziertem Speicher und setzen direkt auf dem WIN32 API auf. Da stellt sich die Frage, wie es mit der Geschwindigkeit wohl aussehen wird, wenn wir dereinst diese Programme ablösen werden durch solche, die in C# geschrieben sind, wo schon der kleinste String ein volles Objekt ist und man in Form des .NET Frameworks eine ganze Menge Code zwischen sich und dem WIN32 API hat?

Um hier etwas Klarheit zu schaffen, habe ich mich heute erstmal schlau gemacht betreffend folgender vereinfachter Frage: Wenn man Code hat, der in etwa dasselbe tut, einmal in C++, und einmal in C#, ist dann der C#-Code per se langsamer, und wenn ja, um wieviel etwa?

Es hat sich herausgestellt, dass diese Frage nicht ganz einfach zu beantworten ist, denn Zeitmessungen haben so ihre Tücken. Ich habe z.B. in einem Forum Postings von jemandem gelesen, der versucht hat, die relative Geschwindigkeit dieser beiden Programmiersprachen (bzw. deren Implementationen auf Windows, wie sie heute sind) mit Hilfe kleiner Schleifen zu vergleichen, und zurückmeldete, die C++-Version sei 28000 Mal so schnell wie die C#-Version. Was war passiert? Der C++-Compiler war so schlau zu erkennen, dass eine zeitfressende Anweisung innerhalb der Schleife von der Schleife unabhängig war und hat sie als Optimierung aus der Schleife herausgenommen…

C#-Source-Code wird vom C#-Compiler übersetzt zu CIL (Common Intermediate Language), früher auch MSIL (Microsoft Intermediate Language) genannt, einem prozessorunabhängigen Bytecode, wie man z.B. in der Wikipedia hier nachlesen kann. Dieser wird dann üblicherweise bei der ersten Ausführung des Codes per JIT (Just in Time) Compiler quasi „fliegend“ in direkte Prozessoranweisungen übersetzt und so schliesslich ausgeführt. Ob diese Übersetzung flüchtig ist oder dauerhaft abgespeichert wird, so dass später bei einer erneuten Programmausführung der Übersetzungsschritt gespart werden kann, hängt von verschiedenen Faktoren ab.

Kann es hier unter Umständen Geschwindigkeitsprobleme geben? Interpretiert man Bytecode statt direkt Prozessoranweisungen auszuführen, ist das schliesslich zwangsweise um einige Faktoren langsamer. Hatte nicht Java, das ebenfalls auf einem Bytecode basiert, vor Jahren zum Teil arge Geschwindigkeitsprobleme, bevor die HotSpot-JVM mit einem JIT-Compiler eingeführt wurde? Bei meinen Abklärungen heute erlebte ich diesbezüglich eine kleine Überraschung: Die Microsoft CLR scheint gar keinen Interpreter für CIL zu beinhalten, es wird nie interpretiert, sondern vor der Ausführung immer alles JIT-compiliert! (Details hierzu z.B. hier.) Also kann es hier keinen prinzipiellen Geschwindigkeitsverlust geben.

Das heisst nicht, dass keine CIL-Interpreter existieren: Mono hat zumindest in der Anfangsphase mit einem Interpreter gearbeitet, bevor ein JIT-Compiler zur Verfügung stand, und es gibt Dinge wie Dot Net Anywhere, einem CIL-Interpreter für Kleinstcomputer. Aber zumindest auf Windows lauert kein solcher und wartet auf Gelegenheiten, Performance zu vernichten.

Geschwindigkeitsunterschiede können sich auch durch Laufzeitprüfungen ergeben: C# überwacht normalerweise jeden einzelnen Array-Zugriff auf einen gültigen Index, während C++ das normalerweise nicht tut. In einem kleinen, synthetischen Testprogramm mit einer Schleife um Array-Zugriffe herum können sich hier schon einmal signifikante Unterschiede ergeben, aber ehrlich, in welchem Programm aus dem richtigen Leben wird schon so ein grosser Teil der Rechenzeit verbracht, und wer wollte allen Ernstes heute noch auf Laufzeitprüfungen verzichten?

Interessanterweise gibt es auch in C# Ansätze für Optimierungen, wenn man denn einmal ein Programmstück hat, das von Array-Zugriffen absolut dominiert wird. Im bereits erwähnten Thread finden sich folgende informative Code-Schnippsel:

Code A:

int[] arry = new int[20000];
for (int index = 0; index < 20000; ++index) { total += arry[index]; } [/sourcecode] Code B: [sourcecode lang="csharp"] int[] arry = new int[20000]; for (int index = 0; index < arry.Length; ++index) { total += arry[index]; } [/sourcecode] Code C: [sourcecode lang="csharp"] int[] arry = new int[20000]; foreach (int element in arry) { total += element; } [/sourcecode] Code B sei 20% schneller als Code A, weil durch die Verwendung von arry.Length der Compiler mitbekommt, dass er auf Bereichsprüfungen verzichten darf (wozu er wohl im Beispiel A nicht schlau genug ist), und Code C sei sogar 50% schneller als Code A, weil es offenbar foreach schafft, einen internen Index schneller durchzuzählen als das ein Programm explizit kann, und weil natürlich ebenfalls Bereichsprüfungen wegfallen.

Solche Tipps und noch viele mehr findet man offenbar in einem schlauen Buch namens Effective C#. Microsofts C# Programming Guide hat ebenfalls Informationen über Optimierungen, zu finden hier, und generelle Informationen darüber, was in Managed Code wie teuer ist, hier. Und wer einmal ganz tief in die CLR und ihre Objekt-Verwaltung hineinschauen möchte, um Ansätze für Optimierungen zu finden, kann das mit Hilfe der Informationen hier tun.

Zusammenfassend kann ich sagen, nirgends Hinweise darauf gefunden zu haben, dass man sich schon alleine durch die Verwendung von C# als Sprache generelle Performance-Probleme einhandelt, weil C# eben „langsam“ ist. Schätzungen, die ich angetroffen habe, gehen davon aus, dass im allgemeinen C#-Code 80% bis 90% der Performance von C++-Code erreicht. Das dürfte zu verkraften sein!

Advertisements
Veröffentlicht in Keine Kategorie. Schlagwörter: , . 9 Comments »