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!

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

9 Antworten to “Die Performance von C#-Programmen”

  1. Thomas Says:

    Danke, durchaus informativ. Dass foreach() schneller ist als for() hätte ich übrigens nicht gedacht.

  2. Chris Says:

    Was Sie schon immer über c# wissen wollten (aber nie zu fragen wagten)
    Sehr gut!

  3. Marcel Says:

    Das mit den Schleifen ist echt erstaunlich. Hätte mich jemand vorher vermuten lassen, hätte ich gewettet, dass es genau anders herum wäre. A schneller als B und B schneller als C.

  4. Anastasius Says:

    Das stimmt ja nicht, aufgrund von verschiedenen Performance Tools ist Code A am schnellsten, dann Code B und dann Code C

    DotTrace als Performance Tool wäre es nicht schlecht:

    Code A: 26 ms
    Code B: 46 ms
    Code C: 47 ms

  5. Steffi Says:

    @Vorredner:
    Was stimmt denn nun?
    Ist Code A oder Code C am schnellsten?

  6. Lukas Says:

    Würde mich jetzt aber auch interessieren.

  7. rob Says:

    Das Thema ist ja schon älter.
    Da es hier aber Fragen gab bzgl derSchnelligkeit der 3 Varianten, kurz eine Messung von 10 Durchläufen je Schleife:

    Durschnitt von:

    a: 0,00312757
    b: 0,00928144
    c: 0,00264287

    Variante b lässt sich auf das Niveau von a heben indem man die Größe des arrays vor der Schleife prüft

    Sprich:
    for (int index = 0; index < arry.Length; ++index)

    int temp=arry.Length;
    for (int index = 0; index < temp; ++index)

    Trotzdem liegt Variante c eindeutig vorn, wenn auch nur marginal

  8. björn Says:

    Ich hatte in einem Projekt allerdings ein Problem mit C#/Dotnet: Komponenten sind ähnlich Java AWT heavyweight. Bei dem Projekt waren sehr viele UI Elemente nötig, was zu Problemen führt: Generell können nur ca 10000 Elemente erzeugt werden, da dann keine User-Handles mehr zur Verfügung stehen. Die Performance bricht sehr schnell massiv ein(ab 1000 – 2000). AWT hatte damals ein ähnliches Problem, Swing ist deshalb komplett anders aufgebaut und fordert Komponenten nicht vom OS an sondern Rendert diese selber. Sicher ist es selten notwendig soviele Komponenten zu verwenden. Inwieweit das für Dotnet > 2 gilt kann ich nicht sagen, meine Aussage bezieht sich auf Windows.Forms.

  9. merano Says:

    Die angesprochenen Schleifen sind so trivial, das Visual-Studio mit C++ das Ergebnis komplett während des kompilierens berechnet und das fertig berechnete Ergebnis einsetzt. Wenn das ganze noch in einem Unterprogramm ausgelagert würde ist selbst das nicht mehr vorhanden – man kann also im Release nicht mal einen Breakpoint darauf setzen.

    Wie diese definitiv zu triviale Schleife die ursprüngliche Frage beantworten könnte erschliesst sich mir nicht. Der Beispielcode erzeugt in C++ immer 0.0 ms – lediglich die Messung selber würde etwas Zeit beanspruchen!


Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: