Performance Tips and Tricks in .NET Applications (Tipps und Tricks zur Leistung in .NET-Anwendungen)

 

Emmanuel Schanzer
Microsoft Corporation

August 2001

Zusammenfassung: Dieser Artikel richtet sich an Entwickler, die ihre Anwendungen für eine optimale Leistung in der verwalteten Welt optimieren möchten. Beispielcode, Erklärungen und Entwurfsrichtlinien werden für Datenbank-, Windows Forms- und ASP-Anwendungen sowie sprachspezifische Tipps für Microsoft Visual Basic und Managed C++ behandelt. (25 gedruckte Seiten)

Inhalte

Übersicht
Leistungstipps für alle Anwendungen
Tipps für den Datenbankzugriff
Leistungstipps für ASP.NET Anwendungen
Tipps zum Portieren und Entwickeln in Visual Basic
Tipps zum Portieren und Entwickeln in verwaltetem C++
Zusätzliche Ressourcen
Anhang: Kosten für virtuelle Anrufe und Zuordnungen

Übersicht

Dieses Whitepaper ist als Referenz für Entwickler konzipiert, die Anwendungen für .NET schreiben und nach verschiedenen Möglichkeiten suchen, die Leistung zu verbessern. Wenn Sie ein Entwickler sind, der noch nicht mit .NET vertraut ist, sollten Sie sowohl mit der Plattform als auch mit der Sprache Ihrer Wahl vertraut sein. Dieses Dokument baut streng auf diesem Wissen auf und geht davon aus, dass der Programmierer bereits genug weiß, um das Programm zum Laufen zu bringen. Wenn Sie eine vorhandene Anwendung zu .NET portieren, lohnt es sich, dieses Dokument zu lesen, bevor Sie mit dem Port beginnen. Einige der hier aufgeführten Tipps sind in der Entwurfsphase hilfreich und enthalten Informationen, die Sie kennen sollten, bevor Sie mit dem Port beginnen.

Dieses Papier ist in Segmente unterteilt, wobei Tipps nach Projekt- und Entwicklertyp organisiert sind. Die erste Reihe von Tipps ist ein Muss für das Schreiben in einer beliebigen Sprache und enthält Ratschläge, die Ihnen bei jeder Zielsprache in der Common Language Runtime (CLR) helfen. Es folgt ein verwandter Abschnitt mit ASP-spezifischen Tipps. Der zweite Satz von Tipps ist nach Sprache geordnet und enthält spezifische Tipps zur Verwendung von Managed C++ und Microsoft® Visual Basic®.

Aufgrund von Zeitplanbeschränkungen musste die Laufzeit der Version 1 (v1) zuerst auf die umfassendste Funktionalität abzielen und sich später mit Sonderfalloptimierungen befassen. Dies führt zu einigen Schubladenfällen, in denen die Leistung zu einem Problem wird. Daher behandelt dieses Papier mehrere Tipps, die diesen Fall vermeiden sollen. Diese Tipps sind in der nächsten Version (vNext) nicht relevant, da diese Fälle systematisch identifiziert und optimiert werden. Ich werde auf sie hinweisen, während wir gehen, und es liegt an Ihnen, zu entscheiden, ob es sich lohnt.

Leistungstipps für alle Anwendungen

Es gibt einige Tipps, an die Sie sich erinnern sollten, wenn Sie an der CLR in einer beliebigen Sprache arbeiten. Diese sind für alle relevant und sollten die erste Verteidigungslinie sein, wenn es um Leistungsprobleme geht.

Weniger Ausnahmen auslösen

Das Auslösen von Ausnahmen kann sehr teuer sein. Stellen Sie daher sicher, dass Sie nicht viele davon auslösen. Verwenden Sie Perfmon, um zu ermitteln, wie viele Ausnahmen Ihre Anwendung auslöst. Es kann Sie überraschen, dass bestimmte Bereiche Ihrer Anwendung mehr Ausnahmen auslösen, als Sie erwartet haben. Um eine bessere Granularität zu erzielen, können Sie die Ausnahmenummer auch programmgesteuert mithilfe von Leistungsindikatoren überprüfen.

Das Suchen und Entwerfen von ausnahmeintensivem Code kann zu einem anständigen Perf-Gewinn führen. Beachten Sie, dass dies nichts mit Try/Catch-Blöcken zu tun hat: Die Kosten fallen nur an, wenn die tatsächliche Ausnahme ausgelöst wird. Sie können beliebig viele Try/Catch-Blöcke verwenden. Die unentgeltliche Verwendung von Ausnahmen ist der Grund, an dem Sie an Leistung verlieren. Beispielsweise sollten Sie sich von Dingen wie der Verwendung von Ausnahmen für den Steuerungsfluss fernhalten.

Hier ist ein einfaches Beispiel dafür, wie teuer Ausnahmen sein können: Wir durchlaufen einfach eine For-Schleife , generieren Tausende oder Ausnahmen und beenden dann das Beenden. Versuchen Sie, die Throw-Anweisung auszukommentieren, um den Unterschied in der Geschwindigkeit zu erkennen: Diese Ausnahmen führen zu einem enormen Mehraufwand.

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • Vorsicht! Die Laufzeit kann Ausnahmen selbst auslösen! Beispielsweise löst Response.Redirect() eine ThreadAbort-Ausnahme aus. Auch wenn Sie keine Ausnahmen explizit auslösen, können Sie Funktionen verwenden, die dies tun. Überprüfen Sie Perfmon, um die wirkliche Geschichte zu erhalten, und den Debugger, um die Quelle zu überprüfen.
  • Für Visual Basic-Entwickler: Visual Basic aktiviert standardmäßig die Int-Überprüfung, um sicherzustellen, dass Dinge wie Überlauf und Dividierung nach Null Ausnahmen auslösen. Sie können dies deaktivieren, um die Leistung zu erzielen.
  • Wenn Sie COM verwenden, sollten Sie beachten, dass HRESULTS als Ausnahmen zurückgegeben werden kann. Achten Sie darauf, diese sorgfältig zu verfolgen.

Klobige Anrufe tätigen

Ein chunky-Aufruf ist ein Funktionsaufruf, der mehrere Aufgaben ausführt, z. B. eine Methode, die mehrere Felder eines Objekts initialisiert. Dies ist für chatzige Aufrufe anzuzeigen, die sehr einfache Aufgaben ausführen und mehrere Aufrufe erfordern, um Dinge zu erledigen (z. B. das Festlegen jedes Feld eines Objekts mit einem anderen Aufruf). Es ist wichtig, chunky statt chatzige Aufrufe über Methoden hinweg auszuführen, bei denen der Mehraufwand höher ist als bei einfachen, intra-AppDomain-Methodenaufrufen. P/Invoke-, Interop- und Remoting-Aufrufe tragen alle Mehraufwand, und Sie möchten sie sparsam verwenden. In jedem dieser Fälle sollten Sie versuchen, Ihre Anwendung so zu entwerfen, dass sie nicht auf kleine, häufige Aufrufe angewiesen ist, die so viel Aufwand verursachen.

Ein Übergang erfolgt immer dann, wenn verwalteter Code aus nicht verwaltetem Code aufgerufen wird und umgekehrt. Die Laufzeit macht es für den Programmierer extrem einfach, Interop durchzuführen, aber dies hat einen Leistungspreis. Wenn ein Übergang erfolgt, müssen die folgenden Schritte ausgeführt werden:

  • Durchführen von Daten marshalling
  • Beheben von Anrufkonventionen
  • Schützen von gespeicherten Registern für Angerufene
  • Umschalten des Threadmodus, sodass gc nicht verwaltete Threads nicht blockiert
  • Erstellen eines Ausnahmebehandlungsrahmens für Aufrufe in verwaltetem Code
  • Übernehmen sie die Kontrolle über den Thread (optional)

Um die Übergangszeit zu beschleunigen, versuchen Sie, nach Möglichkeit P/Invoke zu verwenden. Der Mehraufwand beträgt nur 31 Anweisungen plus die Kosten für das Marshalling, wenn Daten marshalling erforderlich ist, und andernfalls nur 8. COM-Interop ist mit bis zu 65 Anweisungen viel teurer.

Daten marshalling ist nicht immer teuer. Primitive Typen erfordern fast überhaupt kein Marshalling, und Klassen mit explizitem Layout sind ebenfalls günstig. Die tatsächliche Verlangsamung tritt während der Datenübersetzung auf, z. B. bei der Textkonvertierung von ASCI in Unicode. Stellen Sie sicher, dass Daten, die über die verwaltete Grenze übergeben werden, nur konvertiert werden, wenn dies erforderlich ist: Es kann sich herausstellen, dass Sie durch die einfache Vereinbarung auf einen bestimmten Datentyp oder ein bestimmtes Format in Ihrem Programm viel Marshallingaufwand reduzieren können.

Die folgenden Typen werden als blittable bezeichnet, was bedeutet, dass sie direkt über die verwaltete/nicht verwaltete Grenze ohne Marshalling kopiert werden können: sbyte, byte, short, ushort, int, uint, long, ulong, float und double. Sie können diese kostenlos übergeben, sowie ValueTypes und eindimensionale Arrays, die blittbare Typen enthalten. Die ausführlichen Details des Marshallings können in der MSDN Library weiter untersucht werden. Ich empfehle, es sorgfältig zu lesen, wenn Sie viel Zeit mit Marshallen verbringen.

Entwerfen mit ValueTypes

Verwenden Sie einfache Strukturen, wenn Sie können und wenn Sie nicht viel Boxen und Unboxing ausführen. Hier sehen Sie ein einfaches Beispiel, um den Geschwindigkeitsunterschied zu veranschaulichen:

using System;

namespace ConsoleApplication{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

Wenn Sie dieses Beispiel ausführen, sehen Sie, dass die Strukturschleife um Größenordnungen schneller ist. Es ist jedoch wichtig, sich vor der Verwendung von ValueTypes zu hüten, wenn Sie sie wie Objekte behandeln. Dies führt zu zusätzlichem Boxing- und Unboxing-Aufwand für Ihr Programm und kann Sie am Ende mehr kosten, als es wäre, wenn Sie an Objekten hängen geblieben wären! Um dies in Aktion zu sehen, ändern Sie den obigen Code, um ein Array von Foos und Balken zu verwenden. Sie werden feststellen, dass die Leistung mehr oder weniger gleich ist.

Kompromisse ValueTypes sind viel weniger flexibel als Objekte und beeinträchtigen die Leistung, wenn sie falsch verwendet werden. Sie müssen sehr vorsichtig sein, wann und wie Sie sie verwenden.

Versuchen Sie, das obige Beispiel zu ändern und die Foos und Balken in Arrays oder Hashtabellen zu speichern. Sie sehen, dass die Geschwindigkeitssteigerung verschwindet, nur mit einem Boxing- und Unboxing-Vorgang.

Sie können nachverfolgen, wie stark Sie boxen und unboxen, indem Sie sich GC-Zuordnungen und -Sammlungen ansehen. Dies kann entweder mithilfe von Perfmon extern oder mit Leistungsindikatoren im Code erfolgen.

Weitere Informationen zu ValueTypes finden Sie unter Leistungsüberlegungen Run-Time Technologien im .NET Framework.

Verwenden von AddRange zum Hinzufügen von Gruppen

Verwenden Sie AddRange , um eine ganze Auflistung hinzuzufügen, anstatt jedes Element in der Auflistung iterativ hinzuzufügen. Fast alle Windows-Steuerelemente und -Sammlungen verfügen über Add- und AddRange-Methoden, und jedes ist für einen anderen Zweck optimiert. Add ist nützlich, um ein einzelnes Element hinzuzufügen, während AddRange zusätzlichen Mehraufwand hat, aber beim Hinzufügen mehrerer Elemente gewinnt. Hier sind nur einige der Klassen aufgeführt, die Add und AddRange unterstützen:

  • StringCollection, TraceCollection usw.
  • HttpWebRequest
  • UserControl
  • ColumnHeader

Kürzen Ihres Arbeitssatzes

Minimieren Sie die Anzahl der Assemblys, die Sie verwenden, um Den Arbeitssatz klein zu halten. Wenn Sie eine gesamte Assembly laden, um nur eine Methode zu verwenden, zahlen Sie enorme Kosten für sehr wenig Nutzen. Überprüfen Sie, ob Sie die Funktionalität dieser Methode duplizieren können, indem Sie Code verwenden, den Sie bereits geladen haben.

Es ist schwierig, den Überblick über Ihr Arbeitsset zu behalten und könnte wahrscheinlich Gegenstand einer ganzen Arbeit sein. Hier sind einige Tipps, die Ihnen helfen können:

  • Verwenden Sie vadump.exe, um Ihren Arbeitssatz nachzuverfolgen. Dies wird in einem weiteren Whitepaper erläutert, das verschiedene Tools für die verwaltete Umgebung behandelt.
  • Sehen Sie sich Perfmon- oder Leistungsindikatoren an. Sie können Ihnen detailliertes Feedback zur Anzahl der geladenen Klassen oder zur Anzahl der Methoden geben, die JITed erhalten. Sie können Leseauslesungen erhalten, wie viel Zeit Sie im Ladeprogramm verbringen oder wie viel Prozent Ihrer Ausführungszeit für Paging aufgewendet wird.

Verwenden von Für Schleifen für Zeichenfolgeniteration – Version 1

In C# können Sie mit dem foreach-Schlüsselwort (keyword) Elemente in einer Liste, einer Zeichenfolge usw. durchlaufen und Vorgänge für jedes Element ausführen. Dies ist ein sehr leistungsfähiges Tool, da es als universeller Enumerator für viele Typen fungiert. Der Kompromiss für diese Generalisierung ist die Geschwindigkeit. Wenn Sie sich stark auf Zeichenfolgeniteration verlassen, sollten Sie stattdessen eine For-Schleife verwenden. Da Zeichenfolgen einfache Zeichenarrays sind, können sie mit viel weniger Aufwand als andere Strukturen gelaufen werden. Das JIT ist (in vielen Fällen) smart genug, um die Abgrenzungsüberprüfung und andere Dinge innerhalb einer For-Schleife zu optimieren, ist aber bei Foreach-Spaziergängen untersagt. Das Endergebnis ist, dass in Version 1 eine For-Schleife für Zeichenfolgen bis zu fünfmal schneller ist als die Verwendung von foreach. Dies wird sich in zukünftigen Versionen ändern, aber für Version 1 ist dies eine eindeutige Möglichkeit, die Leistung zu steigern.

Hier ist eine einfache Testmethode, um den Unterschied in der Geschwindigkeit zu veranschaulichen. Versuchen Sie, sie auszuführen, und entfernen Sie dann die For-Schleife , und heben Sie die Auskommentierung der foreach-Anweisung auf. Auf meinem Computer dauerte die For-Schleife etwa eine Sekunde, mit etwa 3 Sekunden für die foreach-Anweisung .

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

TradeoffsForeach ist viel besser lesbar und wird in Zukunft so schnell wie eine For-Schleife für Sonderfälle wie Zeichenfolgen werden. Es sei denn, die Zeichenfolgenbearbeitung ist ein echtes Leistungsschwein für Sie, der etwas unordentlichere Code lohnt sich möglicherweise nicht.

Verwenden von StringBuilder für komplexe Zeichenfolgenbearbeitung

Wenn eine Zeichenfolge geändert wird, erstellt die Laufzeit eine neue Zeichenfolge und gibt sie zurück, sodass die ursprüngliche Zeichenfolge gesammelt wird. Meistens ist dies eine schnelle und einfache Möglichkeit, dies zu tun, aber wenn eine Zeichenfolge wiederholt geändert wird, beginnt dies eine Belastung für die Leistung zu sein: All diese Zuordnungen werden schließlich teuer. Hier sehen Sie ein einfaches Beispiel für ein Programm, das 50.000 Mal an eine Zeichenfolge anhängt, gefolgt von einem Programm, das ein StringBuilder-Objekt verwendet, um die Zeichenfolge an Ort und Stelle zu ändern. Der StringBuilder-Code ist viel schneller, und wenn Sie ihn ausführen, wird er sofort offensichtlich.

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

Sehen Sie sich Perfmon an, um zu sehen, wie viel Zeit gespart wird, ohne Tausende von Zeichenfolgen zuzuweisen. Sehen Sie sich den Indikator "% time in GC" unter der .NET CLR-Speicherliste an. Sie können auch die Anzahl der gespeicherten Zuordnungen sowie die Sammlungsstatistik nachverfolgen.

Kompromisse= Beim Erstellen eines StringBuilder-Objekts ist sowohl im Zeit- als auch im Arbeitsspeicher ein gewisser Mehraufwand verbunden. Auf einem Computer mit schnellem Arbeitsspeicher lohnt sich ein StringBuilder , wenn Sie etwa fünf Vorgänge ausführen. Als Faustregel würde ich sagen, dass 10 oder mehr Zeichenfolgenvorgänge eine Rechtfertigung für den Mehraufwand auf jedem Computer sind, selbst bei einem langsameren.

Vorkompilieren von Windows Forms-Anwendungen

Methoden werden bei ihrer ersten Verwendung JITed verwendet. Dies bedeutet, dass Sie eine höhere Startstrafe zahlen, wenn Ihre Anwendung während des Startvorgangs viele Methoden aufruft. Windows Forms viele freigegebene Bibliotheken im Betriebssystem verwenden, und der Aufwand beim Starten kann viel höher sein als bei anderen Arten von Anwendungen. Obwohl nicht immer der Fall ist, führt das Vorkompilieren Windows Forms Anwendungen in der Regel zu einem Leistungsgewinn. In anderen Szenarien ist es normalerweise am besten, das JIT zu lassen, aber wenn Sie ein Windows Forms Entwickler sind, sollten Sie einen Blick darauf werfen.

Mit Microsoft können Sie eine Anwendung vorkompilieren, indem Sie aufrufen ngen.exe. Sie können ngen.exe während der Installationszeit oder vor der Verteilung Ihrer Anwendung ausführen. Es ist definitiv sinnvoll, ngen.exe während der Installationszeit auszuführen, da Sie sicherstellen können, dass die Anwendung für den Computer optimiert ist, auf dem sie installiert wird. Wenn Sie ngen.exe ausführen, bevor Sie das Programm versenden, beschränken Sie die Optimierungen auf die auf Ihrem Computer verfügbaren Optimierungen. Um Ihnen eine Vorstellung davon zu geben, wie viel Vorkompilierung helfen kann, habe ich einen informellen Test auf meinem Computer ausgeführt. Im Folgenden finden Sie die kalten Startzeiten für ShowFormComplex, eine winforms-Anwendung mit etwa hundert Steuerelementen.

Codestatus Time
Framework JITed

ShowFormComplex JITed

3,4 Sekunden
Framework vorkompiliert, ShowFormComplex JITed 2,5 Sekunden
Framework vorkompiliert, ShowFormComplex vorkompiliert 2.1 Sek.

Jeder Test wurde nach einem Neustart durchgeführt. Wie Sie sehen, verwenden Windows Forms Anwendungen viele Methoden im Voraus, sodass die Vorkompilierung einen erheblichen Leistungsgewinn erzielen kann.

Verwenden von jagged Arrays – Version 1

Das v1 JIT optimiert zerklüftete Arrays (einfach "Arrays-of-Arrays") effizienter als rechteckige Arrays, und der Unterschied ist deutlich spürbar. Die folgende Tabelle zeigt den Leistungsgewinn, der sich aus der Verwendung von gezackten Arrays anstelle von rechteckigen Arrays in C# und Visual Basic ergibt (höhere Zahlen sind besser):

  C# Visual Basic 7
Zuordnung (gezackt)

Zuordnung (rechteckig)

14.16

8.37

12.24

8,62

Neuronales Netz (gezackt)

Neuronales Netz (rechteckig)

4.48

3.00

4.58

3.13

Numerische Sortierung (gezackt)

Numerische Sortierung (rechteckig)

4.88

2,05

5,07

2,06

Der Zuweisungs-Benchmark ist ein einfacher Zuweisungsalgorithmus, der aus der schritt-für-Schritt-Anleitung unter Quantitative Entscheidungsfindung für Unternehmen (Gordon, Pressman und Cohn; Prentice-Halle; vergriffen). Der neuronale Netztest führt eine Reihe von Mustern über ein kleines neuronales Netzwerk aus, und die numerische Sortierung ist selbsterklärend. Zusammen stellen diese Benchmarks ein gutes Indiz für die reale Leistung dar.

Wie Sie sehen, kann die Verwendung von gezackten Arrays zu ziemlich dramatischen Leistungssteigerungen führen. Die Optimierungen für gezackte Arrays werden zukünftigen Versionen des JIT hinzugefügt, aber für v1 können Sie sich viel Zeit sparen, indem Sie zackige Arrays verwenden.

Beibehalten der E/A-Puffergröße zwischen 4 KB und 8 KB

Für fast jede Anwendung bietet ihnen ein Puffer zwischen 4 KB und 8 KB die maximale Leistung. Für sehr bestimmte Instanzen können Sie möglicherweise eine Verbesserung aus einem größeren Puffer erzielen (z. B. beim Laden großer Bilder einer vorhersagbaren Größe), aber in 99,99 % der Fälle wird nur Arbeitsspeicher verschwendet. Alle Puffer, die von BufferedStream abgeleitet werden, ermöglichen es Ihnen, die Größe auf alles festzulegen, was Sie möchten, aber in den meisten Fällen bieten 4 und 8 die beste Leistung.

Auf der Suche nach asynchronen E/A-Möglichkeiten

In seltenen Fällen können Sie von asynchroner E/A-E/A profitieren. Ein Beispiel könnte das Herunterladen und Dekomprimieren einer Reihe von Dateien sein: Sie können die Bits aus einem Stream lesen, sie auf der CPU decodieren und in einen anderen schreiben. Es ist sehr aufwendig, asynchrone E/A-E/A effektiv zu verwenden, und es kann zu Leistungseinbußen führen, wenn dies nicht richtig erfolgt. Der Vorteil besteht darin, dass asynchrone E/A-Vorgänge bei richtiger Anwendung das Zehnfache der Leistung bieten können.

Ein hervorragendes Beispiel für ein Programm, das asynchrone E/A verwendet , ist in der MSDN Library verfügbar.

  • Beachten Sie, dass es einen geringen Sicherheitsaufwand für asynchrone Aufrufe gibt: Beim Aufrufen eines asynchronen Aufrufs wird der Sicherheitszustand des Aufrufersstapel erfasst und an den Thread übertragen, der die Anforderung tatsächlich ausführt. Dies ist möglicherweise kein Problem, wenn der Rückruf viel Code ausführt oder wenn asynchrone Aufrufe nicht übermäßig verwendet werden.

Tipps für den Datenbankzugriff

Die Philosophie der Optimierung für den Datenbankzugriff besteht darin, nur die funktionalität zu verwenden, die Sie benötigen, und einen "getrennten" Ansatz zu entwerfen: Stellen Sie mehrere Verbindungen nacheinander her, anstatt eine einzelne Verbindung über einen längeren Zeitraum geöffnet zu halten. Sie sollten diese Änderung berücksichtigen und um sie herum entwerfen.

Microsoft empfiehlt eine N-Tier-Strategie für maximale Leistung im Gegensatz zu einer direkten Client-zu-Datenbank-Verbindung. Betrachten Sie dies als Teil Ihrer Designphilosophie, da viele der eingesetzten Technologien optimiert sind, um ein Szenario mit mehreren Ermüdungsvorgängen zu nutzen.

Verwenden des optimalen Managed Provider

Treffen Sie die richtige Wahl des verwalteten Anbieters, anstatt sich auf einen generischen Accessor zu verlassen. Es gibt verwaltete Anbieter, die speziell für viele verschiedene Datenbanken wie SQL (System.Data.SqlClient) geschrieben wurden. Wenn Sie eine generischere Schnittstelle wie System.Data.Odbc verwenden, wenn Sie möglicherweise eine spezialisierte Komponente verwenden, verlieren Sie die Leistung aufgrund der zusätzlichen Dereferenzierungsebene. Die Verwendung des optimalen Anbieters kann auch dazu führen, dass Sie eine andere Sprache sprechen: Der Verwaltete SQL-Client spricht TDS mit einer SQL-Datenbank, was eine dramatische Verbesserung gegenüber dem generischen OleDb-Protokoll bietet.

Wählen Sie datenleser über Dataset aus, wenn Sie können

Verwenden Sie immer dann einen Datenleser, wenn Sie die Daten nicht beibehalten müssen. Dies ermöglicht ein schnelles Lesen der Daten, die bei Bedarf zwischengespeichert werden können. Ein Reader ist einfach ein zustandsloser Datenstrom, mit dem Sie Daten beim Eingang lesen und dann ablegen können, ohne sie in einem Dataset zu speichern, um die Navigation zu erleichtern. Der Streamansatz ist schneller und hat weniger Aufwand, da Sie sofort mit der Verwendung von Daten beginnen können. Sie sollten auswerten, wie oft sie dieselben Daten benötigen, um zu entscheiden, ob das Zwischenspeichern für die Navigation für Sie sinnvoll ist. Hier ist eine kleine Tabelle, die den Unterschied zwischen DataReader und DataSet für ODBC- und SQL-Anbieter beim Pullen von Daten von einem Server veranschaulicht (höhere Zahlen sind besser):

  ADO SQL
Dataset 801 2507
DataReader 1083 4585

Wie Sie sehen können, wird die höchste Leistung erzielt, wenn Sie den optimalen verwalteten Anbieter zusammen mit einem Datenleser verwenden. Wenn Sie Ihre Daten nicht zwischenspeichern müssen, kann die Verwendung eines Datenlesers zu einer enormen Leistungssteigerung führen.

Verwenden von Mscorsvr.dll für MP-Computer

Stellen Sie für eigenständige Anwendungen der mittleren Ebene und server sicher, dass mscorsvr für Computer mit mehreren Prozessoren verwendet wird. Mscorwks ist nicht für Skalierung oder Durchsatz optimiert, während die Serverversion mehrere Optimierungen aufweist, die eine gute Skalierung ermöglichen, wenn mehr als ein Prozessor verfügbar ist.

Verwenden gespeicherter Prozeduren wann immer möglich

Gespeicherte Prozeduren sind hochoptimierte Tools, die bei effektiver Verwendung zu einer hervorragenden Leistung führen. Richten Sie gespeicherte Prozeduren ein, um Einfügungen, Aktualisierungen und Löschvorgänge mit dem Datenadapter zu verarbeiten. Gespeicherte Prozeduren müssen nicht interpretiert, kompiliert oder sogar vom Client übertragen werden und reduzieren sowohl den Netzwerkdatenverkehr als auch den Serveraufwand. Stellen Sie sicher, dass Sie CommandType.StoredProcedure anstelle von CommandType.Text verwenden.

Seien Sie vorsichtig bei dynamischen Verbindungszeichenfolgen

Das Verbindungspooling ist eine nützliche Möglichkeit, Verbindungen für mehrere Anforderungen wiederzuverwenden, anstatt den Mehraufwand für das Öffnen und Schließen einer Verbindung für jede Anforderung zu bezahlen. Dies erfolgt implizit, aber Sie erhalten einen Pool pro eindeutiger Verbindungszeichenfolge. Wenn Sie Verbindungszeichenfolgen dynamisch generieren, stellen Sie sicher, dass die Zeichenfolgen jedes Mal identisch sind, damit das Pooling erfolgt. Beachten Sie außerdem, dass Sie bei einer Delegierung einen Pool pro Benutzer erhalten. Es gibt viele Optionen, die Sie für den Verbindungspool festlegen können, und Sie können die Leistung des Pools mithilfe von Perfmon nachverfolgen, um Dinge wie Antwortzeit, Transaktionen/Sek. nachzuverfolgen.

Deaktivieren von Features, die Sie nicht verwenden

Deaktivieren Sie die automatische Transaktionseintragung, wenn sie nicht benötigt wird. Für die SQL-Managed Provider erfolgt dies über die Verbindungszeichenfolge:

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

Wenn Sie ein Dataset mit dem Datenadapter füllen, rufen Sie keine Primärschlüsselinformationen ab, wenn Sie dies nicht müssen (z. B. legen Sie MissingSchemaAction.Add nicht mit schlüssel fest):

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

Vermeiden automatisch generierter Befehle

Wenn Sie einen Datenadapter verwenden, vermeiden Sie automatisch generierte Befehle. Diese erfordern zusätzliche Fahrten zum Server, um Metadaten abzurufen, und bieten Ihnen eine geringere Interaktionssteuerung. Die Verwendung automatisch generierter Befehle ist zwar praktisch, aber es lohnt sich, dies selbst in leistungskritischen Anwendungen zu tun.

Vorsicht vor ADO Legacy Design

Beachten Sie, dass beim Ausführen eines Befehls oder einer Aufruffüllung auf dem Adapter jeder von Der Abfrage angegebene Datensatz zurückgegeben wird.

Wenn Servercursor unbedingt erforderlich sind, können sie über eine gespeicherte Prozedur in t-sql implementiert werden. Vermeiden Sie nach Möglichkeit, da servercursorbasierte Implementierungen nicht sehr gut skaliert werden.

Implementieren Sie bei Bedarf das Paging auf zustandslose und verbindungslose Weise. Sie können dem Dataset zusätzliche Datensätze hinzufügen, indem Sie:

  • Sicherstellen, dass PK-Informationen vorhanden sind
  • Bedarfsgerechtes Ändern des Select-Befehls des Datenadapters und
  • Calling Fill

Halten Sie Ihre Datasets schlank

Fügen Sie nur die benötigten Datensätze in das Dataset ein. Denken Sie daran, dass das Dataset alle Daten im Arbeitsspeicher speichert und je mehr Daten Sie anfordern, desto länger dauert die Übertragung über das Netzwerk.

Verwenden des sequenziellen Zugriffs so oft wie möglich

Verwenden Sie mit einem Datenleser CommandBehavior.SequentialAccess. Dies ist für den Umgang mit Blobdatentypen von entscheidender Bedeutung, da daten in kleinen Blöcken aus der Leitung gelesen werden können. Während Sie jeweils nur mit einem Teil der Daten arbeiten können, verschwindet die Latenz beim Laden eines großen Datentyps. Wenn Sie nicht das gesamte Objekt gleichzeitig bearbeiten müssen, erhalten Sie mit sequenziellem Zugriff eine viel bessere Leistung.

Leistungstipps für ASP.NET-Anwendungen

Aggressives Cache

Wenn Sie eine App mit ASP.NET entwerfen, stellen Sie sicher, dass Sie die Zwischenspeicherung im Auge behalten. Bei Serverversionen des Betriebssystems haben Sie viele Möglichkeiten, die Verwendung von Caches auf Server- und Clientseite zu optimieren. Es gibt mehrere Features und Tools in ASP, die Sie zur Leistungssteigerung nutzen können.

Ausgabezwischenspeicherung: Speichert das statische Ergebnis einer ASP-Anforderung. Mithilfe der <@% OutputCache %> -Direktive angegeben:

  • Dauer: Das Element ist im Cache vorhanden.
  • VaryByParam– Variiert Cacheeinträge nach Get/Post-Parametern
  • VaryByHeader – Variiert Cacheeinträge nach HTTP-Header
  • VaryByCustom – Variiert Cacheeinträge nach Browser
  • Überschreiben Sie, um je nach Gewünschtem zu variieren:
    • Fragmentzwischenspeicherung: Wenn es nicht möglich ist, eine gesamte Seite (Datenschutz, Personalisierung, dynamische Inhalte) zu speichern, können Sie Fragmentzwischenspeicherung verwenden, um Teile davon zu speichern, um später schneller abzurufen.

      a) VaryByControl– Variiert die zwischengespeicherten Elemente nach Werten eines Steuerelements.

    • Cache-API: Bietet eine äußerst feine Granularität für die Zwischenspeicherung, indem eine Hashtabelle von zwischengespeicherten Objekten im Arbeitsspeicher beibehalten wird (System.web.UI.caching). Außerdem:

      a) Enthält Abhängigkeiten (Schlüssel, Datei, Zeit)

      b) ungenutzte Elemente laufen automatisch ab.

      c) Unterstützt Rückrufe

Intelligentes Zwischenspeichern kann Ihnen eine hervorragende Leistung bieten, und es ist wichtig, darüber nachzudenken, welche Art von Zwischenspeicherung Sie benötigen. Stellen Sie sich eine komplexe E-Commerce-Website mit mehreren statischen Seiten für die Anmeldung und dann einer Reihe dynamisch generierter Seiten mit Bildern und Text vor. Sie können die Ausgabezwischenspeicherung für diese Anmeldeseiten und dann die Fragmentzwischenspeicherung für die dynamischen Seiten verwenden. Eine Symbolleiste kann beispielsweise als Fragment zwischengespeichert werden. Für eine noch bessere Leistung können Sie häufig verwendete Bilder und Textbausteine, die häufig auf der Website angezeigt werden, mithilfe der Cache-API zwischenspeichern. Ausführliche Informationen zum Zwischenspeichern (mit Beispielcode) finden Sie auf der ASP. NET-Website .

Verwenden Sie den Sitzungszustand nur, wenn Sie

Ein äußerst leistungsstarkes Feature von ASP.NET ist die Möglichkeit, den Sitzungszustand für Benutzer zu speichern, z. B. einen Einkaufswagen auf einer E-Commerce-Website oder einen Browserverlauf. Da dies standardmäßig aktiviert ist, zahlen Sie die Kosten im Arbeitsspeicher, auch wenn Sie ihn nicht verwenden. Wenn Sie den Sitzungszustand nicht verwenden, deaktivieren Sie ihn, und sparen Sie sich den Mehraufwand, indem Sie @% EnabledSessionState = false %> zu Ihrem asp hinzufügen<. Dies umfasst mehrere andere Optionen, die auf der ASP. NET-Website erläutert werden.

Für Seiten, die nur den Sitzungszustand lesen, können Sie EnabledSessionState=readonly auswählen. Dies verursacht weniger Aufwand als den vollständigen Lese-/Schreibsitzungszustand und ist nützlich, wenn Sie nur einen Teil der Funktionalität benötigen und nicht für die Schreibfunktionen bezahlen möchten.

Verwenden Sie den Ansichtszustand nur, wenn Sie dies benötigen.

Ein Beispiel für den Ansichtszustand kann ein langes Formular sein, das Benutzer ausfüllen müssen: Wenn sie im Browser auf Zurück klicken und dann zurückkehren, bleibt das Formular ausgefüllt. Wenn diese Funktionalität nicht verwendet wird, verbraucht dieser Zustand Arbeitsspeicher und Leistung. Die wahrscheinlich größte Leistungseinbuße ist hier, dass jedes Mal, wenn die Seite geladen wird, ein Roundtripsignal über das Netzwerk gesendet werden muss, um den Cache zu aktualisieren und zu überprüfen. Da sie standardmäßig aktiviert ist, müssen Sie angeben, dass Sie den Ansichtszustand nicht mit <@% EnabledViewState = false %>verwenden möchten. Weitere Informationen zum Ansichtsstatus finden Sie auf der ASP. NET-Website , um mehr über die anderen Optionen und Einstellungen zu erfahren, auf die Sie Zugriff haben.

Vermeiden von STA COM

Apartment COM ist für threading in nicht verwalteten Umgebungen konzipiert. Es gibt zwei Arten von Apartment-COM: Singlethread und Multithreaded. MTA COM ist für die Verarbeitung von Multithreading konzipiert, während STA COM auf das Messagingsystem angewiesen ist, um Threadanforderungen zu serialisieren. Die verwaltete Welt ist free-threaded, und die Verwendung von Single Threaded Apartment COM erfordert, dass alle nicht verwalteten Threads im Wesentlichen einen einzelnen Thread für die Interoperabilität gemeinsam nutzen. Dies führt zu einem massiven Leistungstreffer, der nach Möglichkeit vermieden werden sollte. Wenn Sie das Apartment-COM-Objekt nicht in die verwaltete Welt portieren können, verwenden Sie <@%AspCompat = "true" %> für Seiten, die sie verwenden. Eine ausführlichere Erläuterung von STA COM finden Sie in der MSDN Library.

Batchkompilierung

Kompilieren Sie immer im Batch, bevor Sie eine große Seite im Web bereitstellen. Dies kann initiiert werden, indem eine Anforderung an eine Seite pro Verzeichnis ausgeführt wird und gewartet wird, bis der CPU-Leerlauf erneut auftritt. Dadurch wird verhindert, dass der Webserver mit Kompilierungen verzettelt wird, während gleichzeitig versucht wird, Seiten bereitzustellen.

Entfernen unnötiger HTTP-Module

Entfernen Sie abhängig von den verwendeten Features nicht verwendete oder unnötige HTTP-Module aus der Pipeline. Die Rückgewinnung des zusätzlichen Arbeitsspeichers und der verschwendeten Zyklen kann Ihnen einen kleinen Geschwindigkeitsschub bieten.

Vermeiden des Features "Autoeventwireup"

Anstatt sich auf autoeventwireup zu verlassen, überschreiben Sie die Ereignisse aus Page. Anstatt beispielsweise eine Page_Load()- Methode zu schreiben, versuchen Sie, die public void OnLoad() -Methode zu überladen. Dies ermöglicht die Laufzeit von der Erstellung eines CreateDelegate() für jede Seite.

Codieren mit ASCII, wenn Sie UTF nicht benötigen

Standardmäßig ist ASP.NET so konfiguriert, dass Anforderungen und Antworten als UTF-8 codiert werden. Wenn ASCII vollständig für Ihre Anwendung erforderlich ist, können Sie durch den Wegfallen des UTF-Mehraufwands einige Zyklen zurückerhalten. Beachten Sie, dass dies nur auf Anwendungsbasis erfolgen kann.

Verwenden der optimalen Authentifizierungsprozedur

Es gibt verschiedene Möglichkeiten, einen Benutzer zu authentifizieren, und einige sind teurer als andere (in Der Reihenfolge der steigenden Kosten: Keine, Windows, Forms, Passport). Stellen Sie sicher, dass Sie das billigste verwenden, das Ihren Anforderungen am besten entspricht.

Tipps zum Portieren und Entwickeln in Visual Basic

Von Microsoft® Visual Basic® 6 zu Microsoft® Visual Basic® 7 hat sich viel geändert, und die Leistungsübersicht hat sich damit geändert. Aufgrund der hinzugefügten Funktionen und Sicherheitseinschränkungen der CLR können einige Funktionen einfach nicht so schnell wie in Visual Basic 6 ausgeführt werden. In der Tat gibt es mehrere Bereiche, in denen Visual Basic 7 von seinem Vorgänger übernommen wird. Glücklicherweise gibt es zwei gute Nachrichten:

  • Die meisten der schlimmsten Verlangsamungen treten bei einmaligen Funktionen auf, z. B. beim erstmaligen Laden eines Steuerelements. Die Kosten sind vorhanden, aber Sie zahlen sie nur einmal.
  • Es gibt viele Bereiche, in denen Visual Basic 7 schneller ist, und diese Bereiche liegen tendenziell in Funktionen, die während der Laufzeit wiederholt werden. Dies bedeutet, dass der Nutzen im Laufe der Zeit wächst und in mehreren Fällen die einmaligen Kosten überwiegen.

Die meisten Leistungsprobleme stammen aus Bereichen, in denen die Laufzeit ein Feature von Visual Basic 6 nicht unterstützt und hinzugefügt werden muss, um das Feature in Visual Basic 7 beizubehalten. Das Arbeiten außerhalb der Laufzeit ist langsamer, was die Verwendung einiger Features erheblich teurer macht. Die helle Seite ist, dass Sie diese Probleme mit ein wenig Aufwand vermeiden können. Es gibt zwei Standard Bereiche, die eine Optimierung der Leistung erfordern, und einige einfache Optimierungen, die Sie hier und da vornehmen können. Zusammengenommen können sie Ihnen helfen, Leistungseinbußen zu umgehen und die Funktionen zu nutzen, die in Visual Basic 7 viel schneller sind.

Fehlerbehandlung

Das erste Problem ist die Fehlerbehandlung. Dies hat sich in Visual Basic 7 stark geändert, und es gibt Leistungsprobleme im Zusammenhang mit der Änderung. Im Wesentlichen ist die für die Implementierung von OnErrorGoto und Resume erforderliche Logik extrem teuer. Ich schlage vor, einen kurzen Blick auf Ihren Code zu werfen und alle Bereiche hervorzuheben, in denen Sie das Err-Objekt oder einen Fehlerbehandlungsmechanismus verwenden. Sehen Sie sich nun jede dieser Instanzen an, und überprüfen Sie, ob Sie sie für die Verwendung von try/catch umschreiben können. Viele Entwickler werden feststellen, dass sie in den meisten dieser Fälle problemlos in try/catch konvertieren können, und sie sollten eine gute Leistungsverbesserung in ihrem Programm sehen. Die Faustregel lautet: "Wenn Sie die Übersetzung leicht sehen können, tun Sie es."

Hier sehen Sie ein Beispiel für ein einfaches Visual Basic-Programm, das On Error Goto im Vergleich zur try/catch-Version verwendet.

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

Die Geschwindigkeitssteigerung ist spürbar. SubWithError() benötigt 244 Millisekunden mit OnErrorGoto und nur 169 Millisekunden mit try/catch. Die zweite Funktion benötigt 179 Millisekunden im Vergleich zu 164 Millisekunden für die optimierte Version.

Verwenden der frühen Bindung

Das zweite Problem befasst sich mit Objekten und Typecasting. Visual Basic 6 leistet viel Arbeit unter der Kulissen, um das Umwandeln von Objekten zu unterstützen, und viele Programmierer sind sich dessen nicht einmal bewusst. In Visual Basic 7 ist dies ein Bereich, aus dem Sie viel Leistung herauspressen können. Verwenden Sie beim Kompilieren die frühe Bindung. Dadurch wird der Compiler angewiesen, einen Typkoersionsvorgang einzufügen, wenn er explizit erwähnt wird. Dies hat zwei Haupteffekte:

  • Seltsame Fehler lassen sich leichter aufspüren.
  • Nicht benötigte Coercions werden eliminiert, was zu erheblichen Leistungsverbesserungen führt.

Wenn Sie ein Objekt verwenden, als ob es einen anderen Typ hätte, wird das Objekt von Visual Basic für Sie umgewandelt, wenn Sie es nicht angeben. Dies ist praktisch, da der Programmierer sich um weniger Code kümmern muss. Der Nachteil ist, dass diese Zwingungen unerwartete Dinge tun können, und der Programmierer hat keine Kontrolle über sie.

Es gibt Fälle, in denen Sie eine späte Bindung verwenden müssen, aber die meiste Zeit, wenn Sie nicht sicher sind, können Sie mit der frühen Bindung davonkommen. Für Visual Basic 6-Programmierer kann dies zunächst etwas umständlich sein, da Sie sich mehr als in der Vergangenheit um Typen kümmern müssen. Dies sollte für neue Programmierer einfach sein, und Personen, die mit Visual Basic 6 vertraut sind, werden es in kürzester Zeit abholen.

Option streng und explizit aktivieren

Wenn Option Strict aktiviert ist, schützen Sie sich vor versehentlicher verspäteter Bindung und erzwingen ein höheres Maß an Codierungsdisziplin. Eine Liste der Einschränkungen für Option Strict finden Sie in der MSDN Library. Der Nachteil ist, dass alle einschränkenden Typkoercionen explizit angegeben werden müssen. Dies an sich kann jedoch andere Abschnitte Ihres Codes entdecken, die mehr Arbeit leisten, als Sie zuvor gedacht hatten, und es kann Ihnen helfen, einige Fehler im Prozess zu stampfen.

Option Explicit ist weniger restriktiv als Option Strict, zwingt Programmierer jedoch trotzdem dazu, mehr Informationen in ihrem Code bereitzustellen. Insbesondere müssen Sie eine Variable deklarieren, bevor Sie sie verwenden. Dadurch wird der Typrückschluss von der Laufzeit in die Kompilierzeit verschoben. Dieser weggefallene Check führt zu einer zusätzlichen Leistung für Sie.

Ich empfehle, dass Sie mit Option Explicit beginnen und dann Option Strict aktivieren. Dies schützt Sie vor einer Flut von Compilerfehlern und ermöglicht Es Ihnen, schrittweise in der strengeren Umgebung zu arbeiten. Wenn beide Optionen verwendet werden, stellen Sie eine maximale Leistung für Ihre Anwendung sicher.

Verwenden des Binären Vergleichs für Text

Verwenden Sie beim Vergleichen von Text anstelle des Textvergleichs den binären Vergleich. Zur Laufzeit ist der Mehraufwand für Binärdateien viel geringer.

Minimieren der Verwendung von Format()

Wenn möglich, verwenden Sie toString() anstelle von format().. In den meisten Fällen bietet es Ihnen die funktionalität, die Sie benötigen, mit viel weniger Aufwand.

Verwenden von Charw

Verwenden Sie charw anstelle von char. Die CLR verwendet intern Unicode, und char muss zur Laufzeit übersetzt werden, wenn es verwendet wird. Dies kann zu einem erheblichen Leistungsverlust führen, und wenn Sie angeben, dass Ihre Zeichen ein vollständiges Wort lang sind (die Verwendung von charw) entfällt diese Konvertierung.

Optimieren von Zuweisungen

Verwenden Sie exp += val anstelle von exp = exp + val. Da exp beliebig komplex sein kann, kann dies zu einer Menge unnötiger Arbeit führen. Dies zwingt den JIT, beide Kopien von exp auszuwerten, was oft nicht benötigt wird. Die erste Anweisung kann wesentlich besser optimiert werden als die zweite, da der JIT die Auswertung des Exp zweimal vermeiden kann.

Vermeiden einer unnötigen Dereferenzierung

Wenn Sie byRef verwenden, übergeben Sie Zeiger anstelle des tatsächlichen Objekts. Oft ist dies sinnvoll (z. B. Nebenwirkungsfunktionen), aber Sie benötigen es nicht immer. Das Übergeben von Zeigern führt zu einer größeren Dereferenzierung, was langsamer ist als der Zugriff auf einen Wert, der sich im Stapel befindet. Wenn Sie den Heap nicht durchlaufen müssen, ist es am besten, ihn zu vermeiden.

Verkettungen in einen Ausdruck einfügen

Wenn Sie mehrere Verkettungen in mehreren Zeilen haben, versuchen Sie, sie alle auf einen Ausdruck zu setzen. Der Compiler kann optimieren, indem er die vorhandene Zeichenfolge ändert, wodurch eine Geschwindigkeits- und Speichersteigerung bereitgestellt wird. Wenn die Anweisungen in mehrere Zeilen aufgeteilt werden, generiert der Visual Basic-Compiler nicht die Microsoft Intermediate Language (MSIL), um eine direkte Verkettung zu ermöglichen. Weitere Informationen finden Sie im weiter oben erläuterten StringBuilder-Beispiel.

Return-Anweisungen einschließen

Visual Basic ermöglicht es einer Funktion, einen Wert zurückzugeben, ohne die return-Anweisung zu verwenden. Obwohl Visual Basic 7 dies unterstützt, ermöglicht die explizite Verwendung von return dem JIT, etwas mehr Optimierungen durchzuführen. Ohne eine return-Anweisung erhält jede Funktion mehrere lokale Variablen im Stapel, um rückgabende Werte ohne die Schlüsselwort (keyword) transparent zu unterstützen. Wenn Sie diese beibehalten, ist die Optimierung für das JIT schwieriger und kann sich auf die Leistung Ihres Codes auswirken. Durchsuchen Sie Ihre Funktionen, und fügen Sie nach Bedarf die Rückgabe ein . Die Semantik des Codes wird dadurch überhaupt nicht geändert, und es kann Ihnen helfen, ihre Anwendung schneller zu gestalten.

Tipps zum Portieren und Entwickeln in verwaltetem C++

Microsoft zielt managed C++ (MC++) auf eine bestimmte Gruppe von Entwicklern ab. MC++ ist nicht das beste Tool für jeden Auftrag. Nachdem Sie dieses Dokument gelesen haben, können Sie entscheiden, dass C++ nicht das beste Tool ist und dass die Kompromisskosten die Vorteile nicht wert sind. Wenn Sie sich über MC++ nicht sicher sind, gibt es viele gute Ressourcen , die Ihnen bei der Entscheidung helfen. Dieser Abschnitt richtet sich an Entwickler, die bereits entschieden haben, dass sie MC++ in irgendeiner Weise verwenden möchten, und möchten mehr über die Leistungsaspekte wissen.

Für C++-Entwickler erfordert das Arbeiten mit verwaltetem C++ mehrere Entscheidungen. Portieren Sie einen alten Code? Wenn ja, möchten Sie die gesamte Sache in den verwalteten Bereich verschieben, oder planen Sie stattdessen die Implementierung eines Wrappers? Ich werde mich auf die Option "port-everything" konzentrieren oder mc++ für die Zwecke dieser Diskussion von Grund auf neu schreiben, da dies die Szenarien sind, in denen der Programmierer einen Leistungsunterschied feststellen wird.

Vorteile der verwalteten Welt

Das leistungsstärkste Feature von Managed C++ ist die Möglichkeit, verwalteten und nicht verwalteten Code auf Ausdrucksebene zu kombinieren und abzugleichen. Keine andere Sprache ermöglicht ihnen dies, und es gibt einige leistungsstarke Vorteile, die Sie bei ordnungsgemäßer Verwendung davon erhalten können. Ich werde später einige Beispiele dafür durchgehen.

Die verwaltete Welt bietet Ihnen auch große Designgewinne, da viele häufige Probleme für Sie erledigt werden. Speicherverwaltung, Threadplanung und Typkoercionen können auf Wunsch der Laufzeit überlassen werden, sodass Sie Ihre Energien auf die Teile des Programms konzentrieren können, die sie benötigen. Mit MC++ können Sie genau auswählen, wie viel Kontrolle Sie behalten möchten.

MC++-Programmierer haben den Luxus, das Microsoft Visual C® 7 (VC7)-Back-End bei der Kompilierung in IL zu verwenden und dann das JIT darüber hinaus zu verwenden. Programmierer, die an die Arbeit mit dem Microsoft C++-Compiler gewöhnt sind, sind mit blitzschnellen Dingen vertraut. Der JIT wurde mit unterschiedlichen Zielen entwickelt und weist unterschiedliche Stärken und Schwächen auf. Der VC7-Compiler, der nicht an die Zeiteinschränkungen des JIT gebunden ist, kann bestimmte Optimierungen durchführen, die der JIT nicht kann, z. B. die Analyse des gesamten Programms, aggressiveres Inlining und Registrierung. Es gibt auch einige Optimierungen, die nur in typsicheren Umgebungen ausgeführt werden können, sodass mehr Platz für die Geschwindigkeit bleibt, als C++ zulässt.

Aufgrund der unterschiedlichen Prioritäten im JIT sind einige Vorgänge schneller als zuvor, während andere langsamer sind. Es gibt Kompromisse, die Sie für Sicherheit und Sprachflexibilität machen, und einige davon sind nicht billig. Glücklicherweise gibt es Dinge, die ein Programmierer tun kann, um die Kosten zu minimieren.

Portierung: Der gesamte C++-Code kann zu MSIL kompiliert werden.

Bevor wir fortfahren, ist es wichtig zu beachten, dass Sie jeden C ++-Code in MSIL kompilieren können. Alles wird funktionieren, aber es gibt keine Garantie für Typsicherheit und Sie zahlen die Marshalling-Strafe, wenn Sie viel Interop tun. Warum ist es hilfreich, MSIL zu kompilieren, wenn Sie keinen der Vorteile erhalten? In Situationen, in denen Sie eine große Codebasis portieren, können Sie ihren Code schrittweise in Teilen portieren. Sie können Ihre Zeit damit verbringen, mehr Code zu portieren, anstatt spezielle Wrapper zu schreiben, um den portierten und noch nicht portierten Code zusammenzukleben, wenn Sie MC++ verwenden, was zu einem großen Gewinn führen kann. Dies macht das Portieren von Anwendungen zu einem sehr sauber Prozess. Weitere Informationen zum Kompilieren von C++ in MSIL finden Sie unter der Compileroption /clr.

Das einfache Kompilieren Ihres C++-Codes in MSIL bietet Ihnen jedoch nicht die Sicherheit oder Flexibilität der verwalteten Welt. Sie müssen in MC++ und in v1 schreiben, was bedeutet, dass einige Features aufgegeben werden. Die folgende Liste wird in der aktuellen Version der CLR nicht unterstützt, kann aber in Zukunft erfolgen. Microsoft hat sich entschieden, zuerst die gängigsten Features zu unterstützen, und musste einige andere kürzen, um ausgeliefert zu werden. Es gibt nichts, was verhindert, dass sie später hinzugefügt werden, aber in der Zwischenzeit müssen Sie darauf verzichten:

  • Mehrfachvererbung
  • Vorlagen
  • Deterministische Finalisierung

Sie können immer mit unsicherem Code zusammenarbeiten, wenn Sie diese Features benötigen, aber Sie zahlen die Leistungseinbußen, wenn Daten hin und her gemarshallt werden. Beachten Sie, dass diese Features nur innerhalb des nicht verwalteten Codes verwendet werden können. Der verwaltete Bereich hat keine Kenntnis von seiner Existenz. Wenn Sie sich entscheiden, Ihren Code zu portieren, überlegen Sie, in welchem Maße Sie diese Features in Ihrem Entwurf nutzen. In einigen Fällen ist die Neugestaltung zu teuer, und Sie sollten bei nicht verwaltetem Code bleiben. Dies ist die erste Entscheidung, die Sie treffen sollten, bevor Sie mit dem Hacken beginnen.

Vorteile von MC++ gegenüber C# oder Visual Basic

Mc++ hat einen nicht verwalteten Hintergrund und behält viele Möglichkeiten, unsicheren Code zu verarbeiten. Die Möglichkeit von MC++, verwalteten und nicht verwalteten Code reibungslos zu kombinieren, bietet dem Entwickler viel Leistung, und Sie können auswählen, wo auf dem Farbverlauf Sie beim Schreiben Ihres Codes sitzen möchten. In einem Extrem können Sie alles in gerades, unverfälschtes C++ schreiben und einfach mit /clr kompilieren. Andererseits können Sie alles als verwaltete Objekte schreiben und die oben genannten Sprachbeschränkungen und Leistungsprobleme behandeln.

Aber die wahre Leistungsfähigkeit von MC++ kommt, wenn Sie irgendwo dazwischen wählen. Mit MC++ können Sie einige der Leistungstreffer optimieren, die mit verwaltetem Code verbunden sind, indem Sie genau steuern, wann unsichere Features verwendet werden sollen. C# verfügt über einige dieser Funktionen im unsicheren Schlüsselwort (keyword), ist aber kein integraler Bestandteil der Sprache und ist weitaus weniger nützlich als MC++. Sehen wir uns einige Beispiele an, die die feinere Granularität zeigen, die in MC++ verfügbar ist, und wir werden über die Situationen sprechen, in denen dies nützlich ist.

Generalisierte Byref-Zeiger

In C# können Sie nur die Adresse eines Elements einer Klasse übernehmen, indem Sie sie an einen ref-Parameter übergeben. In MC++ ist ein byref-Zeiger ein Konstrukt der ersten Klasse. Sie können die Adresse eines Elements in der Mitte eines Arrays verwenden und diese Adresse von einer Funktion zurückgeben:

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

Wir nutzen dieses Feature, um einen Zeiger auf die "Zeichen" in einer System.String über unsere Hilfsroutine zurückzugeben, und wir können sogar Arrays mithilfe dieser Zeiger durchlaufen:

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

Sie können auch eine Linked-List-Durchquerung mit Injektion in MC++ durchführen, indem Sie die Adresse des Felds "weiter" verwenden (was in C# nicht möglich ist):

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

In C# können Sie nicht auf "Kopf" zeigen oder die Adresse des "nächsten" Felds verwenden, sodass Sie einen Sonderfall erstellen, bei dem Sie an der ersten Position einfügen oder wenn "Head" NULL ist. Darüber hinaus müssen Sie im Code immer einen Knoten vorausschauen. Vergleichen Sie dies mit dem, was eine gute C#-Datei erzeugen würde:

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

Benutzerzugriff auf boxed Types

Ein Leistungsproblem, das bei OO-Sprachen häufig auftritt, ist die Zeit, die für das Boxen und Unboxing von Werten aufgewendet wird. MC++ bietet Ihnen viel mehr Kontrolle über dieses Verhalten, sodass Sie nicht dynamisch (oder statisch) unboxen müssen, um auf Werte zuzugreifen. Dies ist eine weitere Leistungssteigerung. Platzieren Sie einfach __box Schlüsselwort (keyword) vor einem Typ, um das geschachtelte Formular darzustellen:

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

In C# müssen Sie in ein "v" auspacken, dann den Wert aktualisieren und wieder in ein Objekt einfügen:

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

STL-Sammlungen im Vergleich zu verwalteten Sammlungen – v1

Die schlechte Nachricht: In C++ war die Verwendung der STL-Sammlungen oft genauso schnell wie das Schreiben dieser Funktionalität per Hand. Die CLR-Frameworks sind sehr schnell, aber sie leiden unter Box- und Unboxing-Problemen: Alles ist ein Objekt, und ohne Vorlagen- oder generische Unterstützung müssen alle Aktionen zur Laufzeit überprüft werden.

Die gute Nachricht: Langfristig können Sie darauf wetten, dass dieses Problem weggeht, wenn Generika zur Laufzeit hinzugefügt werden. Code, den Sie heute bereitstellen, wird die Geschwindigkeitssteigerung ohne Änderungen erfahren. Kurzfristig können Sie statische Umwandlungen verwenden, um die Überprüfung zu verhindern, aber dies ist nicht mehr sicher. Ich empfehle, diese Methode in engem Code zu verwenden, wo die Leistung absolut wichtig ist und Sie zwei oder drei Hotspots identifiziert haben.

Verwenden von verwalteten Stack-Objekten

In C++ geben Sie an, dass ein Objekt vom Stapel oder Heap verwaltet werden soll. Sie können dies weiterhin in MC++ tun, aber es gibt Einschränkungen, die Sie beachten sollten. Die CLR verwendet ValueTypes für alle stapelverwalteten Objekte, und es gibt Einschränkungen hinsichtlich der Möglichkeiten von ValueTypes (z. B. keine Vererbung). Weitere Informationen finden Sie in der MSDN Library.

Eckfall: Achten Sie auf indirekte Aufrufe in verwaltetem Code – v1

In der v1-Laufzeit werden alle indirekten Funktionsaufrufe nativ ausgeführt und erfordern daher einen Übergang in nicht verwalteten Raum. Jeder indirekte Funktionsaufruf kann nur im einheitlichen Modus erfolgen, was bedeutet, dass alle indirekten Aufrufe aus verwaltetem Code einen Übergang von verwaltet zu nicht verwaltetem Code erfordern. Dies ist ein schwerwiegendes Problem, wenn die Tabelle eine verwaltete Funktion zurückgibt, da dann ein zweiter Übergang erfolgen muss, um die Funktion auszuführen. Im Vergleich zu den Kosten für die Ausführung einer einzelnen Anrufanweisung sind die Kosten fünfzig- bis hundertmal langsamer als in C++!

Wenn Sie eine Methode aufrufen, die sich in einer garbage-collection-Klasse befindet, entfernt die Optimierung diese glücklicherweise. Im spezifischen Fall einer regulären C++-Datei, die mithilfe von /clr kompiliert wurde, wird die Methodenrückgabe jedoch als verwaltet betrachtet. Da dies nicht durch die Optimierung entfernt werden kann, werden Sie mit den vollen Kosten für den doppelten Übergang getroffen. Im Folgenden finden Sie ein Beispiel für einen solchen Fall.

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

Es gibt mehrere Möglichkeiten, dies zu vermeiden:

  • Machen Sie die Klasse in eine verwaltete Klasse ("__gc")
  • Entfernen Sie den indirekten Aufruf, wenn möglich.
  • Lassen Sie die Klasse als nicht verwalteten Code kompiliert (verwenden Sie z. B. nicht /clr).

Minimieren von Leistungstreffern – Version 1

Es gibt mehrere Vorgänge oder Features, die in MC++ unter JIT der Version 1 einfach teurer sind. Ich werde sie auflisten und eine Erklärung geben, und dann werden wir darüber sprechen, was Sie dagegen tun können.

  • Abstraktionen: Dies ist ein Bereich, in dem der bullige, langsame C++-Back-End-Compiler stark gegenüber dem JIT gewinnt. Wenn Sie einen int zu Abstraktionszwecken innerhalb einer Klasse umschließen und streng als int darauf zugreifen, kann der C++-Compiler den Mehraufwand des Wrappers auf praktisch nichts reduzieren. Sie können dem Wrapper viele Abstraktionsebenen hinzufügen, ohne die Kosten zu erhöhen. Das JIT kann nicht die erforderliche Zeit in Anspruch nehmen, um diese Kosten zu beseitigen, wodurch tiefe Abstraktionen in MC++ teurer werden.
  • Gleitkommapunkt: Der v1-JIT führt derzeit nicht alle FP-spezifischen Optimierungen durch, die das VC++-Back-End ausführt, wodurch Gleitkommavorgänge derzeit teurer werden.
  • Mehrdimensionale Arrays: Das JIT ist besser in der Behandlung von gezackten Arrays als mit mehrdimensionalen Arrays, daher verwenden Sie stattdessen gezackte Arrays.
  • 64-Bit-Arithmetik: In zukünftigen Versionen werden dem JIT 64-Bit-Optimierungen hinzugefügt.

Was Sie tun können

In jeder Phase der Entwicklung gibt es mehrere Möglichkeiten. Bei MC++ ist die Entwurfsphase vielleicht der wichtigste Bereich, da sie bestimmt, wie viel Arbeit Sie am Ende erledigen und wie viel Leistung Sie im Gegenzug erhalten. Wenn Sie sich zusammensetzen, um eine Anwendung zu schreiben oder zu portieren, sollten Sie folgendes beachten:

  • Identifizieren Sie Bereiche, in denen Sie mehrere Vererbungen, Vorlagen oder deterministische Finalisierung verwenden. Sie müssen diese entfernen oder diesen Teil Ihres Codes im nicht verwalteten Bereich belassen. Denken Sie an die Kosten für die Neugestaltung, und identifizieren Sie Bereiche, die portiert werden können.
  • Suchen Sie Leistungsheißpunkte, z. B. tiefe Abstraktionen oder virtuelle Funktionsaufrufe im verwalteten Bereich. Diese erfordern auch eine Entwurfsentscheidung.
  • Suchen Sie nach Objekten, die als stapelverwaltet angegeben wurden. Stellen Sie sicher, dass sie in ValueTypes konvertiert werden können. Markieren Sie die anderen für die Konvertierung in heap-verwaltete Objekte.

Während der Codierungsphase sollten Sie sich über die teureren Vorgänge und die Optionen im Umgang mit diesen Vorgängen bewusst sein. Eines der schönsten Dinge an MC++ ist, dass Sie alle Leistungsprobleme im Voraus in den Griff bekommen, bevor Sie mit der Programmierung beginnen. Dies ist hilfreich, um die Arbeit später zu analysieren. Es gibt jedoch noch einige Optimierungen, die Sie beim Coden und Debuggen ausführen können.

Bestimmen Sie, in welchen Bereichen Gleitkommaarithmetik, mehrdimensionale Arrays oder Bibliotheksfunktionen stark genutzt werden. Welche dieser Bereiche sind wichtig für die Leistung? Verwenden Sie Profiler, um die Fragmente zu wählen, bei denen der Aufwand Am meisten kostet, und wählen Sie die beste Option aus:

  • Bewahren Sie das gesamte Fragment im nicht verwalteten Raum auf.
  • Verwenden Sie statische Umwandlungen für die Bibliothekszugriffe.
  • Versuchen Sie, das Boxing-/Unboxing-Verhalten zu optimieren (weiter unten erläutert).
  • Programmieren Sie Ihre eigene Struktur.

Arbeiten Sie schließlich daran, die Anzahl der Übergänge, die Sie vornehmen, zu minimieren. Wenn Sie nicht verwalteten Code oder einen Interop-Aufruf in einer Schleife haben, müssen Sie die gesamte Schleife unmanaged machen. Auf diese Weise zahlen Sie die Übergangskosten nur zweimal und nicht für jede Iteration der Schleife.

Zusätzliche Ressourcen

Verwandte Themen zur Leistung im .NET Framework:

Sehen Sie sich zukünftige Artikel an, die derzeit in der Entwicklung sind, einschließlich einer Übersicht über Entwurfs-, Architektur- und Codierungsphilosophien, einer exemplarischen Vorgehensweise der Leistungsanalysetools in der verwalteten Welt und einem Leistungsvergleich von .NET mit anderen heute verfügbaren Unternehmensanwendungen.

Anhang: Kosten für virtuelle Anrufe und Zuordnungen

Anruftyp # Aufrufe/Sek.
Nicht virtueller ValueType-Aufruf 809971805.600
Nicht virtueller Klassenaufruf 268478412.546
Virtueller Anruf der Klasse 109117738.369
Aufruf von ValueType Virtual (Obj-Methode) 3004286.205
Aufruf von ValueType Virtual (überschriebene Obj-Methode) 2917140.844
Load Type by Newing (nicht statisch) 1434.720
Load Type by Newing (Virtuelle Methoden) 1369.863

Hinweis Der Testcomputer ist ein PIII 733Mhz, auf dem Windows 2000 Professional mit Service Pack 2 ausgeführt wird.

In diesem Diagramm werden die Kosten verglichen, die verschiedenen Arten von Methodenaufrufen zugeordnet sind, sowie die Kosten für die Instanziierung eines Typs, der virtuelle Methoden enthält. Je höher die Zahl, desto mehr Aufrufe/Instanziierungen pro Sekunde können ausgeführt werden. Obwohl diese Zahlen sicherlich auf verschiedenen Computern und Konfigurationen variieren werden, bleiben die relativen Kosten für die Durchführung eines Anrufs über einen anderen erheblich.

  • Nicht virtueller ValueType-Aufruf: Dieser Test ruft eine leere nicht-virtuelle Methode auf, die in einem ValueType enthalten ist.
  • Nicht virtueller Klassenaufruf: Dieser Test ruft eine leere nicht-virtuelle Methode auf, die in einer Klasse enthalten ist.
  • Virtueller Klassenaufruf: Dieser Test ruft eine leere virtuelle Methode auf, die in einer Klasse enthalten ist.
  • ValueType Virtual (Obj-Methode)-Aufruf: Dieser Test ruft ToString() (eine virtuelle Methode) für einen ValueType auf, der auf die Standardobjektmethode zurückgreift.
  • ValueType Virtual (Overridden Obj-Methode) Aufruf: Dieser Test ruft ToString() (eine virtuelle Methode) für einen ValueType auf, der den Standardwert überschrieben hat.
  • Load Type by Newing (Static): Dieser Test weist Speicherplatz für eine Klasse mit nur statischen Methoden zu.
  • Load Type by Newing (Virtual Methods): Dieser Test weist Speicherplatz für eine Klasse mit virtuellen Methoden zu.

Eine Schlussfolgerung, die Sie ziehen können, ist, dass Virtuelle Funktionsaufrufe etwa doppelt so teuer sind wie reguläre Aufrufe, wenn Sie eine Methode in einer Klasse aufrufen. Denken Sie daran, dass Anrufe zunächst günstig sind, sodass ich nicht alle virtuellen Anrufe entfernen würde. Sie sollten immer virtuelle Methoden verwenden, wenn dies sinnvoll ist.

  • Das JIT kann keine virtuellen Methoden inline ausführen, sodass Sie eine potenzielle Optimierung verlieren, wenn Sie nicht virtuelle Methoden loswerden.
  • Das Zuweisen von Speicherplatz für ein Objekt mit virtuellen Methoden ist etwas langsamer als die Zuordnung für ein Objekt ohne diese, da zusätzliche Arbeit geleistet werden muss, um Speicherplatz für die virtuellen Tabellen zu finden.

Beachten Sie, dass das Aufrufen einer nicht virtuellen Methode in einem ValueType mehr als dreimal so schnell ist wie in einer Klasse, aber sobald Sie sie als Klasse behandeln, verlieren Sie furchtbar. Dies ist charakteristisch für ValueTypes: Behandeln Sie sie wie Strukturen und leuchten schnell. Behandeln Sie sie wie Klassen, und sie sind schmerzhaft langsam. ToString() ist eine virtuelle Methode. Bevor sie aufgerufen werden kann, muss die Struktur in ein Objekt auf dem Heap konvertiert werden. Anstatt doppelt so langsam zu sein, ist das Aufrufen einer virtuellen Methode für einen ValueType jetzt achtzehnmal so langsam! Die Moral der Geschichte? Behandeln Sie ValueTypes nicht als Klassen.

Wenn Sie Fragen oder Kommentare zu diesem Artikel haben, wenden Sie sich an Claudio Caldato, Programmmanager für .NET Framework Leistungsprobleme.