Der Historical Debugger in VS2010

Schon vor einiger Zeit hat Microsoft angekündigt, dass Visual Studio 2010 einen neuen, speziellen Debugger namens Historical Debugger enthalten wird. Man findet z.B. hier eine Ankündigung aus dem Oktober 2008:

Are you tired of constantly setting breakpoints to hone in on a pesky bug? How would you like to be able to step „back in time“ through your debugger? The Historical Debugger in Visual Studio Team System 2010 promises to revolutionize your debugging experience.

Ich sagte mir, na das ist doch mal was Interessantes: Irgendwie rückwärts durch ein Programm debuggen? Weil ich für Revolutionen immer zu haben bin und als „Systemprogrammierer“ einfach wissen muss, wie man einen Debugger implementiert, der so etwas leistet, bin ich der Sache heute auf den Grund gegangen und kann nun berichten, worum es geht.

Zunächst musste ich eine Hürde aus dem Weg räumen: Der Historical Debugger ist tatsächlich nur im Visual Studio Team System 2010 verfügbar, das „normale“ Visual Studio ohne TS, das ich mir zuerst testweise installiert hatte, enthält ihn nicht. Man findet die VS2010-TS-Preview als ISO-Download hier.

Der Witz an der Sache ist der folgende: Der Historical Debugger schreibt selbständig zu bestimmten Zeitpunkten während der Programmausführung ein Log mit Informationen über den Zustand des Programms zum jeweiligen Zeitpunkt. Der angepriesene „Schritt zurück in der Zeit“ besteht darin, nach einem Stopp des Programms aus der Debug History, d.h. der Liste der Zeitpunkte, einen Eintrag auszuwählen und sich im Debugger den Zustand des Programms anzuschauen, wie er damals war.

Das kann natürlich – im Prinzip – ganz nützlich sein: Hat man z.B. einen Breakpoint erreicht und stellt fest, dass eine Variable einen falschen Wert aufweist, muss man nun nicht mehr mit dem Debugging von vorne beginnen und sich vorsichtig an den Zeitpunkt heranpirschen, wo der falsche Wert zum ersten Mal auftaucht, sondern kann bequem und entspannt die Debug History verwenden, um den Zeitpunkt zu finden oder mindestens stark einzugrenzen, wo die Sache aus dem Ruder läuft.

Man kann in 2 Stufen wählen, wie die Zeitpunkte bestimmt werden sollen: Log schreiben entweder bei „wichtigen Systemereignissen“ oder bei solchen Ereignissen plus bei jedem Methoden-Aufruf. Man kann hier erahnen, dass die Sache Potential hat auszuufern, denn Methoden-Aufrufe können ja sehr zahlreich sein.

Ins Log geschrieben werden der Call Stack zum Zeitpunkt und Variablen-Werte zum Zeitpunkt, wobei man bei Objekten nur noch das Resultat von ToString sieht, aber keinen Zugriff mehr bekommt auf die einzelnen Komponenten.

Ein paar Screenshots hierzu findet man im Blog Dariusz quatscht hier.

Meiner Meinung nach ist das ganz hübsch, aber ob es für eine Revolution reicht, ist fraglich. Insbesondere die doch sehr eingeschränkte historische Sicht auf Variablen-Werte enttäuscht. Mir ist schon klar, dass es schlicht unmöglich ist, zu Hunderten von Zeitpunkten den gesamten Heap ins Log zu schreiben, damit ich auch nachträglich noch Objekte mit all ihren Details anschauen kann, aber hey, es hätte ja sein können, dass jemand mit Hilfe einer genialen Idee ein Log implementiert hat, das viele Details mit praktikablem Aufwand speichern kann, z.B. mit einer Heap-Differenzen-Methode…

Was haben andere so gemacht in Sachen Debugger mit Historie? Hier ist von einem experimentellen Java-Debugger die Rede, der vor ein paar Jahren implementiert wurde und der offenbar alle Variablen-Zuweisungen während eines Programmlaufs protokolliert, indem er sich in die Pseudocode-Ausführung der JVM einklinkt. Er nennt sich denn auch treffend Omniscient Debugger („allwissender Debugger“) und erlaubt offenbar die volle Sicht zu einem beliebigen Zeitpunkt.

In einer PDF-Datei, welche den Debugger genauer beschreibt, findet sich die Information, dass Apache Ant etwa 24 Millionen Variablen-Zuweisungen macht, während das Tool seine eigene Kompilation steuert. Das sind eine ganze Menge, aber nicht so viele, dass sie den Ansatz an sich gleich zu Fall bringen.

VMWare hat etwas im Angebot, das die Firma Replay Debugging nennt, beschrieben auf der Website hier. Unterstützt wird Debugging für C/C++, wie man im Artikel hier nachlesen kann. Das ganze funktioniert mit Hilfe der Virtual Machine, wo man natürlich wie bei der JVM eine zentrale Stelle hat, wo man sich einklinken kann, um zu protokollieren, was läuft. Das Debugging, sprich der Blick auf das Log findet statt in einem Visual Studio, das um ein VMWare-Plugin ergänzt wurde.

Anhand der Beschreibung vermute ich, dass komplett protokolliert wird und man so in einzelnen Schritten vorwärts und sogar rückwärts durch das Programm gehen kann. Die Suche nach dem Punkt im Programm, wo eine Datenstruktur „zerstört“ wird, finde man so: Zeitpunkt finden, wo sie falsch ist, dann Programm „rückwärts laufen lassen“ mit Daten-Breakpoint auf der Struktur und einfach auf den Break durch Veränderung warten.

Hier meldet sich natürlich wieder meine Neugier, die mich veranlasst hat, überhaupt erst den Historical Debugger von Microsoft anzuschauen und die sich nun fragt: Wie macht VMWare das? Ich vermute, dass die komplette Abfolge aller CPU-Befehle, welche das Programm ausführte, der zentrale Bestandteil des Logs ist, und dass das „Debugging“ darin besteht, mit Hilfe der Virtual Machine das Programm irgendwie entlang dieser Abfolge zu führen, vielleicht gestützt auf Komplett-Speicherabzüge in gewissen Zeitabständen.

Und ich vermute ebenfalls, dass die Sache derart auf die Ausführung direkter CPU-Anweisungen zentriert ist, dass die Ausdehnung des Prinzips auf C# mit seinem JIT-Compiler eine schwierige bis unmögliche Sache werden dürfte.

Veröffentlicht in Keine Kategorie. Schlagwörter: , . 1 Comment »

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 »