Wartezyklen beim ATARI ST

In einigen Zeitschriften konnte man über den ST lesen, daß der 68000-Prozessor (CPU) beim Zugriff auf den Speicher nicht durch WAIT-Zyklen gebremst wird und die Buszyklen mit voller Geschwindigkeit durchgeführt werden. Dies ist aber leider nicht ganz richtig. Es wird zwar nicht jeder Buszyklus mit WAITs verlangsamt, und der eigentliche Zugriff auf den Bus dauert nur vier Taktzyklen, aber bei einigen Buszyklen wird der Zugriff auf den Bus durch zwei WAITs verzögert.

Ursprünglich wollte ich ein kleines Programm zur Messung der Laufzeiten einzelner Befehle schreiben, um die in verschiedenen Handbüchern zum 68000 angegebenen Zeiten zu überprüfen. Denn erstens gaben verschiedene Bücher verschiedene Zeiten für gleiche Befehle an, und zweitens schienen viele Zeiten fehlerhaft und nicht plausibel. Also schrieb ich ein kurzes C-Programm mit Inline-Assembly, das einen Timer startet, dann 50mal den zu testenden Befehl ausführt und den Timer ausliest.

Ein kurzer Test mit Befehlsfolgen, die aus einem bis fünf NOPs bestanden, zeigte, daß das Programm richtig arbeitete. Beim Testen von etwa 15-20 weiteren Befehlen (hauptsächlich MOVE- und CLR-Befehle) stimmten zu meiner Verwunderung die meisten gemessenen Zeiten nicht mehr mit denen in den Dokumentationen überein. Trotzdem überprüfte ich weitere Befehle und stieß auf immer merkwürdigere Ergebnisse, die sich immer schwerer erklären ließen.

Bei den Messungen zeigte sich nämlich, daß Befehlsfolgen, in denen sich nur die Reihenfolge der Befehle unterschied, nicht die gleiche Ausführungszeit brauchen. Beispielsweise braucht die Schleife

loop: nop
      clr.l     D0 
      dbra      D7,loop

nur 20 Taktzyklen pro Durchlauf,

loop: clr.l     D0 
      nop
      dbra      D7,loop

dagegen benötigt 24 Takte.

Zuerst überlegte ich mir verschiedene Modelle, mit denen ich dieses Verhalten der CPU zu erklären versuchte. Aber erstens gefiel mir keines dieser Modelle so richtig, und zweitens konnte keines die gemessenen Zeiten vollständig erklären.

Obwohl es laut Motorola viele Befehle gibt, die in 6,10,14... Takten ausgeführt werden, z.B. clr.l Dn (6 Takte) und dbra (10 bzw. 14 Takte), sind die Ausführungszeiten aller Befehlsfolgen, die das Programm ausgibt, stets Vielfache von vier Taktzyklen.

Das brachte mich auf die Idee, daß die Buszugriffe der CPU möglicherweise mit einem 2-MHz-Takt, also einem Viertel des 8-MHz-Taktes der CPU, synchronisiert werden, und daß daher alle Zeiten Vielfache von vier CPU-Takten sind. Der Grund für solche Wartezyklen ist bei vielen Computern (z.B. auch beim C64), daß das RAM. in dem der Bildschirminhalt gespeichert wird, nicht vom Speicher des Prozessors getrennt ist. Daher müssen sich der Prozessor und der Video-Chip das RAM teilen und können nicht gleichzeitig den Inhalt auslesen oder beschreiben. Aus der Bildfrequenz von 71 Hz im Monochrommodus und 32 kByte Bildschirmspeicher kann man abschätzen, wenn man die Zeit für den horizontalen und vertikalen Strahlrücklauf sowie die schwarzen Ränder des Bildes berücksichtigt, daß der Video-Shifter des ST tatsächlich mit ungefähr 2 MHz auf das RAM zugreifen - und damit die CPU bremsen - muß, um das Bild darzustellen. Also versprach dieser Ansatz eine Erklärung der erwähnten Effekte.

Numeriert man die Takte mit 0 beginnend, so bedeutet das, daß die Buszugriffe der CPU nur bei Taktnummern beginnen können, die ein Vielfaches von vier sind. Will die CPU aber einen Zugriff im Takt 4n+2, n>=0 durchführen, muß sie zwei Takte warten und kann den Bus erst im Takt 4n+4 benutzen.

Auf diese Weise läßt sich dann auch erklären, warum die Ausführungszeiten einiger Befehlsfolgen von der Reihenfolge der Befehle abhängen. Es gibt nämlich Befehle, die nach ihrem letzten Buszyklus noch 2 Takte intern arbeiten (z.B. clr.l Dn). Der darauffolgende Befehl wird also in einem Takt mit der Nummer 4n+2 begonnen. Wenn dieser gleich mit einem Buszyklus anfängt (z.B. nop), muß die CPU erst zwei Takte warten, bevor sie auf den Bus zugreifen darf, weil sie sonst den Video-Shifter beim Bildaufbau stören würde. Wenn aber statt eines nop ein Befehl folgt, der vor seinem ersten Buszugriff zwei Takte verstreichen läßt (z.B. dbra d16 oder bra d16, also Branch-Befehle mit 16-Bit-Distanz). fällt dieser Zugriff auf den Takt 4n+4 (also ein Vielfaches von vier), und die CPU muß keine WAIT-Zyklen einfügen. Das Bild zeigt das Timing noch einmal grafisch auf. Aus Gründen der Übersichtlichkeit habe ich die WAIT-Zyklen immer vordem Buszyklus. in dem sie auftreten. eingezeichnet. In Wirklichkeit werden diese Zyklen in den Buszyklus eingebaut, aber noch vor dem eigentlichen Zugriff auf den Bus.

In Listing 1 ist ein kurzes Programm in Megamax-C angegeben, mit dem einzelne Befehle schnell auf ihr Timing untersucht werden können. Der zu testende Befehl wird in drei verschiedenen Schleifen ausgeführt. In der ersten Schleife folgt der Befehl immer einem nop, beginnt also synchron zum 2-MHz-Takt (also mit einer Taktnummer 4n), denn der nop-Befehl endet sofort mit dem Abschluß des Buszyklus. In der zweiten Schleife steht an Stelle des nop ein clr.l D0, der zwei Takte länger dauert als ein nop. Der zu testende Befehl beginnt also in der Mitte eines 2-MHz-Taktes (Taktnummer 4n+2). Wenn der zu testende Befehl sofort mit einem Buszyklus beginnt, kann dieser in der ersten Schleife sofort ausgeführt werden, während in der zweiten Schleife zwei zusätzliche WAITs eingefügt werden, so daß der Schleifendurchlauf im zweiten Fall vier Takte länger dauert. Bei Testbefehlen, die ihren ersten Buszyklus erst nach zwei Takten beginnen. sind die Ausführungszeiten der beiden Schleifen gleich. In der ersten Schleife müssen nach diesen ersten zwei Takten noch zwei zusätzliche WAITs eingefügt werden, und der Zugriff beginnt im Takt 4n+4. In der zweiten Schleife beginnt der Befehl erst im Takt 4n+2 und arbeitet zuerst zwei Takte intern. Danach kann der Buszyklus sofort im Takt 4n+4 ausgeführt werden.

Entsprechend kann man an der Differenz der Laufzeiten für die erste und die dritte Schleife ablesen. ob die CPU nach dem letzten Buszyklus noch 2,6,10.... oder 0,4,8.... Takte intern arbeitet, bevor der Befehl beendet und der nächste begonnen wird.

Bevor das Assemblerprogramm zur eigentlichen Zeitmessung aufgerufen wird, übergibt das Hauptprogramm in den globalen Variablen ptr und iter einen Zeiger auf einen 8 kByte großen Speicherblock und die Anzahl der Schleifendurchläufe. Der Speicher wird nur gebraucht, falls man Befehle wie move ea,-(A0) oder move ea,(A0)+ testen will, denn irgendwohin muß das Adreßregister ja zeigen. Zuerst zeigt das Adreßregister in die Mitte dieses Speichers und kann dann bei jedem Schleifendurchlauf erhöht oder erniedrigt werden. So wird verhindert. daß es einen zufälligen Wert enthält und beim Schreiben der Rechner abstürzt. Die gemessenen Zeiten werden in dem globalen Array ticks[3] zurückgegeben und in die Anzahl der Takte umgerechnet. Dazu dient die Konstante CLKF. Sie setzt sich zusammen aus den 8 MHz des CPU-Taktes und den 2.4576 MHz. die am MFP 68901 anliegen und vom Vorteiler des Timers noch durch 4 geteilt werden. Für eine leere Schleife, die nur aus dem dbra-Befehl besteht, mißt das Programm 11.98 Takte, die deshalb noch subtrahiert werden. Warum die leere Schleife 12 Takte und nicht, wie es laut 68000-Dokumentation sein müßte. 10 Takte dauert, kann sich nun jeder selbst überlegen. Einige interessante Ergebnisse kann man Tabelle 1 entnehmen.

Will man also die Laufzeit eines Programms auf dem ST berechnen, muß man nicht nur wissen, wieviele Takte jeder einzelne Befehl dauert, sondern auch, wie die Buszyklen innerhalb des Befehls verteilt sind und wo WAITs auftreten. Dabei ist nicht nur interessant, wieviele Takte die CPU vor dem ersten bzw. nach dem letzten Buszugriff eines Befehls nur intern arbeitet. Bei MOVE (A0),d8(A1,A2) braucht die CPU zwischen zwei Buszugriffen zwei Takte, um die zweite, relativ komplizierte Adresse zu berechnen. Diese zwei Takte werden auf dem ST aber aus den genannten Gründen immer auf vier Takte ausgedehnt, gleichgültig welche Befehle vor oder nach dem MOVE stehen.

nop (0,4,0) clr.w Dn (0,4,0) clr.l Dn (0,4,2) scc Dn (0,4,2) falls Bedingung erfüllt scc Dn (0,4,0) falls Bedingung nicht erfüllt bcc d16 (2,8,0) falls Bedingung erfüllt bcc.s d8 (2,8,0) falls Bedingung erfüllt bcc d16 (4,8,0) falls Bedingung nicht erfüllt bcc.s d8 (4,4.0) falls Bedingung nicht erfüllt dbcc Dn,d16 (4,8,0) falls Bedingung erfüllt dbcc Dn,d16 (6,8,0) falls Sprung nicht ausgeführt dbcc Dn,d16 (2,8,0) falls Sprung ausgeführt bsr d16 (2,16,0) rts (0,16,0) asr.w #m,Dn (0,4,2+2m) asr.l #m.Dn (0,4,4+2m) move.x -(An),ea (2,x,0) hängt von ea ab move.x Dn,d8(An,Rm) (2,x,0)

nop (0,4,0)
clr.w Dn (0,4,0)
clr.l Dn (0,4,2)
scc Dn (0,4,2) falls Bedingung erfüllt
scc Dn (0,4,0) falls Bedingung nicht erfüllt
bcc d16 (2,8,0) falls Bedingung erfüllt
bcc.s d8 (2,8,0) falls Bedingung erfüllt
bcc d16 (4,8,0) falls Bedingung nicht erfüllt
bcc.s d8 (4,4,0) falls Bedingung nicht erfüllt
dbcc Dn,d16 (4,8,0) falls Bedingung erfüllt
dbcc Dn,d16 (6,8.0) falls Sprung nicht ausgeführt
dbcc Dn,d16 (2,8,0) falls Sprung ausgeführt
bsr d16 (2,16,0)
rts (0,16,0)
asr.w #m,Dn (0,4,2+2m)
asr.l #m.Dn (0,4.4+2m)
move.x -(An),ea (2,x,0) hängt von ea ab
move.x Dn,d8(An,Rm) (2,x,0)

Tabelle 1: Einige interessante Erkenntnisse über Buszyklen. Die drei Zahlen in den Klammern bedeuten jeweils die Anzahl der Takte vor dem ersten Buszyklus, vom Beginn des ersten bis zum Ende des letzten Buszyklus’ und die Anzahl der Takte nach dem letzten Buszyklus.

Hier sei noch erwähnt, daß alles, was ich über das Timing geschrieben habe, erstmal nur eine Hypothese ist. die nur auf den Ergebnissen des vorgestellten Programms basiert. Leider habe ich kein Oszilloskop, um das Timing genauer zu untersuchen und diese Hypothese zu stützen. Es lassen sich damit jedoch die Ausführungszeiten aller Befehle, die ich bisher getestet habe, so gut erklären, daß sie mit ziemlicher Wahrscheinlichkeit richtig ist.

Für viele Assembler-Programmierer werden diese Eigenschaften des STs keinen Einfluß auf ihre Programme haben. Es gibt jedoch auch unter den 68000-Programmierern Leute (angeblich besonders die Grafikprogrammierer), die jeden Trick an wenden, um eine Mikrosekunde zu sparen. Man kann z.B. ADDA.W #d16,An durch LEA d16(An)An ersetzen und spart dann zwei oder sogar vier Taktzyklen. Solche Geschwindigkeitsfanatiker sollten diesen Artikel also besonders aufmerksam lesen und sich vielleicht mit Hilfe des abgedruckten Programms eine Tabelle anlegen. aus der besonders günstige Befehlsreihenfolgen hervorgehen. Ich selbst halte einen solchen Aufwand allerdings für zu groß, wenn man bedenkt, wie wenig Zeit man mit solchen Tricks sparen kann, und daß sich ein anderer 68000-Rechner schon wieder ganz anders verhält. Die Erfahrung zeigt außerdem, daß sich durch effizientere Algorithmen wesentlich mehr aus einem Rechner herausholen läßt als durch geschickte und trickreiche Programmierung.


#include <stdio.h>
#include <osbind.h>

#define CLKF (8e6/(2.4576e6/4.0))
#define SIZE 8192L

#define INSTR clr.l D0  /* Hier steht der zu testende Befehl */

extern gettim();
char *ptr;              /* Zeiger auf 8kB RAM */
int iter;               /* Anzahl der Schleifendurchläufe */ 
int ticks[3]/           /* gemessene Ausführungszeiten */
                        /* in MFP-Takten (0.6144 MHz) */

main ()
{
    double time[3];     /* Ausführungszeiten in Sekunden */
    int i;

    ptr = (char*)Malloc(SIZE); /* pointer auf 8k RAM */
    iter = 50;          /* 50 Schleifendurchläufe */
    Supexec(gettim);    /* Routine muss im Supervisor-Mode laufen */ 
    printf("Die Anzahl der Takte pro Schleifendurchlauf war\n"); 
    for(i =0; i < 3; i++)
    {   time[i] = ticks[i] * CLKF; /* Zeit in Sekunden berechnen */ 
        printf("in der %d. Schleife: %.01f\n",i,time[i] / iter - 11.98);
    }
    Cnecin();           /* auf Taste warten */
    Mfree(ptr);
}

asm
{
gettim:
        move    SR,D6
        ori     #0x0F00,SR  ; Interrupts sperren
        move.b  #0,0xFA19   ; Timer lnitialisieren

        bsr     getpar
        move.b  #255,0xFA1F ; Startwert 255
        move.b  #1,0xFA19   ; und Countdown starten

loop1:
        nop

        INSTR               ; der zu testende Befehl

        nop
        nop
        dbf     D7,loop1

        move.b  #0,0xFA19   ; Timer anhalten
        move.b  0xFA1F,D1
        move.b  #255,D0 
        sub.b   D1,D0
        and.w   #0xFF,D0
        move.w  D0,ticks(A4)

        bsr     getpar
        move.b  #255,0xFA1F ; Startwert 255
        move.b  #1,0xFA19   ; und Countdown starten

loop2:
        clr.l   D0

        INSTR               ; der zu testende Befehl

        nop
        nop
        dbf     D7,loop2

        move.b  #0,0xFA19   ; Timer anhalten
        move.b  0xFA1F,D1
        move.b  #255,D0
        sub.b   D1,D0
        and.w   #0xFF,D0
        move.w  D0,ticks+2(A4)

        bsr     getpar
        move.b  #255,0xFA1F ; Startwert 255
        move.b  #1,0xFA19   ; und Countdown starten

loop3:
        nop

        INSTR               ; der zu testende Befehl

        bra     lab
        nop
lab:    dbf     D7,loop3

        move.b  #0,0xFA19 ; Timer anhalten
        move.b  0xFA1F,D1
        move.b  D0
        sub.b   D1,D0
        and.w   #0xFF,D0
        move.w  D0,ticks+4(A4)
        move    D6,SR

        rts

getpar:
        move.l  ptr(A4),A0  ; Zeiger auf den 8 kByte-Block nach A0 
        adda    #SIZE/2,A0  ; für move ea,-(An)
        move.l  A0,A1       ; oder move ea,(An)+ Befehle u.ä. 
        move.w  iter(A4),D7 ; Anzahl der Schleifendurchläufe 
        subq.w  #1,D7       ; nach D7
        rts
}

Urs Thürmann
Aus: ST-Computer 11 / 1989, Seite 148

Links

Copyright-Bestimmungen: siehe Über diese Seite