Cache as Cache can: Über Caches und deren Funktionen

Ein Grundlagenartikel zu modernen Prozessoren und was bei ihrer Programmierung zu beachten ist.

520/1040ST und im Mega ST kam noch ein einfacher 68000er Prozessor ohne Cache und sonstigen Schnickschnack zum Einsatz. Jeder Befehl, der abgearbeitet werden muss, macht das Programm langsamer, und auch der Speicher war damals noch recht knapp bemessen, so dass findige Programmierer nicht nur anfingen, Prozessor-Taktzyklen abzuzählen, sondern auch den Programmcode zu modifizieren, um die Aufgabe so schnell und effektiv wie möglich zu lösen.

Jedoch schon mit der Einführung des Mega STE und dem TT traten erste Probleme auf, die nicht nur darauf zurückzuführen waren, dass sehr handwarenah programmiert wurde, sondern auch auf so neue Dinge wie Cache-Speicher.

Sinn und Zweck eines Caches ist es, den schnellen Prozessor von der langsamen Peripherie zu trennen. Je nach Art des Caches können so die zuletzt benötigten bzw. demnächst benötigten Befehle in dem schnellen Cache-Speicher gehalten und dem Prozessor ungebremst zur Verfügung gestellt werden. Dadurch traten jedoch für Programme, die ihren Programmcode zur Laufzeit verändern, die ersten Probleme auf.

An dieser Stelle möchte ich kurz auf die verschiedenen Cache-Systeme eingehen.. Der Mega STE hatte noch einen für Daten und Code gemeinsamen einfach assoziativen Write-Through Cache. Was sich so kompliziert anhört, ist eigentlich recht einfach: Die Cachelogik legt die AdressLeitungen an den sogenannten TagSpeicher an, der dann zurückliefert, ob die Adresse im Cache steht oder nicht. Da der Cache jedoch nicht den kompletten Adressraum umfaßt (sonst bräuchte man ja keinen Hauptspeicher mehr), sondern nur einen Teil, führt es dazu, dass wenn die unteren Adressen übereinstimmen, die oberen jedoch nicht, bei einem einfach assoziativem Cache der alte Wert aus dem Cache verworfen und dafür der neue Wert zwischengespeichert wird.

Ein zweifach assoziativer Cache könnte also zwei Werte speichern, die in den unteren Adressen übereinstimmen, in den oberen jedoch nicht usw. bis hin zum vollständig assoziativen Cache.

Der Prozessor zeigt der Cache-Logik an, ob er einen Daten- oder einen Code-Zugriff (und zusätzlich auch noch, ob er einen Zugriff im User oder Supervisormodus) durchführen möchte. Da der Programmcode und seine Daten unter Umständen an so weit entfernten Stellen im Code stehen, dass jeweils abwechselnde Zugriffe auf Daten und Code sich gegenseitig immer wieder aus dem Cache werfen können, ist ein getrennter Cache für beide nicht gerade unsinnig, führt aber zu Problemen, auf die ich später noch eingehen werde. Daten, die aus dem Hauptspeicher kommen, werden grundsätzlich im Cache abgelegt, sofern es nicht ausdrücklich verboten wird. So ergibt es zum Beispiel keinen Sinn, wenn man die serielle Schnittstelle mit einem Zugriff auf ein MFP-Register überwachen möchte, ob ein Zeichen eingegangen ist, und immer aus dem Cache der alte Wert zurückgeliefert wird. Beim Schreiben sind jedoch unterschiedliche Taktiken möglich.

Zum einen kann man die Daten gleichzeitig in dem Cache und im Hauptspeicher ablegen (Write-Through), somit kann der Inhalt des Caches jederzeit verworfen werden, ohne dass der Inhalt vorher zurückgeschrieben werden muss, hat aber beim Schreiben keinen Geschwindigkeitsvorteil mehr, da der Prozessor nach jedem Schreibzugriff warten muss, bis die Daten auch im Hauptspeicher angekommen sind.

Die andere Möglichkeit ist, die Daten erst nach einer festgelegten Zeit oder wenn die Speicherzelle im Cache benötigt wird, zurückzuschreiben (Copyback).

Speziell beim Mega STE treten jedoch durch den Write-Trough und den für Daten und Code gemeinsamen Cache keinerlei Probleme mit Programmen, die ihren Code selbst modifizieren, auf. Nur Programme, die sich mit ihren Zeitschleifen - z.B. zum Überprüfen eines ROM-Port-Kopierschutzsteckers - darauf verließen, auf einem 8 MHz 68000er zu laufen, fielen auf die Nase.

Anders sieht es beim TT aus. Der TT hat keinen externen, sondern nur einen internen Level 1 (L1) Cache. Dieser Cache ist einfach assoziativ, für Daten und Code getrennt, und jeweils 256 Bytes groß. Der Cache ist intern in 16 Cache-Lines von je 16 Byte (4 Langwörter) organisiert. Je nach Programmierung kann der Cache entweder mit oder ohne Write-Allocation betrieben werden. Mit Write-Allocation werden beim Schreiben die Daten nicht nur im Hauptspeicher, sondern zusätzlich auch im Cache abgelegt; ohne werden sie nur dann im Cache abgelegt, wenn die entsprechende Adresse bereits vorher im Cache vorhanden war.

Mit dem TT kamen dann auch die ersten Probleme mit Programmen, die ihren Programmcode selbst modifizieren. Durch die getrennten Caches von Daten und Code kann es durchaus vorkommen, dass Modifikationen über den Daten-Cache in den Hauptspeicher geschrieben werden, die von dem Prozessor jedoch nicht wahrgenommen werden, weil dieser seine Befehle aus dem Code-Cache bekommt, der sie unter Umständen schon zwischengespeichert hat. Zusätzlich hat sich Motorola damals aus Kompatibilitätsgründen entschieden, zusätzlich zur Unterscheidung von Code und Daten beim Cache-Zugriff auch noch User und Supervisorzugriffe zu unterscheiden, was an dieser Stelle zu zusätzlichen Schwierigkeiten führen kann. Da der Cache beim 68030 jedoch nur jeweils 256 Byte groß ist, sind in der Praxis nur selten die zu erwartenden Auswirkungen zu sehen.

Moderne Rechner

Jedoch seit dem Hades und dem Milan mit ihren 68040/68060 Prozessoren treten diese Probleme verstärkt auf, da z.B. der 68040 bereits über einen jeweils 4 KByte für Daten und Code getrennten, vierfach assoziativen Ll Cache verfügt, der entweder in Write- Trough oder im Copyback Modus betrieben werden kann. Deshalb ist es auf modernen Prozessoren nicht mehr sinnvoll, mit selbst modifizierendem Code zu arbeiten, da man jedesmal nach einer solchen Modifikation den Daten-Cache zurückschreiben und den Code-Cache für ungültig erklären muss, was den Prozessor garantiert ausbremst. Abgesehen davon sind die Befehle zur Cache-Steuerung bereits zwischen 68030 und 68040 verschieden, so dass man zuvor auch noch überprüfen muss, auf welchem Prozessor das Programm läuft.

Darüber hinaus sollte man bedenken, dass jeder Prozessor eine interne Pipeline hat, die ein Befehl von vorn bis hinten durchlaufen muss, um ausgeführt werden zu können. In dieser Pipeline befinden sich nun mehrere Befehle in verschiedenen Stadien der Ausführung, wobei der Prozessor grundsätzlich davon ausgeht, dass der Programmcode linear abgearbeitet wird und sich demzufolge die nachfolgenden Befehle schon mal vorsorglich holt. Wenn nun jedoch ein Sprungbefehl, wie z.B. bei einer Schleife, ausgeführt werden muss, sind alle bis dahin vorsorglich geholten Befehle hinfällig und müssen verworfen werden. Die Pipeline muss dann erneut gefüllt werden, bis der nächste Befehl ausgeführt werden kann. Da die Daten bei einer kleinen Schleife bereits im Cache stehen, geht das recht schnell, jedoch sollte man sich als Programmierer überlegen, ob man eine kleine Schleife mit wenigen Wiederholungen nicht ausrollen sollte, was zwar zu mehr Code führt, aber bei geschwindigkeitssensiblen Teilen des Programms zu einer Geschwindigkeitssteigerung führt.


U. Schneider
Aus: ST-Computer 02 / 1998, Seite 28

Links

Copyright-Bestimmungen: siehe Über diese Seite