Zeitliche Steuerung von Spielen und Multi-Core-Prozessoren

Da Energieverwaltungstechnologien in heutigen Computern immer häufiger verwendet werden, funktioniert die RDTSC-Anweisung möglicherweise nicht mehr wie erwartet, eine häufig verwendete Methode zum Abrufen von CPU-Timings mit hoher Auflösung. In diesem Artikel wird eine genauere und zuverlässigere Lösung zum Abrufen von CPU-Timings mit hoher Auflösung mithilfe der Windows-APIs QueryPerformanceCounter und QueryPerformanceFrequency vorgeschlagen.

Hintergrund

Seit der Einführung des x86 P5-Anweisungssatzes haben viele Spieleentwickler den Lesezeitstempelzähler, die RDTSC-Anweisung, verwendet, um eine zeitintensive Zeitsteuerung mit hoher Auflösung auszuführen. Die Windows-Multimediatimer sind für die Audio- und Videoverarbeitung präzise genug, aber mit Framezeiten von weniger als einem Dutzend Millisekunden verfügen sie nicht über genügend Auflösung, um Deltazeitinformationen bereitzustellen. Viele Spiele verwenden beim Start immer noch einen Multimedia-Timer, um die Häufigkeit der CPU festzulegen, und sie verwenden diesen Frequenzwert, um Ergebnisse von RDTSC zu skalieren, um eine genaue Zeit zu erhalten. Aufgrund der Einschränkungen von RDTSC stellt die Windows-API den richtigen Weg zum Zugriff auf diese Funktionalität über die Routinen QueryPerformanceCounter und QueryPerformanceFrequency zur Verfügung.

Diese Verwendung von RDTSC für das Timing hat folgende grundlegende Probleme:

  • Diskontinuierliche Werte. Bei der direkten Verwendung von RDTSC wird davon ausgegangen, dass der Thread immer auf demselben Prozessor ausgeführt wird. Multiprozessor- und Dual-Core-Systeme garantieren keine Synchronisierung ihrer Zykluszähler zwischen Kernen. Dies wird verschärft, wenn sie mit modernen Energieverwaltungstechnologien kombiniert wird, die verschiedene Kerne zu unterschiedlichen Zeiten im Leerlauf und wiederherstellen, was dazu führt, dass die Kerne in der Regel nicht mehr synchronisiert sind. Für eine Anwendung führt dies in der Regel zu Störungen oder potenziellen Abstürzen, wenn der Thread zwischen den Prozessoren springt und Timingwerte abruft, die zu großen Deltas, negativen Deltas oder angehaltenem Timing führen.
  • Verfügbarkeit dedizierter Hardware. RDTSC sperrt die Zeitsteuerungsinformationen, die die Anwendung an den Zykluszähler des Prozessors anfordert. Viele Jahre war dies die beste Möglichkeit, hochpräzise Zeitinformationen zu erhalten, aber neuere Motherboards enthalten jetzt dedizierte Timing-Geräte, die hochauflösende Timing-Informationen ohne die Nachteile von RDTSC bereitstellen.
  • Variabilität der CPU-Häufigkeit. Häufig wird davon ausgegangen, dass die Häufigkeit der CPU für die Lebensdauer des Programms festgelegt ist. Bei modernen Energieverwaltungstechnologien ist dies jedoch eine falsche Annahme. Obwohl anfänglich auf Laptop-Computer und andere mobile Geräte beschränkt, wird Technologie, die die Häufigkeit der CPU ändert, auf vielen High-End-Desktop-PCs verwendet. Die Deaktivierung der Funktion zur Aufrechterhaltung einer konsistenten Häufigkeit ist für Benutzer in der Regel nicht akzeptabel.

Empfehlungen

Spiele benötigen genaue Zeitinformationen, aber Sie müssen auch Zeitsteuerungscode so implementieren, dass die Probleme im Zusammenhang mit der Verwendung von RDTSC vermieden werden. Wenn Sie zeitauflösend implementieren, führen Sie die folgenden Schritte aus:

  1. Verwenden Sie QueryPerformanceCounter und QueryPerformanceFrequency anstelle von RDTSC. Diese APIs können RDTSC verwenden, sondern stattdessen ein Timing-Gerät auf der Hauptplatine oder einige andere Systemdienste verwenden, die qualitativ hochwertige und hochauflösende Timinginformationen bereitstellen. Obwohl RDTSC viel schneller ist als QueryPerformanceCounter, da es sich bei letzterem um einen API-Aufruf handelt, handelt es sich um eine API, die mehrere hundert Mal pro Frame ohne spürbare Auswirkungen aufgerufen werden kann. (Entwickler sollten jedoch versuchen, ihre Spiele so wenig wie möglich QueryPerformanceCounter aufrufen zu lassen, um Leistungseinbußen zu vermeiden.)

  2. Beim Berechnen von Deltas sollten die Werte eingespannt werden, um sicherzustellen, dass fehler in den Zeitsteuerungswerten keine Abstürze oder instabile zeitbezogene Berechnungen verursachen. Der Spannbereich sollte von 0 (um negative Deltawerte zu verhindern) bis zu einem vernünftigen Wert basierend auf der niedrigsten erwarteten Framerate liegen. Das Clamping ist wahrscheinlich beim Debuggen Ihrer Anwendung nützlich, aber denken Sie daran, wenn Sie eine Leistungsanalyse durchführen oder das Spiel in einem nicht optimierten Modus ausführen.

  3. Berechnen Sie die gesamte Zeitsteuerung in einem einzelnen Thread. Die Berechnung des Timings für mehrere Threads – z. B. mit jedem Thread, der einem bestimmten Prozessor zugeordnet ist – verringert die Leistung von Multikernsystemen erheblich.

  4. Legen Sie mithilfe der Windows-API SetThreadAffinityMask fest, dass dieser einzelne Thread auf einem einzelnen Prozessor verbleibt. In der Regel ist dies der Standard Spielthread. Während QueryPerformanceCounter und QueryPerformanceFrequency in der Regel für mehrere Prozessoren angepasst werden, können Fehler im BIOS oder den Treibern dazu führen, dass diese Routinen unterschiedliche Werte zurückgeben, wenn der Thread von einem Prozessor zu einem anderen wechselt. Daher ist es am besten, den Thread auf einem einzelnen Prozessor zu belassen.

    Alle anderen Threads sollten ausgeführt werden, ohne eigene Timerdaten zu sammeln. Es wird nicht empfohlen, einen Workerthread zum Berechnen des Timings zu verwenden, da dies zu einem Synchronisierungsengpass wird. Stattdessen sollten Arbeitsthreads Zeitstempel aus dem Standard Thread lesen, und da die Arbeitsthreads nur Zeitstempel lesen, müssen keine kritischen Abschnitte verwendet werden.

  5. Rufen Sie QueryPerformanceFrequency nur einmal auf, da sich die Häufigkeit während der Systemausführung nicht ändert.

Anwendungskompatibilität

Viele Entwickler haben über viele Jahre Hinweg Annahmen über das Verhalten von RDTSC getroffen. Daher ist es sehr wahrscheinlich, dass einige vorhandene Anwendungen probleme aufweisen, wenn sie aufgrund der Zeitlichen Implementierung auf einem System mit mehreren Prozessoren oder Kernen ausgeführt werden. Diese Probleme manifestieren sich in der Regel als Spalt- oder Zeitlupenbewegung. Es gibt keine einfache Abhilfe für Anwendungen, die sich der Energieverwaltung nicht bewusst sind, aber es ist ein Shim vorhanden, um zu erzwingen, dass eine Anwendung immer auf einem einzelnen Prozessor in einem Multiprozessorsystem ausgeführt wird.

Um diesen Shim zu erstellen, laden Sie das Microsoft Application Compatibility Toolkit unter Windows-Anwendungskompatibilität herunter.

Erstellen Sie mithilfe des Kompatibilitätsadministrators, teil des Toolkits, eine Datenbank Ihrer Anwendung und der zugehörigen Fehlerbehebungen. Erstellen Sie einen neuen Kompatibilitätsmodus für diese Datenbank, und wählen Sie die Kompatibilitätskorrektur SingleProcAffinity aus, um die Ausführung aller Threads der Anwendung auf einem einzelnen Prozessor/Kern zu erzwingen. Mithilfe des Befehlszeilentools Fixpack.exe (ebenfalls Teil des Toolkits) können Sie diese Datenbank zu Installation, Test und Verteilung in ein installierbares Paket konvertieren.

Anweisungen zur Verwendung des Kompatibilitätsadministrators finden Sie in der Dokumentation zum Toolkit. Syntax für und Beispiele für die Verwendung von Fixpack.exe finden Sie in der Befehlszeilenhilfe.

Kundenorientierte Informationen finden Sie in den folgenden Wissensdatenbank Artikeln von Microsoft Help and Support: