Leistungsunterschiede zwischen Debug- und Release-Builds

Leistungsunterschiede zwischen Debug- und Release-Builds

Der C#-Compiler selbst verändert die ausgegebene IL im Release-Build kaum. Bemerkenswert ist, dass es nicht mehr die NOP-Opcodes ausgibt, mit denen Sie einen Haltepunkt auf einer geschweiften Klammer setzen können. Der große ist der Optimierer, der in den JIT-Compiler integriert ist. Ich weiß, dass es die folgenden Optimierungen vornimmt:

  • Methoden-Inlining. Ein Methodenaufruf wird durch das Einfügen des Codes der Methode ersetzt. Dies ist eine große Sache, es macht Eigenschaftszugriffe im Wesentlichen kostenlos.

  • CPU-Registerzuordnung. Lokale Variablen und Methodenargumente können in einem CPU-Register gespeichert bleiben, ohne jemals (oder seltener) zurück in den Stapelrahmen gespeichert zu werden. Dies ist eine große Sache, die bemerkenswert ist, weil sie das Debuggen von optimiertem Code so schwierig macht. Und die volatile geben Schlüsselwort eine Bedeutung.

  • Eliminierung der Überprüfung des Array-Index. Eine wichtige Optimierung beim Arbeiten mit Arrays (alle .NET-Collection-Klassen verwenden intern ein Array). Wenn der JIT-Compiler überprüfen kann, dass eine Schleife ein Array niemals außerhalb der Grenzen indiziert, wird die Indexprüfung eliminiert. Großer.

  • Loop-Abrollen. Schleifen mit kleinen Körpern werden verbessert, indem der Code bis zu 4 Mal im Körper wiederholt und weniger Schleifen verwendet werden. Reduziert die Verzweigungskosten und verbessert die superskalaren Ausführungsoptionen des Prozessors.

  • Eliminierung von totem Code. Eine Anweisung wie if (false) { /... / } wird vollständig eliminiert. Dies kann durch ständiges Falten und Inlining verursacht werden. In anderen Fällen kann der JIT-Compiler feststellen, dass der Code keine möglichen Nebenwirkungen hat. Diese Optimierung macht Profiling-Code so schwierig.

  • Code-Hebung. Code innerhalb einer Schleife, die nicht von der Schleife betroffen ist, kann aus der Schleife verschoben werden. Der Optimierer eines C-Compilers wird viel mehr Zeit damit verbringen, Möglichkeiten zum Heben zu finden. Es ist jedoch eine teure Optimierung aufgrund der erforderlichen Datenflussanalyse und der Jitter kann sich die Zeit nicht leisten, sodass nur offensichtliche Fälle aufgezogen werden. .NET-Programmierer dazu zwingen, besseren Quellcode zu schreiben und sich hochzuheben.

  • Eliminierung gemeinsamer Unterausdrücke. x =y + 4; z =y + 4; wird z =x; Ziemlich häufig in Anweisungen wie dest[ix+1] =src[ix+1]; zur besseren Lesbarkeit geschrieben, ohne eine Hilfsvariable einzuführen. Keine Notwendigkeit, die Lesbarkeit zu beeinträchtigen.

  • Ständiges Falten. x =1 + 2; wird x =3; Dieses einfache Beispiel wird vom Compiler früh abgefangen, geschieht jedoch zur JIT-Zeit, wenn andere Optimierungen dies ermöglichen.

  • Ausbreitung kopieren. x =ein; y =x; wird zu y =a; Dies hilft dem Registerzuordner, bessere Entscheidungen zu treffen. Es ist eine große Sache im x86-Jitter, weil es nur wenige Register hat, mit denen man arbeiten kann. Die richtige Auswahl ist entscheidend für die Leistung.

Dies sind sehr wichtige Optimierungen, die ein großes machen können großen Unterschied, wenn Sie beispielsweise den Debug-Build Ihrer App profilieren und ihn mit dem Release-Build vergleichen. Das ist aber nur dann wirklich wichtig, wenn sich der Code auf Ihrem kritischen Pfad befindet, die 5 bis 10 % des Codes, die Sie eigentlich schreiben beeinflusst die Leistung Ihres Programms. Der JIT-Optimierer ist nicht schlau genug, um im Voraus zu wissen, was kritisch ist, er kann nur den Drehknopf „auf elf drehen“ für den gesamten Code anwenden.

Das effektive Ergebnis dieser Optimierungen in Bezug auf die Ausführungszeit Ihres Programms wird häufig durch Code beeinflusst, der an anderer Stelle ausgeführt wird. Lesen einer Datei, Ausführen einer Datenbankabfrage usw. Machen Sie die Arbeit des JIT-Optimierers vollständig unsichtbar. Es macht aber nichts aus :)

Der JIT-Optimierer ist ein ziemlich zuverlässiger Code, vor allem, weil er millionenfach getestet wurde. Es ist äußerst selten, dass Probleme in der Release-Build-Version Ihres Programms auftreten. Es passiert jedoch. Sowohl der x64- als auch der x86-Jitter hatten Probleme mit Strukturen. Der x86-Jitter hat Probleme mit der Fließkommakonsistenz und erzeugt subtil unterschiedliche Ergebnisse, wenn die Zwischenwerte einer Fließkommaberechnung in einem FPU-Register mit 80-Bit-Präzision gehalten werden, anstatt beim Flushen in den Speicher abgeschnitten zu werden.


  1. Ja, es gibt viele Leistungsunterschiede, und diese gelten wirklich für Ihren gesamten Code. Debug bewirkt sehr wenig Leistungsoptimierung und der Release-Modus sehr viel;

  2. Nur Code, der auf DEBUG angewiesen ist kann bei einem Release-Build anders funktionieren. Abgesehen davon sollten Sie keine Probleme sehen.

Ein Beispiel für Framework-Code, der von DEBUG abhängt Konstante ist die Debug.Assert() Methode, die das Attribut [Conditional("DEBUG)"] hat definiert. Das heißt, es kommt auch auf die DEBUG an konstant und ist nicht im Release-Build enthalten.


Dies hängt stark von der Art Ihrer Anwendung ab. Wenn Ihre Anwendung UI-lastig ist, werden Sie wahrscheinlich keinen Unterschied bemerken, da die langsamste Komponente, die mit einem modernen Computer verbunden ist, der Benutzer ist. Wenn Sie einige UI-Animationen verwenden, möchten Sie vielleicht testen, ob Sie beim Ausführen im DEBUG-Build eine merkliche Verzögerung wahrnehmen können.

Wenn Sie jedoch viele rechenintensive Berechnungen haben, würden Sie Unterschiede bemerken (könnten bis zu 40 % betragen, wie @Pieter erwähnt hat, obwohl dies von der Art der Berechnungen abhängen würde).

Es ist im Grunde ein Design-Kompromiss. Wenn Sie unter DEBUG-Build veröffentlichen, können Sie, wenn die Benutzer Probleme haben, eine aussagekräftigere Rückverfolgung erhalten und eine viel flexiblere Diagnose durchführen. Durch die Veröffentlichung im DEBUG-Build vermeiden Sie auch, dass der Optimierer obskure Heisenbugs produziert.