Im zweiten Teil meines Artikels über Computersystem-Simulatoren werde ich weiterhin in einfacher und verständlicher Form über Computersimulatoren sprechen, insbesondere über die plattformübergreifende Simulation, mit der die meisten Nutzer häufig in Kontakt kommen, sowie über Taktmodelle und Strecken, die in Entwicklerkreisen verbreiteter sind.

In Ich habe erklärt, was Simulatoren im Allgemeinen sind, sowie die Ebenen der Modellierung. Auf dieser Grundlage lade ich Sie ein, etwas tiefer einzutauchen und über plattformübergreifende Simulationen zu sprechen, darüber, wie man Strecken erstellt, was man danach damit macht, sowie über die taktische Mikroarchitektur-Emulation.
Der plattformübergreifende Simulator (full platform simulator) oder „Ein Krieger ist keine Armee“
Wenn Sie die Funktionsweise eines bestimmten Geräts, wie einer Netzwerkkarte, untersuchen oder eine Firmware oder einen Treiber dafür schreiben müssen, kann dieses Gerät separat modelliert werden. Es ist jedoch nicht sehr praktisch, es von der restlichen Infrastruktur zu trennen. Um den entsprechenden Treiber zu starten, werden ein Prozessor, Arbeitsspeicher, Zugriff auf den Datenbus und weitere Komponenten benötigt. Darüber hinaus erfordert der Treiber ein Betriebssystem (OS) und einen Netzwerkstack. Zusätzlich könnte ein separater Paketgenerator und ein Server zum Empfang von Antworten erforderlich sein.
Ein vollplattformfähiger Simulator erstellt eine Umgebung, um einen vollständigen Software-Stack auszuführen, der alles von BIOS und Bootloader bis hin zum Betriebssystem selbst und verschiedenen seiner Subsysteme, wie dem Netzwerkstack, Treibern und Anwendungen auf Benutzerebene umfasst. Dazu implementiert er Softwaremodelle der meisten Computergeräte, darunter Prozessor und Arbeitsspeicher, Festplatte, Eingabegeräte (Tastatur, Maus, Bildschirm) sowie die besagte Netzwerkkarte.
Im Folgenden sehen Sie ein Blockdiagramm des x58-Chipsatzes von Intel. In einem vollumfänglichen Computersimulator, der auf diesem Chipsatz basiert, ist die Implementierung der meisten aufgeführten Geräte erforderlich, einschließlich derjenigen, die sich im IOH (Input/Output Hub) und ICH (Input/Output Controller Hub) befinden und nicht detailliert im Blockdiagramm dargestellt sind. Allerdings zeigt die Praxis, dass es nicht wenige Geräte gibt, die von der Software, die wir ausführen möchten, nicht genutzt werden. Modelle solcher Geräte müssen daher nicht erstellt werden.

Vollumfängliche Simulatoren werden meist auf der Ebene der Prozessoranweisungen (ISA, siehe ). Dies ermöglicht es, relativ schnell und kostengünstig den Simulator selbst zu erstellen. Das ISA-Niveau ist zudem vorteilhaft, da es mehr oder weniger konstant bleibt, im Gegensatz zum API/ABI-Niveau, das häufiger ändert. Darüber hinaus erlaubt die Implementierung auf Maschinenbefehlsebene das Ausführen sogenannter unmodifizierter Binärprogramme, also das Ausführen bereits kompilierten Codes ohne Änderungen, genau so wie er auf echter Hardware verwendet wird. Mit anderen Worten, man kann ein Abbild („Dump“) der Festplatte erstellen, es als Image für das Modell in einem vollwertigen Simulator angeben und – voilà! – das Betriebssystem und andere Programme werden im Simulator ohne zusätzliche Maßnahmen geladen.
Leistung von Simulatoren

Wie bereits oben erwähnt, ist der gesamte Simulationsprozess des Systems, also aller seiner Geräte, ein relativ langsames Unterfangen. Wenn man dies sogar auf einer sehr detaillierten Ebene umsetzen möchte, zum Beispiel auf Mikroarchitektur- oder logischer Ebene, wird die Ausführung extrem langsam. Das Instruktionsniveau hingegen ist eine geeignete Wahl und ermöglicht es dem Betriebssystem und den Programmen, mit Geschwindigkeiten zu arbeiten, die für eine komfortable Interaktion mit dem Benutzer ausreichend sind.
Hier ist es passend, das Thema der Leistungsfähigkeit von Simulatoren zu behandeln. Diese wird normalerweise in IPS (instructions per second) gemessen, genauer gesagt in MIPS (millions IPS), also der Anzahl der Anweisungen, die der Prozessor vom Simulator in einer Sekunde ausführt. Gleichzeitig hängt die Simulationsgeschwindigkeit jedoch auch von der Leistungsfähigkeit des Systems ab, auf dem die Simulationssoftware läuft. Daher wäre es vielleicht korrekter, von einer "Verlangsamung" (slowdown) des Simulators im Vergleich zum Originalsystem zu sprechen.
Die am häufigsten auf dem Markt verfügbaren vollständigen Plattform-Simulatoren, wie QEMU, VirtualBox oder VmWare Workstation, bieten eine solide Leistung. Oft bemerkt der Nutzer vielleicht nicht einmal, dass die Arbeit in einem Simulator erfolgt. Dies ist möglich dank einer speziellen Virtualisierungsfunktion, die in modernen Prozessoren implementiert ist, sowie Algorithmen zur binären Übersetzung und weiteren interessanten Aspekten. Das ist alles Stoff für einen eigenen Artikel, aber um es kurz zu machen: Virtualisierung ist eine Hardware-Fähigkeit moderner Prozessoren, die es den Simulatoren ermöglicht, Anweisungen nicht zu simulieren, sondern sie direkt an den echten Prozessor zur Ausführung zu übergeben, vorausgesetzt, die Architekturen des Simulators und des Prozessors stimmen überein. Die binäre Übersetzung ist die Umwandlung des Gast-Maschinencodes in den Host-Code, gefolgt von dessen Ausführung auf dem echten Prozessor. Dadurch ist die Simulation nur wenig langsamer, um den Faktor 5-10, und oft läuft sie sogar mit der gleichen Geschwindigkeit wie das reale System. Dies hängt jedoch von vielen Faktoren ab. Wenn wir beispielsweise ein System mit mehreren Dutzend Prozessoren simulieren möchten, sinkt die Geschwindigkeit sofort um das Mehrfache. Andererseits unterstützen Simulatoren wie Simics in den neuesten Versionen mehrprozessorsysteme und parallelisieren die simulierten Kerne effizient auf die Kerne des echten Prozessors.
Wenn es um die Geschwindigkeit der Mikroarchitektur-Simulation geht, ist diese in der Regel einige Größenordnungen, etwa 1000–10000 Mal, langsamer als die Ausführung auf einem normalen Computer ohne Simulation. Die Implementierungen auf der Ebene logischer Elemente sind noch um mehrere Größenordnungen langsamer. Daher werden FPGAs als Emulatoren auf dieser Ebene verwendet, was die Leistung erheblich steigert.
Das folgende Diagramm zeigt die ungefähre Abhängigkeit der Simulationsgeschwindigkeit vom Detaillierungsgrad des Modells.

Taktsimulation
Trotz der geringen Ausführungsgeschwindigkeit sind Mikroarchitektur-Simulatoren recht verbreitet. Das Modelling der internen Blocks eines Prozessors ist notwendig, um die Ausführungszeit jeder Anweisung präzise zu simulieren. Hier kann Verwirrung auftreten – warum sollte man nicht einfach die Ausführungszeit jeder Anweisung programmieren? Ein solcher Simulator wäre jedoch sehr ungenau, da die Ausführungszeit derselben Anweisung von Aufruf zu Aufruf unterschiedlich sein kann.
Ein einfaches Beispiel – die Anweisung zum Zugriff auf den Speicher. Wenn die angeforderte Speicherzelle im Cache verfügbar ist, ist die Ausführungszeit minimal. Liegt die Information nicht im Cache vor (ein "Cache-Miss"), erhöht sich die Ausführungszeit erheblich. Daher ist für eine genaue Simulation ein Cache-Modell erforderlich. Doch das Cache-Modell allein reicht nicht aus. Der Prozessor wartet nicht einfach auf die Beschaffung der Daten aus dem Speicher, wenn diese nicht im Cache vorhanden sind. Stattdessen beginnt er mit der Ausführung der folgenden Anweisungen und wählt dabei solche aus, die von dem Ergebnis des Speicherzugriffs unabhängig sind. Dies nennt man "Out of Order Execution" (OOO) und ist notwendig, um die Leerlaufzeit des Prozessors zu minimieren. Bei der Berechnung der Ausführungszeiten muss auch die Simulation der entsprechenden Prozessorblöcke berücksichtigt werden. Unter diesen Anweisungen, die ausgeführt werden, während auf das Ergebnis des Speicherzugriffs gewartet wird, kann sich ein bedingter Sprung befinden. Wenn das Ergebnis der Bedingung in diesem Moment unbekannt ist, stoppt der Prozessor die Ausführung nicht, sondern trifft eine "Vermutung", führt den entsprechenden Sprung aus und fährt mit dem Vorab-Auszählen von Anweisungen ab der Sprungstelle fort. Ein solcher Block, der als Branch Predictor bezeichnet wird, muss ebenfalls im Mikroarchitektursimulator implementiert werden.
Das Bild unten zeigt die Hauptblöcke eines Prozessors. Es ist nicht notwendig, diese zu kennen, es dient nur dazu, die Komplexität der mikroarchitektonischen Realisierung zu verdeutlichen.

Die Arbeit all dieser Blöcke in einem realen Prozessor wird durch spezielle Taktsignale synchronisiert, ähnlich geschieht es auch im Modell. Einen solchen mikroarchitektonischen Simulator nennt man taktsynchron (cycle accurate). Der Hauptzweck besteht darin, die Leistung des entstehenden Prozessors genau vorherzusagen und/oder die Ausführungszeit eines bestimmten Programms, wie zum Beispiel eines Benchmarks, zu berechnen. Sollten die Werte unter den erforderlichen liegen, müssen die Algorithmen und Blöcke des Prozessors überarbeitet oder das Programm optimiert werden.
Wie oben gezeigt, ist die taktsynchronisierte Simulation sehr langsam, weshalb sie nur bei der Untersuchung bestimmter Aspekte der Programmentwicklung eingesetzt wird, wenn die tatsächliche Ausführungsgeschwindigkeit und die zukünftige Leistung des zu modellierenden Prototyps ermittelt werden müssen.
Für die Simulation der restlichen Programmlaufzeit wird ein funktionaler Simulator eingesetzt. Wie erfolgt eine solche kombinierte Nutzung in der Praxis? Zuerst wird der funktionale Simulator gestartet, auf dem das Betriebssystem und alles Notwendige zum Starten des untersuchten Programms geladen wird. Uns interessiert dabei weder das Betriebssystem selbst noch die Anfangsphasen des Programmstarts, die Konfiguration usw. Allerdings können wir diese Teile nicht einfach überspringen und direkt zur Ausführung des Programms in der Mitte übergehen. Daher werden all diese vorbereitenden Schritte im funktionalen Simulator durchlaufen. Nachdem das Programm bis zu dem für uns interessanten Punkt ausgeführt wurde, gibt es zwei Möglichkeiten. Man kann das Modell auf eine taktorientierte Simulation umstellen und die Ausführung fortsetzen. Der Simulationsmodus, bei dem der ausführbare Code (d.h. reguläre kompilierte Programmdateien) verwendet wird, wird als ausführungsgesteuerte Simulation (execution driven simulation) bezeichnet. Dies ist die gebräuchlichste Art der Simulation. Es gibt auch einen anderen Ansatz – die Simulation basierend auf Traces (trace driven simulation).
Simulation basierend auf Traces
Der Prozess besteht aus zwei Schritten. Zunächst wird mithilfe eines funktionalen Simulators oder auf einem echten System ein Protokoll der Aktionen des Programms erstellt und in einer Datei gespeichert. Dieses Protokoll wird als Trace bezeichnet. Je nach dem, was untersucht wird, kann der Trace ausführbare Anweisungen, Speicheradressen, Portnummern und Interruptinformationen enthalten.
Der nächste Schritt besteht darin, den Trace "abzuspielen", wobei der taktsynchrone Simulator den Trace liest und alle darin aufgezeichneten Anweisungen ausführt. Am Ende erhalten wir die Ausführungszeit dieses Programmabschnitts sowie verschiedene Kennzahlen zu diesem Prozess, wie beispielsweise die Cache-Trefferquote.
Ein wesentliches Merkmal der Arbeit mit Traces ist die Determiniertheit: Wenn wir die Simulation auf die beschriebene Weise starten, reproduzieren wir immer wieder die gleiche Abfolge von Aktionen. Dadurch können wir, indem wir die Modellparameter (Größe des Caches, der Buffers und der Warteschlangen) ändern und verschiedene interne Algorithmen oder deren Einstellungen verwenden, untersuchen, wie sich jeweils bestimmte Parameter auf die Systemleistung auswirken und welche Variante die besten Ergebnisse liefert. All dies kann mit dem Prototypenmodell durchgeführt werden, bevor ein echtes Hardware-Prototyp erstellt wird.
Die Komplexität dieses Ansatzes liegt in der Notwendigkeit, die Anwendung vorher zu testen und den Trace zu erfassen, sowie in der riesigen Dateigröße des Traces. Ein Vorteil ist, dass es genügt, nur den interessierenden Teil des Geräts oder der Plattform zu modellieren, während die Ausführungssimulation in der Regel ein vollständiges Modell erfordert.
In diesem Artikel haben wir die Merkmale der vollständigen Plattform-Simulation betrachtet, über die Implementierungsgeschwindigkeit auf verschiedenen Ebenen, taktile Simulationen und Strecken gesprochen. Im nächsten Artikel werde ich die Hauptanwendungsfälle von Simulatoren beschreiben, sowohl für private Zwecke als auch aus der Sicht der Entwicklung in großen Unternehmen.
Quelle: habr.com
