Aktuelles Projekt: 59 Funktionen für FPU X87
PENTIUM-OPTIMIERTER ASSEMBLER-KODE
Der Pentium-Prozessor ist sehr wahrscheinlich die letzte Entwicklung
aus der Reihe der x86-Prozessoren, der auf direkte Weise kompatibel
zu seinen Vorgängern ist und sicherlich noch für einen längeren
Zeitraum eine nennenswerte Bedeutung haben wird.
Es lohnt sich daher, zumindest Bibliotheks-Funktionen mit optimal
angepaßtem Kode zu entwickeln oder bereits bestehende zu überarbeiten,
weil dadurch sogar eine Vervielfachung der Abarbeitungsgeschwindigkeit
des Kodes erreichbar ist.
Dieser Artikel soll Hilfe und Richtschnur für solche Arbeiten sein.
Übersicht
Die Kerninformationen zum Thema sind kurz und bündig in Tabelle 1
und in den Kapiteln Maßnahmenkatalog und Parallelitätsmechanismus
enthalten. Lesern mit guten Kenntnissen hinsichtlich Intel-Mnemonics
und Assemblersprache wird hier eine Möglichkeit geboten, sehr schnell
einen hohen Kenntnisstand zu erreichen.
Der Pentium hat eine neue Gleitkomma-Einheit mit gegenüber dem 486er
sehr erheblich gesteigerter Leistungsfähigkeit.
Deshalb wurde Tabelle 2 für Vergleichsbetrachtungen hinzugefügt.
Tabelle 3 enthält Leistungsdaten zu verschiedenen bekannten
Bibliotheks-Funktionen und ermöglicht ebenfalls vergleichende
Betrachtungen.
Listing 1 zeigt den Assembler-Kode einer Funktion aus Tabelle 3.
Erklärungen zu den unverzichtbaren Abkürzungen und Darstellungsarten
im Text und in den Tabellen beenden den Textteil.
Grundlegendes zum Pentium
Der Pentium-Prozessor ist ein 32-Bit-Prozessor mit 64-Bit-Datenbus
und Buszyklus-Pipelining.
Er enthält zwei Integer-Ausführungseinheiten, die U- und die V-Pipeline,
die gemeinsam zwei Instruktionen gleichzeitig abarbeiten können.
Die Adressenberechnung ist ebenfalls doppelt vorhanden. Deshalb bleibt
die Parallelitätsfähigkeit auch mit Operanden für Speicherzugriff (mem)
erhalten. Der »Barrel-Shifter« ist allerdings nur einfach vorhanden
und der U-Pipeline zugeordnet, weshalb Schiebe- und Rotier-Instruktionen
nur eingeschränkt oder überhaupt nicht parallelisierbar sind.
Ein zusätzlicher Prefetch-Puffer hält Sprungziel-Kode bereit, der das
Potential hat, ausgeführt werden zu müssen. Bei tatsächlicher
Verzweigung dorthin wird ohne Verzögerung mit dessen Abarbeitung
begonnen.
Die Gleitkommaeinheit mit sehr erheblich gesteigerter Effizienz ist das
dritte wesentliche Merkmal des Pentium, das diesen leistungsmäßig
deutlich von seinem Vorgänger, dem 486-Prozessor, unterscheidet.
Die 8 Universalregister des Pentium:
>=386sx Subregister Subregister
32 Bit 16 Bit 8 Bit
---------------------------------------
eax : ax : ah, al
ebx : bx : bh, bl
ecx : cx : ch, cl
edx : dx : dh, dl
esi : si source index
edi : di destination index
ebp : bp base pointer
esp : sp stack pointer
Parallelitätsmechanismus
Eine Instruktion, zu der verzweigt wird, die hinter einer call- oder
jccc-Instruktion steht oder die Nachfolgerin einer NP-Instruktion ist,
wird jeweils der U-Pipeline des Pentium zugeordnet,
was man als Grundstellung des Parallelitätsmechanismus bezeichnen kann.
Falls dies eine (E)P-Instruktion ist, kann die V-Pipeline eine
nachfolgende (E)P-Instruktion aufnehmen und gleichzeitig abarbeiten.
Zwei bereits parallelisierte Instruktionen sind als eine einzige
NP-Instruktion anzusehen - mit den daraus resultierenden Konsequenzen
für vor- und nachgeordnete Instruktionen.
Man muß stets genau wissen, welche Instruktionen sich verbinden!
Oberflächliches Vorgehen führt zu Zufallsergebnissen.
Ein Übersehen von Details kann eine längere Kodesequenz »kippen«!
Parallelitätsfähigkeit
Ausgerichtet an der Parallelitätsfähigkeit kann man den Instruktionssatz
in drei Gruppen einteilen:
P-Instruktionen: parallelisierbar
EP-Instruktionen: eingeschränkt parallelisierbar
NP-Instruktionen: nicht parallelisierbar
Im großen und ganzen kann man sagen, daß diejenigen Instruktionen,
die zum Instruktionssatz des 8086-Prozessors gehören und die man
gleichzeitig den einfachen, elementaren Instruktionen zuordnen kann,
parallelitätsfähig sind.
Es überrascht, daß 'not, neg, xchg' nicht dabei sind.
'not eax' läßt sich durch 'xor eax,0ffffffffH' ersetzen, aber für die
anderen beiden braucht man mindestens 2 Ersatzinstruktionen.
P-Instruktionen sind nahezu ohne Einschränkungen parallelisierbar.
call- und jmp-Instruktionen können sich nur mit davorstehenden
Instruktionen vereinigen.
Bei den EP-Instruktionen gibt es verschiedene Arten der
Eingeschränktheit.
Die Shift- und Rotate-Instruktionen gehen nicht untereinander
und nicht mit einer davorstehenden Instruktion zusammen,
sondern nur mit einer nachfolgenden Instruktion.
Die Kurzform-memoffs-Instruktionen des eax-Registers sind untereinander
nicht parallelitätsfähig!
'fxch' kann sich in einer anderen f-Instruktion »verstecken«.
Bedingte Sprünge (j{ccc}) verzweigen in ½-1½ oder 1-2 Takten.
Ohne Verzweigung genügt ½ oder 1 Takt.
Sie schließen sich grundsätzlich nicht mit Nachfolgerinnen zusammen.
NP-Instruktionen verhalten sich so, als benutzten sie beide
Pipelines des Pentium. Sie sind folglich nicht parallelisierbar.
Ungünstige Positionierung verursacht bis zu zwei Zusatztakte
über den eigenen Zeitbedarf hinaus.
Zum einen kostet es einen Takt, wenn die Position ungeradzahlige
Abschnitte mit P-Instruktionen erzwingt.
Zum anderen entsteht oft ein Zusatztakt, wenn einer NP-Instruktion
mehr als zwei P-Instruktionen vorausgehen.
Taktbedarf und Parallelisierung
Ausgerichtet am Taktbedarf kann man die (E)P-Instruktionen
in 5 Gruppen einteilen:
1 reine Register-Instruktionen
1 Lesezugriff auf den Speicher (mov-memq)
2 Lesezugriff auf den Speicher (memq)
3 Lese- und Schreibzugriff auf den Speicher (memqz)
1-4 Schreibzugriff auf den Speicher (mov-memz)
Alle diese Instruktionen sind in beliebiger Kombination parallelläufig.
Es ist jedoch nicht so, daß zwei Instruktionen mit beispielsweise
je 3 Takten gemeinsam nur einmal 3 Takte benötigen, sondern ein durch
Parallelisierung erzielter Gewinn beträgt meistens nur 1 Takt!
Einzige Ausnahme ist die einzeln 2 Takte benötigende memq-Instruktion.
Die folgende Darstellung zeigt alle Kombinationen mit dem jeweils
resultierenden Taktbedarf, und in Klammern denjenigen Zusatztakt, den
man sich meistens einhandelt, wenn die beiden parallellaufenden
Instruktionen auf ein und dieselbe 4-Byte-Einheit (DWORD) zugreifen:
3|3 ==> 5(+1) 3|2 ==> 4(+1) 3|1 ==> 3(+1)
2|3 ==> 4(+0) 2|2 ==> 2(+1) 2|1 ==> 2(+0)
1|3 ==> 3(+1) 1|2 ==> 2(+1) 1|1 ==> 1(+1)
Achtung, der soeben beschriebene PZ-Zusatztakt kann zu dem Irrtum
führen, daß Parallelität gar nicht vorliegt - dem ist nicht so!
Auch bei der Folge 'push, pop' wird der P-Taktgewinn durch den
PZ-Zusatztakt wieder eliminiert!
Verhinderung von Parallelläufigkeit
Parallelläufigkeit zweier Instruktionen wird grundsätzlich verhindert
bei beliebiger Verwendung eines Registers in der zweiten Instruktion,
das in der Vorgängerinstruktion als ZIELoperand auftrat.
Diese Abhängigkeit gilt auch bei Subregistern, wenn diese
Bestandteil ein und desselben Registers sind oder wenn eines davon
Bestandteil eines vorher oder nachher benutzten Registers ist!
Beispiel: Mit 'al, ah, ax' wird jedesmal 'eax' und 'ax' angesprochen!
Gleichzeitiges Vorkommen von Displacement und Immediate schließt
Parallelisierung aus! (mov-memz, memqz, cmp, test)
Instruktionen, die diese zwei Datenfelder enthalten, muß der Pentium
also exklusiv verarbeiten.
Eine '1' als rechter Operand bei Schiebe- und Rotier-Instruktionen
ist dort ausnahmsweise kein Immediate-Wert, sondern im Opcode enthalten!
'cmp' und 'test' wirken auf nachfolgende Instruktionen so, als wäre
ihr linker Operand Zuweisungsziel!
Das gewinnt Bedeutung in den Fällen, wo man einen bedingten Sprung
nicht direkt folgen lassen möchte, sondern beispielsweise
'mov' oder 'lea', die ja die Bedingungs-Flags nicht verändern.
Die das Register 'esp' implizit verwendenden Instruktionen
(push,pop,call) können sich nur mit davorstehenden expliziten
esp-Instruktionen vereinigen.
Instruktionen mit hinzugefügtem Präfix gehen untereinander
und mit vorstehenden Instruktionen nicht mehr zusammen.
Das gilt mindestens für die Präfixe '66H' und '67H', die bei Benutzung
von 32-Bit-Registern in 16-Bit-Kodesegmenten -und umgekehrt- den
Opcodes vorangestellt werden.
'nop' ist ein Alias von 'xchg eax,eax'; der Opcode ist in beiden Fällen
'90H'. Der Pentium führt diesen Opcode glücklicherweise als 'nop' aus,
so daß nie irgendwelche Abhängigkeiten zu anderen Instruktionen
auftreten.
'xchg ebx,ebx' wird von Assemblern als Kode-Alignment eingesetzt,
weshalb 'ALIGN' u.a. nur hinter 'jmp' und 'ret' vorkommen sollte.
Unerwünschte Zusatztakte
BI Basis/Index-Zusatztakt
PZ Zugriff zweier P-Instr. auf ein und dasselbe Datum (-->)
Z2 Zugriff einer Instr. auf 2 Dateneinheiten gleichzeitig
PR Präfix-Takte
Ein Register als Zieloperand, das nachfolgend als Basis- oder
Index-Register zur Adressenberechnung dient, bewirkt einen BI-Zusatztakt
und verhindert gegebenenfalls Parallelisierung.
Beispielsweise können die beiden Instruktionen 'add + lea' zusammen
einen oder zwei oder drei Takte (ohne mem) zur Ausführung benötigen.
Achtung, zwei parallelisierte Instruktionen wirken als eine einzige
zusammengefaßte Instruktion, wodurch die Abhängigkeit, die einen
Zusatztakt verursachen kann, über bis zu zwei zwischenliegende
Instruktionen hinweg wirkt! Nach erfolgter Parallelisierung sind
diese beiden Instruktionen nämlich quasi verschwunden!
; 2 Takte + 1 [eax]-Zusatztakt:
mov eax, ecx
mov edi, ebp
dec edx
lea ebx, [eax+4]
Dieser Effekt ist wirklich sehr hartnäckig und tritt ebenfalls auf
bei zwischenliegender Sprung-Instruktion, und bei Verzweigung
(auch durch call!) wird auch diese zum Sprungziel hin überbrückt!
; 3.5 Takte + 1 [eax]-Zusatztakt:
ZIEL:
nop
add ebx, [eax+16]
nop
nop
sub eax, 4
jge SHORT ZIEL
Die tatsächliche Abarbeitungsreihenfolge ist hier maßgebend!
Daten-Alignment ist beim Pentium sehr wichtig:
Jeder »krumme« Adressenwert, der dazu führt, daß EINE Instruktion
auf ZWEI 4-Byte-Einheiten gleichzeitig zugreifen muß (Überlappung!),
hat 3 Cache-Zusatztakte pro Zugriff zur Folge!
Bei memqz-Instruktionen sind dies entsprechend 2*3=6 Zusatztakte!
Ein Ersatz durch Byte-Zugriffe kann schneller sein, obwohl dadurch
die Anzahl der Zugriffe größer ist. Ein Byte kann nicht überlappen!
Bei Cache-Zusatztakten durch »krumme« Adressen und gleichzeitiger
Parallelläufigkeit werden wiederum bis zu 3 Takte eingespart.
Ein Berechnungsalgorithmus für den letztgenannten Spezialfall existiert,
wird hier aber wegen Nutzenabwägung nicht beschrieben.
Daten, auf die die Gleitkommaeinheit zugreift, sollten in Begleitung
von 'ALIGN 8' angelegt werden. Andernfalls können 3 oder 6
zusätzliche Takte die erhebliche Folge sein!
Die Instruktionen 'push,pop,call,ret' verwenden implizit das
Register 'esp'.
Einen von diesen Stack-Instruktionen ausgehenden [esp]-BI-Zusatztakt
gibt es nicht, aber Instruktionen mit 'esp' als explizitem Zieloperand
lösen einen solchen andersherum aus.
Die Ausführung einer ret-Instruktion direkt vor 'call' (call call)
bewirkt 3-3.5 Zusatztakte, wenn es die gleiche Funktion (Adresse) ist.
Es wurden hier bis zu 9 Zusatztakte (3+6=9) gemessen, wobei die
Herkunft der 6 Takte nicht geklärt werden konnte.
Es entsteht ein zusätzlicher Präfix-Takt bei aufeinanderfolgenden
Instruktionen mit vorangestelltem Präfix, was auch für den permanenten
0fH-Präfix von 'mov[sz]x, set, ...' gilt.
Weiterhin entsteht ein Zusatztakt, wenn einer NP-Instruktion mit
permanentem Präfix mehr als zwei P-Instruktionen vorausgehen.
Beispielsweise 'cld,neg,not,xchg' sind NP-Instruktionen OHNE
permanenten Präfix!
486-Zusatztakte, wie bei Verwendung von Displacement+Immediate, Index,
und Vollregister-Benutzung nach Schreiben in ein zugehöriges
Subregister, gibt es beim Pentium nicht mehr.
mov-Instruktionen mit Memory-Operanden
Schreibende mov-Instruktionen haben einen Taktbedarf, dessen Mittelwert
nicht ganzzahlig ist und von der Anzahl der hintereinander angeordneten
Instruktionen abhängt. Der Bereich geht von 2.2-2.5, über 1.6-1.9,
3.8 (bei n=16), bis hin zu 4 Takten pro Instruktion.
Achtung, dennoch sind sie paarweise parallelisiert!
Beschleunigen kann man, indem in solche Instruktions-Blöcke eingefügte
Nicht-mem-Instruktionen keinen zusätzlichen Zeitbedarf erzeugen, sondern
quasiparallel laufen, was auch für NP-Instruktionen gilt!
Dies geht natürlich nur bis zu einer gewissen Grenze, und zwar muß
mindestens 1 Takt für jede memz übrigbleiben und bei eingefügten NPs
gilt hier ein Wert von etwa 1.4 Takten.
Eine mov-memz gemeinsam mit einer xxx-memqz (3) ergeben 3 Takte.
Folglich gilt mov-memz prinzipiell als 1-Takt-Instruktion: (3+1)-1=3.
Zwei Instruktionen 'mov mem1,r + mov r,mem2' benötigen zusammen
nur 1 Takt! Solche Kreuz-Anordnungen sind somit sehr schnell
und ergeben ganzzahlige Werte.
Viele solche Doppel hintereinander brauchen jedoch wiederum wesentlich
mehr als jeweils einen Takt, was auch für wiederholtes Anlaufen eines
solchen Doppels in einer -kleinen- Schleife gilt.
Die größte Wirkung im Zusammenhang mit mov-memz wird erzielt, wenn man
einem Block aus bis zu 12 solcher Instruktionen 2 mov-memq nachstellt,
die das erste und das letzte zuvor geschriebene Datenwort lesen.
Diesen Vorgang wickelt der Pentium in nur 6+1=7 Takten ab!
Bis zu 4 KByte im Stück lassen sich so mit ½ Takt pro DWORD schreiben!
(8 KByte: ~0.6 clks, >=16 KByte: ~6.5/4 clks)
Oberhalb von 8 KByte sollten die nachgestellten mov-memq wieder entfernt
werden, weil dann 4 statt 6.5 Takte erzielt werden.
Voranstellung von memq(z) wirkt auf mindestens 4 mov-memz, ist jedoch
insgesamt »anfälliger«.
Als weiteren »Geheimtip« kann man die an anderer Stelle dargestellte
SWAP-Funktion bezeichnen, die zwei Speicherstellen untereinander
austauscht und dafür (zumeist) nur 2 Takte benötigt.
In modifizierter Form kann die SWAP-Anordnung (Doppelkreuz)
auch andere Aufgaben erfüllen.
Man sollte stets anstreben, daß in Begleitung von mem-Instruktionen
beliebige andere (auch NP) Instruktionen auftreten, weil im weitesten
Sinne davon auszugehen ist, daß der Prozessor Operationen, die ihn daran
hindern sofort zur nächsten mem-Instruktion überzugehen, mit irgend=
welchen Register-Instruktionen (quasi)parallel ausführen kann.
Es darf nicht davon ausgegangen werden, daß die in diesem Kapitel
dargelegten Beobachtungen und Meßwerte für alle anderen Pentium-
-Systeme genau zutreffend sind!
Gleitkomma-Instruktionen
Die FPU-mem-Instruktionen gleichen im Verhalten ziemlich umfassend
ihren Integer-Entsprechungen.
Tabelle 2 zeigt den Taktbedarf der meistverwendeten FPU-Instruktionen.
In diesem Bereich ist der Pentium etwa um den Faktor 3 schneller
als der 486-Prozessor - der Spitzenwert liegt bei 10!
Die häufig gebrauchte Instruktion 'fxch' kann in der V-Pipe parallel
abgearbeitet werden, während die U-Pipe eine andere FPU-Instruktion
bearbeitet.
Ein ganz neuer Aspekt:
Die FPU-Instruktionen 'fmul' und 'fdiv' sind ganz erheblich bzw.
merklich schneller als die Integerversionen 'mul' und 'div'.
Die 7 Takte für 'fstsw ax' sind ärgerlich im Zusammenhang
mit 'fcom' == 1 Takt.
C-kompatible Assembler-Funktionen
Tabelle 3 zeigt auf, welches Potential darin enthalten ist, wenn man:
Library-Funktionen selbst entwickelt,
auf ein Pentium-System umsteigt, und,
Kode für den Pentium optimiert.
(Hinweis: '_i' kennzeichnet Intrinsics, '_L' und '87' Eigenentwcklgn.)
Die Angaben gelten für komplette Funktionsaufrufe einschließlich
der abschließenden Resultatzuweisungen.
Die Funktion fmne_L enthält über 300 FPU-Instruktionen und hatte sich
immer wieder als geeigneter Leistungsmesser bewiesen.
In der Regel kann man davon ausgehen, daß gute Eigenentwicklungen
erheblich schneller sind und einen geringeren Kodeumfang benötigen
als viele mitgelieferte Funktionen.
Desweiteren sollte man sich keineswegs auf Neuauflagen von Funktionen
aus der Standard-Bibliothek beschränken. Selbsterstellte Libraries
sparen ganz erheblich an Entwicklungs- und Kompilierzeit, reduzieren
die Fehlerwahrscheinlichkeit und ergeben höchsteffiziente Programme.
Zur Library-Entwicklung bietet sich auch die Sprache C an!
Durch speziell für den Pentium optimierten Kode wurden zusätzliche
Geschwindigkeitssteigerungen auf das 1.1- bis 4-fache erzielt.
Abkürzungen und Erklärungen
Begriffserklärungen:
Vollbestückte mov-memz-Instruktion mit Immediate-Wert:
mov mem, imm
mov Tab_Daten[esi+ebx*8+128], 654321
Basis-Register: esi
Index-Register: ebx
Scale-Faktor : *8
Displacement : Tab_Daten+128 (4 Byte)
Immediate-Wert: 654321 (4 Byte)
Kurzform-eax-memoffs-Instruktionen:
mov Tab_Daten, eax
mov eax, Tab_Daten
Assembler-Direktiven:
BYTE : 1-Byte-Datum
WORD : 2-Byte-Datum
DWORD: 4-Byte-Datum
QWORD: 8-Byte-Datum
TBYTE: 10-Byte-Datum
ALIGN: Gezieltes Hinzufügen von Füll-Bytes zum Zwecke einer
einheitlichen Ausrichtung (Alignment)
Es werden folgende Abkürzungen benutzt:
P-, EP- und NP-Instruktionen sind parallelisierbare, eingeschränkt
parallelisierbare und nicht parallelisierbare Instruktionen.
xxx : Nicht-mov-Instruktionen.
mem : Instruktionen mit Speicherzugriff; Memory-Operand
memq : Memory-Operand ist der Quell-Operand.
memz : Memory-Operand ist der Ziel-Operand.
memqz: Lesen, ändern, zurückschreiben. (z.B. add mem,r)
m&d : Memory-Instruktionen mit Displacement.
SWAP : 4 x mov: r1<--mem1, r2<--mem2, mem1<--r2, mem2<--r1
m : Memory-Operand
r : Register-Operand
imm : Immediate-Wert (Konstante) als Operand
Die Darstellungsformen in den Tabellen folgen den Konventionen,
die in den Intel-Handbüchern vorherrschen:
shl r/m,imm 1/3 :
shl r,imm 1
shl m,imm 3
shl r/m,imm ½¦1/2½¦3 :
shl r,imm ½¦1
shl m,imm 2½¦3
parallelisiert ¦ nicht parallelisiert
s[ha][lr] : shl, shr, sal, sar
rep movsd 13+2n;4 :
Initialisierung + (x * ecx) ; Takte ohne rep-Präfix
{ ... } : Abweichende Werte aus Intel-Handbuch [1]
Tabelle 2:
Mittelwert(von-bis)concurrent_processing_Wert (Spalte i486)
[ optional ]
a ¦(oder) b
Die in diesem Artikel gemachten Angaben zum Pentium-Prozessor
stützen sich fast ausschließlich auf eigene Messungen.
Hauptsächlich bei Abweichungen wurden entsprechende Werte aus einem
Intel-Handbuch [1] in geschweiften Klammern hinzugefügt.
Maßnahmenkatalog
Nachfolgend eine Aufzählung von optimierenden Maßnahmen in Kurzform -
- das, was den Pentium noch schneller macht:
- Einsatz von NP-Instruktionen möglichst weitgehend vermeiden.
Besonders diejenigen mit 0fH-Präfix (zusätzlich ab dem 386er).
- Verwendung von P- und EP-Instruktionen forcieren.
- Möglichst große, zusammenhängende Gruppen aus P- und
EP-Instruktionen
bilden. Dabei idealerweise eine geradzahlige Anzahl anstreben.
- NP-Instruktionen nicht inmitten von P- und EP-Instruktionen anordnen,
sondern vorher oder nachher; in Schleifen möglichst zuoberst.
- EP-Instruktionen (shift,rotate) nicht nach P- und nicht vor
NP-Instruktionen setzen, sondern vor P-Instruktionen.
- Aufeinanderfolgende EP-Instruktionen (shift,rotate) vermeiden.
- Register (Quelle, Ziel, Basis oder Index), die zuvor Zieloperand
waren, verhindern Parallelisierung mit der Vorgängerin.
Achtung, beispielsweise 'al' und 'ah' sind auch voneinander abhängig,
weil sie beide Bestandteil des Registers '(e)ax' sind!
NP- und bereits parallelisierte Instruktionen sind natürlich
in diesen Hinsichten isoliert.
- Adreßregister (Basis oder Index), die zuvor Zieloperand waren,
bewirken einen Zusatztakt und verhindern ggf. Parallelisierung.
Achtung, zwei parallelisierte Instruktionen wirken als eine einzige
zusammengefaßte (NP-)Instruktion, wodurch verlangsamende Wirkungen
bis zu zwei Instruktionen überspringen können!
- Man beachte die impliziten Verwendungen des Stackpointers 'esp'!
- Daten-Alignment durch 'ALIGN 4(8)' ist sehr wichtig.
Zugriffe mittels »krummer« Adressen, die auf zwei DWORDs (QWORDs)
gleichzeitig erfolgen (Überlappung), sollten möglichst nicht
vorkommen! Nichtbeachtung kann den Taktbedarf von bis zu 12
Instruktionen verschwenden!
- Zugriff zweier parallelisierter mem-Instruktionen
auf ein und dasselbe Datum vermeiden. (Zusatztakt!)
- Gleichzeitige Verwendung von Displacement und Immediate-Operanden
vermeiden.
- xxx-memq-Instruktionen möglichst untereinander parallelisieren.
- Besonders memz-Instruktionen mit beliebigen anderen vermischen.
- Bei mem-Instruktionen memz- und memq(z)-Instruktionen kombinieren!
Eine Folge 'mov-memz, mov-memq' ist mehr als doppelt so schnell
wie eine einzelne mov-memz-Instruktion!
- Füll-Instruktionen (Kode-Alignment) an anderen Stellen als hinter
'jmp' und 'ret' unbedingt vermeiden.
- Memory-Operand (zumindest) bei 'xchg' (NP) tunlichst unterlassen.
Diese Instruktion hat dann einen abenteuerlich hohen Taktbedarf!
- Mit Ausnahme bei den zwangsweise zugeordneten Instruktionen gibt
es längst keinen Grund mehr für eine »Verliebtheit«
in das
eax-Register! Das heißt, es sollten keine Extras vorgenommen werden,
nur um dieses Register verwenden zu können.
Manche Instruktionen mit 'eax' sind zwar kürzer - aber langsamer!
- Instruktionen mit vorangestelltem Präfix vermeiden.
- Wirklich »echter« 32-Bit-Kode in 32-Bit-Segmenten hat beim
Pentium
eine noch weiter gesteigerte positive Wirkung.
32-Bit-Instruktionen in 16-Bit-Segmenten (z.B. DOS) sind nämlich
präfix-behaftet und desweiteren gibt es viele Segment-Präfixe
und
viele zusätzliche langsame Segmentregister-Instruktionen!
Tabelle 1a
========================================================================
P-Instruktionen Taktbedarf EP-Instruktionen Taktbedarf
------------------------------------------------------------------------
add ½¦1 s[ha][lr] r/m,imm ½¦1/2½¦3
adc ½¦1 s[ha][lr] r/mem,1 ½¦1/2½¦3
sub ½¦1 s[ha][lr] r/m&d,1 ½¦1/2½¦3
sbb ½¦1 r[oc][lr] r/mem,1 ½¦1/2½¦3
inc ½¦1 r[oc][lr] r/m&d,1 ½¦1/2½¦3
dec ½¦1 call ½¦1
and ½¦1 jmp ½¦1
or ½¦1 j{ccc} ½-2,½¦1
xor ½¦1 fxch 0¦1
lea ½¦1 mov moffs,eax ½-4 {½¦1}
mov ½¦1 mov eax,moffs ½¦1
cmp ½¦1
test ½¦1
nop ½¦1
push ½¦1
pop ½¦1
jmp ½¦1
call ½¦1
mov r,mem ½¦1
mov mem,r/imm ½-4 {½¦1}
xxx mem,r 2½¦3
xxx r,mem 1¦1½¦2
SWAP mem<->mem 2¦>2
========================================================================
Tabelle 1b
========================================================================
NP-Instruktionen Taktbedarf NP-Instruktionen Taktbedarf
------------------------------------------------------------------------
neg r/m 1/3 xchg (lock!?) 2¦3/11.5
not r/m 1/3 xchg r,m + mov r,m 24.5 !!!
s[ha][lr] m&d,imm 3 xchg r,m + 2x mov r,m 36.5 !!!
s[ha][lr] r/m,cl 4/5 xxx m&d,imm 3
ro[lr] r/m,imm 1/3 mov m&d,imm 1-4
ro[lr] r/m,cl 4/5 push mem 2
rc[lr] r/m,imm 8/10 pop mem 3
rc[lr] r/m,cl 7/9 rep movsd 13+2n;4
shld 5¦4 rep movsd {13+1n;4}
mov[zs]x 3/3(+1) rep lodsd 7+3n;2
set{ccc} 1 (+1) rep stosd 9+(2-4)n;3-4.5
cld 2 rep stosd { 9+1n;3}
enter 11 rep__ cmpsd 9+4n;5
leave 3 rep__ scasd 8+4n;4
ret 2
pushad 5
popad 5
========================================================================
Tabelle 2
========================================================================
FPU-Instruktionen: Pentium i486 i387/386
-----------------------------------------------------------------------
fld m32,64,80,st 1,1,3,1 3,3,6,4 20,25,44,14
fst[p] mem32,64 3¦4-8 {2/2} 7,8 44,45
fstp mem80 4¦5-10 {3/3} 6 53
fst[p] st(i) 1 3 11¦12
fild 1-3 {3/1} 12-17()2-8 45-72
fist[p] 6,8,8+ {6/6} 33 79-97
fld{1¦z} 2 4 24,20
fld{const} <=8 {5/3} 8()2 40¦41
fxch 0¦1 4 18
fadd[p] 3 10(8-20)7 23-37
fsub[p][r] 3 10(8-20)7 30(24-36)
fiadd 4-6 {7/4} 23(19-35)7 57-85
fisub[r] 4-6 {7/4} 23(19-35)7 57-86
fmul 3 11,14()8,11 27-57,52
fmulp 3 16()13 29-57
fimul 4-6 {7/4} 24()8 61-87
fdiv[p][r] 19¦33¦39 73()70 90-94,120-141
fidiv 22¦36¦42 73()70 90-94,120-141
fsqrt 70 86()70 122-129
fabs 1 3 22
fchs 1 6 25
fprem 32 {16-64} 84(70-138)2 74-155
frndint 16 {9-20} 29(21-30)7 66-80
fscale 32 {20-31} 31()2 67-86
fxtract 12 {13/13} 19(16-20)4 70-76
f[u]com[p][p] 1 ! {4/1} 4,5 24-26,-31
ficom[p] 4 ! {8/4} 17()1 56-75
ftst 1 ! {4/1} 4()1 28
fxam 18 {21/21} 8 30-38
fsin, fcos 70 {16-126} 241(193-279)2 122-772/+76
fsincos 93 {17-137} 291(243-329)2 194-809/+76
fptan 124 {17-173} 244(200-273)70 191-497/+76
fpatan 130 {19-134} 289(218-303)5 314-487
f2xm1 50 {13-57} 242(140-279)2 211-476
fyl2x[p1] 95 {22-111} 311(196-329)13 120-538
fstsw ax 7 ! {6/2} 3(+3) 15,13(+6)
fstcw 4 {2/2} 3(+3) 15(+6)
fldcw 8 {7/7} 4 19
ffree 2 {1/1} 3 18
fwait 1 1-3 6
-----------------------------------------------------------------------
Tabelle 3
========================================================================
Funktion Zeit [æs] Takte Prozessor-f[MHz]
------------------------------------------------------------------------
i= atoi ("1234567899"); 25.50 638 i386DX-25
i= atoi ("1234567899"); 4.50 270 Pentium-60
i= atoi_L ("1234567899"); 1.48 89 Pentium-60
i= atoi ( "12345"); 2.58 155 Pentium-60
i= atoi_L ( "12345"); 1.04 62 Pentium-60
i= atoi ( "5"); 1.07 64 Pentium-60
i= atoi_L ( "5"); 0.37 22 Pentium-60
s= itoa ( 1234567899 ); 15.02 901 Pentium-60
s= itoa_L ( 1234567899 ); 6.41 384 Pentium-60
s= itoa87 ( 1234567899 ); 3.23 194 Pentium-60
s= itoa ( 12345 ); 8.71 522 Pentium-60
s= itoa_L ( 12345 ); 2.62 157 Pentium-60
s= itoa87 ( 12345 ); 3.66 219 Pentium-60
s= itoa ( 5 ); 3.77 226 Pentium-60
s= itoa_L ( 5 ); 0.76 45 Pentium-60
s= itoa87 ( 5 ); 3.73 224 Pentium-60
i= strcmp_i( abcd , abcD ); 1.072 ! 64 Pentium-60
i= strcmp_i( abcd , "abcD"); 0.602 ! 36 Pentium-60
i= strcmp ("abcd", "abcD"); 0.770 46 Pentium-60
i= strcmp_L("abcd", "abcD"); 0.351 21 Pentium-60
fmne_L (/* SPEZfp */); 592.00 14800 i387DX-25
fmne_L (/* SPEZfp */); 285.00 7125 cx387DX-25
fmne_L (/* SPEZfp */); 41.20 2472 Pentium-60
d= pow (b, e); 578.67 14467 i387DX-25
d= pow87 (b, e); 77.50 1938 i387DX-25
d= pow (b, e); 326.67 8167 cx387DX-25
d= pow87 (b, e); 34.00 850 cx387DX-25
d= pow (b, e); 27.20 1632 Pentium-60
d= pow87 (b, e); 4.40 264 Pentium-60
d= powi87 (b, 10); 44.33 1108 i386+387-25
d= powi87 (b, 10); 22.67 567 i386+cx387-25
d= powi87 (b, 10); 1.43 86 Pentium-60
d= powi87 (b, 1000); 3.82 229 Pentium-60
d= powi87 (b, 100000); 5.73 344 Pentium-60
s= findstr_L(s, a1, a2); 260 ms pro MByte Pentium-60
s= findstr_L(s, a1, a2); OPT. 27 ms! pro MByte Pentium-60
------------------------------------------------------------------------
Listing 1
; sc/11.07.94
TITLE atoi_L
.386
.MODEL small
PUBLIC _atoi_L
.CODE ; 32-Bit-Segment
_atoi_L PROC ;Takte
push ebx ; 1
push esi ; 1
mov esi, [esp+12] ; 2, 4+2*4=12
xor eax, eax ; 2
push edx ; 3, wegen [esi]-BI
push ecx ; 3, wegen [esi]-BI
mov dl, [esi] ; 4
xor ebx, ebx ; 4
cmp dl, '0' ; 5
jae SHORT $digit ; 5
inc esi
$digit:
push edi ; 6
xor edi, edi ; 6
mov ecx, 10 ; 7
$loop:
mov al, [esi] ; 1/7
add edi, edi ; 1, edi(=ebx) *= 2
sub al, '0' ; 2
js SHORT $break ; 2
cmp al, 9 ; 3
ja SHORT $break ; 3
lea ebx, [edi+ebx*8] ; 4, ebx *= 10
inc esi ; 4
add ebx, eax ; 5
dec ecx ; 5
mov edi, ebx ; 6
jg SHORT $loop ; 6
$break:
cmp dl, '-' ; 1
jne SHORT $pos ; 1
neg ebx
$pos:
mov eax, ebx ; 2
pop edi ; 2
pop ecx ; 3
pop edx ; 3
pop esi ; 4
pop ebx ; 4
ret ; 6
ALIGN 4
_atoi_L ENDP
END
[1] Pentium Family User's Manual,
Volume 3: Architecture and Programming Manual, INTEL 1994
[2] i486 Microprocessor, Programmer's Reference Manual, INTEL 1990
[3] 387DX User's Manual, Programmer's Reference, INTEL 1989
|