Share via


Tipps zum Verbessern von zeitkritischem Code

Für das Schreiben von schnellem Code müssen Sie alle Aspekte Ihrer Anwendung und der Interaktion mit dem System verstehen. In diesem Artikel werden Alternativen zu einigen der offensichtlicheren Codierungstechniken vorgeschlagen, mit denen Sie sicherstellen können, dass die zeitkritischen Teile Ihres Codes zufriedenstellend ausgeführt werden.

Zusammenfassend ist für das Verbessern von zeitkritischem Code Folgendes erforderlich:

  • Sie müssen wissen, welche Teile Ihres Programms schnell sein müssen

  • Sie müssen die Größe und Geschwindigkeit Ihres Codes kennen.

  • Sie müssen die Kosten neuer Funktionen kennen.

  • Sie müssen die erforderliche Mindestarbeit kennen, um die Aufgabe zu erledigen.

Sie können den Leistungsmonitor (perfmon.exe) verwenden, um Informationen zur Leistung Ihres Codes zu sammeln.

Abschnitte in diesem Artikel

Cachefehler und Seitenfehler

Fehler bei Cachetreffern, sowohl im internen als auch im externen Cache, und Seitenfehler (das Zugreifen auf sekundären Speicher für Programmanweisungen und Daten) verlangsamen die Leistung eines Programms.

Ein CPU-Cachetreffer kann Ihr Programm 10 bis 20 Uhrzyklen kosten. Ein externer Cachetreffer kann 20 bis 40 Uhrzyklen kosten. Ein Seitenfehler kann eine Million Taktzyklen kosten (vorausgesetzt, ein Prozessor verarbeitet 500 Millionen Anweisungen/Sekunde und eine Zeit von 2 Millisekunden für einen Seitenfehler). Daher ist es im besten Interesse der Programmausführung, Code zu schreiben, der die Anzahl von Fehlern bei Cachetreffern und Seitenfehlern verringert.

Ein Grund für langsame Programme ist, dass diese mehr Seitenfehler oder mehr Cachefehler als erforderlich verursachen. Um dieses Problem zu vermeiden, ist es wichtig, Datenstrukturen mit guter Referenzität zu verwenden, was bedeutet, dass verwandte Dinge zusammengehalten werden. Manchmal erweist sich eine Datenstruktur, die großartig aussieht, aufgrund einer schlechten Positionierung von Verweisen als schrecklich, manchmal ist es umgekehrt. Zwei Beispiele:

  • Dynamisch zugeordnete verknüpfte Listen können die Programmleistung verringern. Wenn Sie nach einem Element suchen oder wenn Sie eine Liste am Ende durchlaufen, kann jeder übersprungene Link den Cache verpassen oder einen Seitenfehler verursachen. Eine Listenimplementierung, die auf einfachen Arrays basiert, kann aufgrund einer besseren Zwischenspeicherung und weniger Seitenfehlern schneller sein. Auch wenn Sie die Tatsache zulassen, dass das Array schwieriger zu wachsen wäre, kann es immer noch schneller sein.

  • Hashtabellen, die dynamisch zugewiesene verknüpfte Listen verwenden, können die Leistung verschlechtern. Durch Erweiterung können Hashtabellen, die dynamisch zugeordnete verknüpftet Listen zum Speichern ihrer Inhalte verwenden, eine wesentlichere schlechtere Leistung zeigen. Tatsächlich ist in der letztendlichen Analyse eine einfache lineare Suche durch ein Array tatsächlich möglicherweise schneller (je nach Umständen). Die Verwendung einer arraybasierten Hashtabelle (sog. "closed hashing") ist eine häufig übersehene Implementierung, die häufig eine überlegene Leistung aufweist.

Sortieren und Suchen

Das Sortieren ist im Vergleich zu vielen typischen Vorgängen an sich zeitaufwendig. Die beste Methode, um eine unnötige Verlangsamung zu vermeiden, besteht darin, das Sortieren in kritischen Zeiten zu vermeiden. Möglicherweise können Sie Folgendes tun:

  • Verzögern der Sortierung bis zu einem Zeitpunkt, der für die Leistung nicht kritisch ist

  • Sortieren der Daten zu einem früheren Zeitpunkt, der für die Leistung nicht kritisch ist

  • Reines Sortieren des Teils der Daten, der wirklich sortiert werden muss

Manchmal können Sie die Liste in sortierter Reihenfolge erstellen. Gehen Sie sorgfältig vor, denn wenn Sie Daten in sortierter Reihenfolge einfügen müssen, ist möglicherweise eine kompliziertere Datenstruktur mit einer schlechten Positionierung von Hinweisen erforderlich, die zu Cachefehlern und Seitenfehlern führen kann. Es gibt keinen Ansatz, der in allen Fällen funktioniert. Probieren Sie mehrere Ansätze aus, und messen Sie die Unterschiede.

Hier sind einige allgemeine Tipps für das Sortieren:

  • Verwenden Sie eine vordefinierte Sortierung, um Fehler zu minimieren.

  • Jede Arbeit, die Sie vorab erledigen können, um die Komplexität der Sortierung zu reduzieren, lohnt sich. Wenn ein einmaliger Durchlauf Ihrer Daten die Vergleiche vereinfacht und die Sortierung von O(n log n) auf O(n) reduziert, werden Sie fast sicher vorauskommen.

  • Denken Sie über die Positionierung der Hinweise des Sortierungsalgorithmus und die Daten nach, mit denen dieser erwartungsgemäß ausgeführt wird.

Es gibt weniger Alternativen für Suchen als für die Sortierung. Wenn die Suche zeitkritisch ist, ist eine binäre oder Hashtabellensuche nahezu immer am besten, aber wie bei der Sortierung müssen Sie die Positionierung im Hinterkopf behalten. Eine lineare Suche in einem kleinen Array kann schneller sein als eine binäre Suche durch eine Datenstruktur mit vielen Zeigern, die Seitenfehler oder Cachefehler verursachen.

MFC- und Klassenbibliotheken

Die Microsoft Foundation Classes (MFC) können das Schreiben von Code erheblich vereinfachen. Wenn Sie zeitkritischen Code schreiben, sollte Ihnen der einigen der Klassen innewohnende Mehraufwand bewusst sein. Untersuchen Sie den MFC-Code, den Ihr zeitkritischer Code verwendet, um zu sehen, ob er Ihren Leistungsanforderungen entspricht. In der folgenden Liste sind die MFC-Klassen und -Funktionen aufgeführt, die Ihnen bewusst sein sollten:

  • CString MFC ruft die C-Laufzeitbibliothek auf, um Speicher dynamisch zuzuweisen CString . CString Im Allgemeinen ist dies genauso effizient wie jede andere dynamisch zugeordnete Zeichenfolge. Wie bei jeder dynamisch zugewiesenen Zeichenfolge besteht der Mehraufwand der dynamischen Zuordnung und Freigabe. Häufig erfüllt ein einfaches char-Array im Stapel denselben Zweck und ist schneller. Verwenden Sie nicht CString, um eine konstante Zeichenfolge zu speichern. Verwenden Sie stattdessen const char *. Jeder Vorgang, den Sie mit einem CString-Objekt durchführen, hat irgendeinen Mehraufwand. Möglicherweise ist es schneller, die Zeichenfolgenfunktionen der Laufzeitbibliothek zu verwenden.

  • CArray Eine CArray bietet Flexibilität, die ein normales Array nicht, aber Ihr Programm benötigt dies möglicherweise nicht. Wenn Sie bestimmte Grenzen für das Array kennen, können Sie stattdessen ein globales festes Array verwenden. Wenn Sie CArray benutzen, verwenden Sie CArray::SetSize, um die Größe festzulegen und die Anzahl der Elemente anzugeben, um die es wachsen kann, wenn eine Neuzuordnung erforderlich ist. Andernfalls kann das Hinzufügen von Elementen dazu führen, dass Ihr Array häufig neu zugeordnet und kopiert wird, was ineffizient ist und den Arbeitsspeicher fragmentieren kann. Wenn Sie ein Element in ein Array einfügen, CArray werden nachfolgende Elemente im Arbeitsspeicher verschoben und möglicherweise das Array vergrößert. Diese Aktionen können zu Cachefehlern und Seitenfehlern führen. Wenn Sie den Code durchsehen, den die MFC verwenden, sehen Sie möglicherweise, dass Sie etwas Spezifischeres für Ihr Szenario schreiben können, um die Leistung zu verbessern. Da CArray eine Vorlage ist, können Sie beispielsweise CArray-Spezialisierungen für bestimmte Typen bereitstellen.

  • CListCList ist eine doubly verknüpfte Liste, sodass die Elementeinfügung an der Kopf-, Schwanz- und an einer bekannten Position (POSITION) in der Liste schnell ist. Für die Suche nach einem Element nach Wert oder Index ist jedoch eine sequenzielle Suche erforderlich, die langsam sein kann, wenn die Liste lang ist. Wenn Ihr Code keine doubly verknüpfte Liste erfordert, sollten Sie die Verwendung CListüberdenken. Die Verwendung einer singly verknüpften Liste spart den Aufwand, einen anderen Zeiger für alle Vorgänge und den Speicher für diesen Zeiger zu aktualisieren. Der zusätzliche Arbeitsspeicher ist nicht groß, aber es ist eine weitere Möglichkeit für Cachefehler oder Seitenfehler.

  • IsKindOf Diese Funktion kann viele Aufrufe generieren und kann in verschiedenen Datenbereichen auf Speicher zugreifen, was zu einer schlechten Referenzlokalität führt. Es ist nützlich für einen Debugbuild (z. B. in einem ASSERT-Aufruf), aber versuchen Sie, die Verwendung in einem Releasebuild zu vermeiden.

  • PreTranslateMessage Verwenden Sie PreTranslateMessage, wenn eine bestimmte Fensterstruktur Tastaturbeschleuniger benötigt oder Sie eine Verarbeitung von Meldungen in das Nachrichtensystem einfügen müssen. PreTranslateMessage ändert MFC-Dispatchmeldungen. Wenn Sie PreTranslateMessage überschreiben, tun Sie das nur auf der erforderlichen Ebene. Beispielsweise ist es nicht erforderlich, außer Kraft zu setzen CMainFrame::PreTranslateMessage , wenn Sie nur an Nachrichten interessiert sind, die zu untergeordneten Elementen einer bestimmten Ansicht wechseln. Überschreiben Sie stattdessen PreTranslateMessage für die Ansichtsklasse.

    Umgehen Sie den normalen Verteilerpfad nicht, PreTranslateMessage indem Sie alle nachrichten verarbeiten, die an ein beliebiges Fenster gesendet werden. Verwenden Sie für diesen Zweck Fensterprozeduren und MFC-Meldungszuordnungen.

  • OnIdle Leerlaufereignisse können manchmal auftreten, die Sie nicht erwarten, z. B. zwischen WM_KEYDOWN und WM_KEYUP Ereignissen. Timer können eine effizientere Methode zum Auslösen Ihres Codes sein. Erzwingen OnIdle Sie nicht, wiederholt aufgerufen zu werden, indem Sie falsche Nachrichten generieren oder immer von einer Außerkraftsetzung OnIdlevon ,, die es Ihrem Thread niemals erlauben würde, in den Ruhezustand zu TRUE versetzen. Auch hier kann ein Timer oder ein separater Thread angemessener sein.

Freigegebene Bibliotheken

Eine Wiederverwendung von Code ist wünschenswert. Wenn Sie jedoch den Code einer anderen Person verwenden möchten, sollten Sie sicherstellen, dass Sie genau wissen, was dies in diesen Fällen tut, in denen die Leistung für Sie von entscheidender Bedeutung ist. Die beste Möglichkeit, es zu verstehen, besteht darin, den Quellcode zu durchlaufen oder mit Tools wie PView oder Leistungsmonitor zu messen.

Heaps

Verwenden Sie mehrere Heaps mit Vorsicht. Mit zusätzlichen mit HeapCreate und HeapAlloc erstellen Heaps können Sie einen relevanten Satz von Zuordnungen verwalten und dann verwerfen. Sichern Sie nicht zu viel Arbeitsspeicher zu. Wenn Sie mehrere Heaps verwenden, achten Sie besonders auf die Menge des Arbeitsspeichers, der anfangs zugesichert wird.

Anstelle von mehreren Heaps können Sie Hilfsfunktionen verwenden, um eine Schnittstelle zwischen Ihrem Code und dem Standardheap bereitzustellen. Hilfsfunktionen vereinfachen benutzerdefinierte Zuordnungsstrategien, die die Leistung Ihrer Anwendung verbessern können. Wenn Sie beispielsweise oft kleine Zuordnungen durchführen, sollten Sie diese Zuordnungen möglicherweise auf einen Teil des Standardheaps lokalisieren. Sie können einen großen Speicherblock zuordnen und dann eine Hilfsfunktion verwenden, um untergeordnete Zuordnungen von diesem Block durchzuführen. Dann verfügen Sie nicht über mehrere Heaps mit nicht verwendetem Arbeitsspeicher, da die Zuordnung aus dem Standard-Heap kommt.

In einigen Fällen kann die Verwendung des Standardheaps jedoch die Positionierung von Verweisen reduzieren. Verwenden Sie den Prozess-Viewer, Spy++ oder den Leistungsmonitor, um die Auswirkung der Verschiebung von Objekten von Heap zu Heap zu messen.

Messen Sie Ihre Heaps, damit Sie jede Zuordnung im Heap erklären können. Verwenden Sie die Debugheaproutinen der C-Runtime, um einen Prüfpunkt und eine Sicherung für Ihren Heap festzulegen. Sie können die Ausgabe in ein Tabellenkalkulationsprogramm wie Microsoft Excel lesen und Pivottables verwenden, um die Ergebnisse anzuzeigen. Beachten Sie die Gesamtanzahl, die Größe und die Verteilung der Zuordnungen. Vergleichen Sie diese Ergebnisse mit der Größe von Arbeitssätzen. Sehen Sie sich auch das Clustering der Objekte verwandter Größe an.

Sie können außerdem die Leistungsindikatoren ansehen, um die Speicherauslastung zu überwachen.

Threads

Für Hintergrundaufgaben ist eine effektive Handhabung von Leerläufen möglicherweise schneller als die Verwendung von Threads. Es ist einfacher, die Positionierung von Hinweisen in einem Programm mit einem einzigen Thread zu verstehen.

Eine gute Faustregel ist, einen Thread nur dann zu verwenden, wenn eine Betriebssystembenachrichtigung, die Sie blockiert haben, am Stamm der Hintergrundarbeit liegt. Threads sind die beste Lösung in einem solchen Fall, da es unpraktisch ist, einen Standard Thread für ein Ereignis zu blockieren.

Threads stellen außerdem Kommunikationsprobleme dar. Sie müssen die Kommunikationsverbindung zwischen Ihren Threads verwalten, mit einer Liste von Meldungen oder indem Sie freigegebenen Speicher zuordnen und verwenden. Für die Verwaltung der Kommunikationsverbindung ist normalerweise eine Synchronisierung erforderlich, um Racebedingungen und Deadlockprobleme zu vermeiden. Diese Komplexität kann leicht zu Fehlern und Leistungsproblemen führen.

Weitere Informationen finden Sie unter Leerlaufschleifenverarbeitung und Multithreading.

Kleines Workingset

Kleinere Workingsets bedeuten eine bessere Positionierung von Verweisen, weniger Seitenfehler und mehr Cachetreffer. Die Verarbeitung von Workingsets ist die engste Metrik, die das Betriebssystem direkt für das Messen der Positionierung von Verweisen bereitstellt.

  • Zum Festlegen der oberen und unteren Grenzwerte des Workingsets verwenden Sie SetProcessWorkingSetSize.

  • Zum Abrufen der oberen und unteren Grenzwerte des Workingsets verwenden Sie GetProcessWorkingSetSize.

  • Zum Anzeigen der Größe des Workingsets verwenden Sie Spy++.

Siehe auch

Codeoptimierung