Im zweiten Teil des Artikels über Simulationen von Computersystemen werde ich weiterhin in einfacher, informeller Form über Computersimulationen sprechen, insbesondere über die plattformübergreifende Simulation, mit der die meisten gewöhnlichen Benutzer am häufigsten konfrontiert sind, sowie über Taktmodelle und Strecken, die in Entwicklerkreisen verbreiteter sind.

Im Ich habe erklärt, was Simulationen im Allgemeinen sind, sowie die Ebenen der Modellierung. Auf dieser Grundlage schlage ich vor, etwas tiefer einzutauchen und über die plattformübergreifende Simulation zu sprechen, darüber, wie man Strecken zusammenstellt, was man damit später macht, sowie über die Takt-Mikroarchitektur-Emulation.
Plattformübergreifender Simulator (full platform simulator) oder „Ein Krieger ist nicht allein auf dem Feld“
Wenn es erforderlich ist, die Funktionsweise eines bestimmten Geräts zu untersuchen, zum Beispiel einer Netzwerkkarte, oder für dieses Gerät eine Firmware oder einen Treiber zu schreiben, kann dieses Gerät separat modelliert werden. Es ist jedoch nicht besonders bequem, es isoliert von der restlichen Infrastruktur zu verwenden. Um den entsprechenden Treiber auszuführen, sind ein zentraler Prozessor, Speicher, Zugriff auf den Bus zur Datenübertragung und weiteres erforderlich. Darüber hinaus braucht der Treiber ein Betriebssystem (OS) und einen Netzwerkstack. Zusätzlich kann ein separater Paketgenerator und ein Server zur Antwortannahme erforderlich sein.
Der plattformübergreifende Simulator schafft eine Umgebung zur Ausführung des vollständigen Softwarestapels, der alles umfasst, beginnend beim BIOS und Bootloader bis hin zum Betriebssystem und verschiedenen seiner Teilsysteme, wie dem Netzwerkstack, Treibern und Anwendungen auf Benutzerebene. Dafür sind in ihm Softwaremodelle der meisten Computergeräte implementiert: Prozessor und Speicher, Festplatte, Eingabegeräte (Tastatur, Maus, Display) sowie die besagte Netzwerkkarte.
Unten finden Sie das Blockdiagramm des Chipsatzes X58 von Intel. In einem vollplattformübergreifenden Computersimulator erfordert die Implementierung der meisten aufgeführten Geräte, einschließlich derjenigen, die sich im IOH (Input/Output Hub) und ICH (Input/Output Controller Hub) befinden, eine detaillierte Darstellung, die im Blockdiagramm nicht zu sehen ist. Allerdings zeigt die Praxis, dass es viele Geräte gibt, die von der Software, die wir ausführen möchten, nicht verwendet werden. Modelle solcher Geräte müssen nicht erstellt werden.

Vollplattformübergreifende Simulatoren werden normalerweise auf der Ebene der Prozessoranweisungen (ISA, siehe ) implementiert. Dies ermöglicht eine relativ schnelle und kostengünstige Erstellung des Simulators. Die ISA-Ebene ist auch vorteilhaft, da sie mehr oder weniger konstant bleibt, im Gegensatz zur API/ABI-Ebene, die sich häufiger ändert. Darüber hinaus ermöglicht die Implementierung auf Anweisungsebene das Ausführen sogenannter unmodifizierter binärer Software, das heißt, die Ausführung 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 Abbild für das Modell im vollplattformübergreifenden Simulator angeben und – voilà! – das Betriebssystem und andere Programme werden im Simulator ohne zusätzliche Maßnahmen geladen.
Die Leistung von Simulatoren

Wie bereits erwähnt, ist der gesamte Simulationsprozess des Systems, also aller seiner Geräte, eine eher langsame Angelegenheit. Wenn man dies auch noch auf einer sehr detaillierten Ebene, wie der Mikroarchitektur oder logischen Ebene, umsetzt, wird die Ausführung extrem langsam. Die Ebene der Anweisungen ist jedoch eine geeignete Wahl und ermöglicht es, dass das Betriebssystem und die Programme mit Geschwindigkeiten ausgeführt werden, die dem Benutzer ein komfortables Interagieren ermöglichen.
Hier ist es angebracht, das Thema der Leistung von Simulatoren zu erwähnen. Diese wird normalerweise in IPS (Instructions per Second) gemessen, genauer gesagt in MIPS (Millionen IPS), also der Anzahl der Prozessoranweisungen, die der Simulator in einer Sekunde ausführt. Gleichzeitig hängt die Geschwindigkeit der Simulation auch von der Leistung des Systems ab, auf dem die Simulation selbst läuft. Daher wäre es möglicherweise richtiger, von einer "Verlangsamung" (slowdown) des Simulators im Vergleich zum ursprünglichen System zu sprechen.
Die am häufigsten auf dem Markt anzutreffenden vollumfänglichen Simulatoren, wie QEMU, VirtualBox oder VmWare Workstation, bieten eine gute Leistung. Für den Benutzer ist es möglicherweise nicht einmal erkennbar, dass die Arbeit in einem Simulator stattfindet. Dies geschieht dank der in den Prozessoren implementierten speziellen Virtualisierungsfunktionen, Algorithmen zur binären Übersetzung und anderer interessanter Dinge. Das alles ist ein Thema für einen separaten Artikel, aber kurz gesagt, die Virtualisierung ist die hardwareseitige Möglichkeit moderner Prozessoren, die es Simulatoren ermöglicht, Anweisungen nicht zu simulieren, sondern sie direkt an den echten Prozessor zur Ausführung weiterzugeben, wenn die Architekturen von Simulator und Prozessor ähnlich sind. Die binäre Übersetzung ist die Umwandlung des Gast-Maschinen-Codes in den Host-Code und die anschließende Ausführung auf dem echten Prozessor. Infolgedessen ist die Simulation nur geringfügig langsamer, etwa 5-10 Mal, und oft arbeitet sie sogar mit derselben Geschwindigkeit wie das echte System. Allerdings beeinflussen viele Faktoren dies. Wenn wir beispielsweise ein System mit mehreren Dutzend Prozessoren simulieren wollen, sinkt die Geschwindigkeit sofort um mehrere Dutzend. Auf der anderen Seite unterstützen Simulatoren wie Simics in den neuesten Versionen Multiprozessor-Host-Hardware und parallelisieren effektiv die simulierten Kerne auf die Kerne des echten Prozessors.
Wenn es um die Geschwindigkeit der Mikroarchitektursimulation geht, ist diese normalerweise um mehrere Größenordnungen langsamer, etwa 1000-10000 Mal, als die Ausführung auf einem normalen Computer ohne Simulation. Implementierungen auf der Ebene der logischen Elemente sind noch langsamer, um mehrere Größenordnungen. Daher werden als Emulatoren auf dieser Ebene FPGAs verwendet, was die Leistung erheblich steigert.
Das folgende Diagramm zeigt die ungefähre Abhängigkeit der Simulationsgeschwindigkeit von der Detaillierung des Modells.

Taktsimulation
Trotz der geringen Ausführungsgeschwindigkeit sind mikroarchitektonische Simulatoren ziemlich verbreitet. Die Modellierung der internen Blöcke eines Prozessors ist notwendig, um die Ausführungszeit jeder Anweisung genau zu simulieren. Hier kann es zu Missverständnissen kommen – denn es scheint, als könnte man einfach die Ausführungszeit für jede Anweisung programmieren. Aber ein solcher Simulator würde sehr ungenau arbeiten, da die Ausführungszeit derselben Anweisung von Aufruf zu Aufruf unterschiedlich sein kann.
Ein einfaches Beispiel ist der Zugriff auf den Speicher. Wenn die angeforderte Speicherzelle im Cache verfügbar ist, ist die Ausführungszeit minimal. Wenn die benötigten Informationen jedoch nicht im Cache sind ("Cache Miss"), verlängert sich die Ausführungszeit der Anweisung erheblich. Daher ist ein Cache-Modell für eine präzise Simulation erforderlich. Doch damit ist es nicht getan. Der Prozessor wartet nicht einfach darauf, die Daten aus dem Speicher zu erhalten, wenn diese nicht im Cache sind. Stattdessen beginnt er, die nächsten Anweisungen auszuführen, wobei er solche auswählt, die nicht vom Ergebnis des Speicherlesens abhängen. Dies wird als "Out of Order Execution" (OOO) bezeichnet, was notwendig ist, um die Leerlaufzeit des Prozessors zu minimieren. Bei der Berechnung der Ausführungszeit von Anweisungen sollte die Modellierung der entsprechenden Prozessorblöcke berücksichtigt werden. Unter diesen Anweisungen, die während der Wartezeit auf das Ergebnis des Speicherlesens ausgeführt werden, kann auch eine bedingte Sprunganweisung auftauchen. Wenn das Ergebnis der Bedingung im Moment unbekannt ist, stoppt der Prozessor nicht die Ausführung, sondern trifft eine "Annahme", führt den entsprechenden Sprung aus und setzt die proaktive Ausführung der Anweisungen ab der Sprungstelle fort. Ein solcher Block, der als Branch Predictor bezeichnet wird, muss ebenfalls im mikroarchitektonischen Simulator implementiert werden.
Das Bild unten zeigt die grundlegenden Blöcke des Prozessors. Es ist nicht notwendig, dies zu wissen; es wird nur gezeigt, um die Komplexität der mikroarchitektonischen Umsetzung zu veranschaulichen.

Die Funktionsweise all dieser Blöcke in einem echten Prozessor wird durch spezielle Taktsignale synchronisiert, was auch in dem Modell so erfolgt. Solch einen mikroarchitektonischen Simulator nennt man taktsynchron (cycle accurate). Seine Hauptaufgabe besteht darin, die Leistung des entwickelten Prozessors genau vorherzusagen und/oder die Ausführungszeit eines bestimmten Programms, beispielsweise eines Benchmarks, zu berechnen. Wenn die Werte unter den notwendigen liegen, müssen die Algorithmen und Blöcke des Prozessors überarbeitet oder das Programm optimiert werden.
Wie oben gezeigt, ist die taktsynchrone Simulation sehr langsam, weshalb sie nur bei der Untersuchung bestimmter Aspekte der Programmabläufe verwendet wird, bei denen es notwendig ist, die tatsächliche Ausführungsgeschwindigkeit von Programmen zu erfahren und die zukünftige Leistung des Geräts zu bewerten, dessen Prototyp modelliert wird.
Für die Simulation der restlichen Programmlaufzeit wird ein funktionaler Simulator verwendet. Wie erfolgt dieses kombinierte Vorgehen in der Realität? Zunächst wird der funktionale Simulator gestartet, auf dem das Betriebssystem und alles Notwendige zum Starten des betreffenden Programms geladen wird. Uns interessiert weder das Betriebssystem selbst noch die Anfangsphasen des Programmstarts, dessen Konfiguration und Ähnliches. Dennoch können wir diese Teile nicht überspringen und direkt zur Programmausführung aus der Mitte fortfahren. 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 durch das taktsynchrone ersetzen und die Ausführung fortsetzen. Der Simulationsmodus, bei dem ausführbarer Code (d.h. normale kompilierte Programmdateien) verwendet wird, wird als Ausführungssimulation (execution driven simulation) bezeichnet. Dies ist die häufigste Form der Simulation. Es gibt auch einen anderen Ansatz – die Simulation basierend auf Spuren (trace driven simulation).
Simulation basierend auf Spuren
Sie besteht aus zwei Schritten. Mithilfe eines funktionalen Simulators oder auf einem realen System wird ein Log der Programmausführung gesammelt und in eine Datei geschrieben. Dieses Log wird als Spur (trace) bezeichnet. Je nachdem, was untersucht wird, kann die Spur ausführbare Instruktionen, Speicheradressen, Portnummern, Informationen über Interrupts umfassen.
Der nächste Schritt ist das "Abspielen" der Spur, bei dem der Taktgeber-Simulator die Spur liest und alle darin aufgezeichneten Anweisungen ausführt. Am Ende erhalten wir die Ausführungszeit dieses Programmabschnitts sowie verschiedene Merkmale dieses Prozesses, wie z.B. den Cache-Trefferanteil.
Ein wichtiges Merkmal der Arbeit mit Spuren ist die Determiniertheit, das heißt, wenn wir die Simulation wie oben beschrieben ausführen, reproduzieren wir wiederholt dieselbe Aktionsfolge. Dies ermöglicht es uns, durch die Veränderung von Modellparametern (Größen von Cache, Puffern und Warteschlangen) und die Nutzung unterschiedlicher interner Algorithmen oder deren Anpassungen zu erkunden, wie ein bestimmter Parameter die Leistung des Systems beeinflusst und welche Variante die besten Ergebnisse liefert. All dies kann mit dem Modell eines Geräteprototyps durchgeführt werden, bevor ein tatsächlicher Hardwareprototyp erstellt wird.
Die Komplexität dieses Ansatzes liegt in der Notwendigkeit, die Anwendung im Voraus auszuführen und die Spur zu sammeln, sowie in der enormen Dateigröße der Spur. Zu den Vorteilen gehört, dass es ausreichend ist, 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 Besonderheiten der plattformübergreifenden Simulation betrachtet, über die Geschwindigkeit der Implementierungen auf verschiedenen Ebenen, den Taktgeber-Simulator und die Spuren gesprochen. Im nächsten Artikel werde ich die grundlegenden Anwendungsszenarien von Simulatoren beschreiben, sowohl für persönliche Zwecke als auch aus der Sicht der Entwicklung in großen Unternehmen.
Quelle: habr.com
