Welcome to the new Friends-of-FPC!

Here you can find all kinds of information about the FreePascal Compiler. We have many tutorials and howtos as well as a selection of tools to help you with your programming. We also have some example codes for you. And if you want to contribute some information/ sources/ tools yourself you can do so.
Also we have finally relaunched the FoFPC forum. It's your chance for some Q&A about everything FreePascal.

Friends-of-FPC

Tutorials: Learn how to code with FreePascal.

Source Codes: A collection of examples, miscellaneous source codes and open source stuff.

Tools and Help Files: Intro- duction of some tools that might help you with FPC.

Community

Forum: Ask or answer questions about the FreePascal Compiler, programming or just babble about coding.

Contribute! Contribute your own Tutorial, Source Codes or Tools and send them to us!

Website

About: Information about Friends-of-FPC.org.

Grundlagen der Grafikprogrammierung - Teil 13 - by Delax

Tach'!

Oh nein! Tutorial, der 13.! Assembler! Aber keine Sorge, so schlimm wird es schon nicht - ich werde nur auf ein paar Grundlagen von Assembler eingehen, gerade genug, damit ihr die PutPixel Routinen und alle anderen versteht, die da folgen mögen. Das bedeutet aber auch, das dies keinesfalls als kompletter ASM Einstieg gesehen werden sollte. Mehr als ein Überblick soll es gar nicht sein.

Assembler hat irgendwie den Ruf völlig unverständlich und kompliziert zu sein und wenn man mal von ASM redet, dann nur nervös im Flüsterton. Vielleicht liegt es an dem Trend nur noch in visual Sprachen zu coden und alles hübsch abstrakt und so natürlichsprachlich wie möglich zu halten. Ich hoffe aber, ich kann euch dennoch zeigen, das Assembler weder Teufelswerk noch nur für Götter ist.

In ASM ist alles so klar wie möglich. Jede Zeile besteht nur aus einer Anweisung! Zeilen wie "IF x > 1 THEN y := TRUE ELSE y := FALSE;" sind nicht. Eine Zeile, eine Anweisung. Das macht ASM Code natürlich länger als Pascal Code, aber denkt mal nach: pro Zeile nur eine Anweisung heißt auch eine sehr gute Übersichtlichkeit!

Was Assembler nun so gefürchtet macht ist die Art, wie es mit Variablen und Schleifen umgeht. Dazu muß man nämlich etwas logisch zu werke gehen. Fangen wir aber mal ganz harmlos an.. mit Registern. Es gibt grundsätzlich einmal 4 Register: AX, BX, CX, DX. Diese könnt ihr euch als eine Art Variable vorstellen. Vom Typ sind sie Words (double bytes) mit einer Reichweite von 0 bis 65535. Dazu gibt es noch EAX, EBX, ECX und EDX. Diese sind erweiterte Register, die auch mit größeren Werten umgehen können (die Wirklichkeit ist wie immer etwas komplizierter, aber im Moment soll das euch genügen).

Neben diesen "Variablen" gibt es noch 2 Pointer: ES:DI und DS:SI bzw. EDI und ESI. Die linke Seite ist der Zeiger auf das jeweilige Datensegment und die rechte Seite ist der Offset, also wie weit ihr in das Segment zeigt. Gut, aber was heißt das im Klartext? Mal angenommen wir nutzen unseren Pointer zum Bildschirmspeicher "vidvscr". Dann wäre ES = vidvscr und DI wäre das Segment in dem wir uns in vidvscr befinden. Alles klar?

Beispiel aus der PutPixel: mit "mov edi, vidvscr" binden wir den Pointer an EDI. Danach können wir mit "add edi, eax" uns im vidvscr um eax Segmente weiterbewegen. Den Inhalt eben jenes Segmentes könnten wir mit [edi] verändern oder auslesen.

Die Nachfolgende Tabelle ist aus dem Denthor Tutorial 7:

mov

destination, source

Dies bewegt einen Wert von Source in Destination.
Z.B. mov ax,50.

add

destination, source

Dies addiert den Wert in Source zu Destination.

mul

source

Dies Multipliziert den Wert in AX mal dem Source. Wenn Source ein byte ist wird Source mal AL genommen und das Ergebnis in AX gespeichert. Wenn Source ein word ist wird Source mit AX multipliziert und das Ergebnis in DX:AX gespeichert.

movsb

Dies bewegt das Byte auf das DS:SI zeigt in ES:DI und erhöht SI und DI

movsw

Das selbe wir movsb, nur mit einem word

stosw

Dies bewegt AX in ES:DI. stosb bewegt AL in ES:DI. DI wird erhöht.

push

register

Dies speichert den Wert des Registers indem es in den Stack kopiert wird. Das Register kann dann verändert werden, wird aber den alten Wert erhalten, wenn es pop erhält.

pop

register

Dies stellt einen Wert her, der vorher in den Stack kopiert wurde. ANMERKUNG: Werte die mit push in den Stack gelegt wurden müssen IN DER SELBEN REIHENFOLGE ANDERS HERUM wieder zurück geholt werden.

rep

command

Dies wiederholt Command so oft wie in CX angegeben.

Ich wies schon auf einige Schwierigkeiten im Verständnis von Assembler hin und auf jene wollen wir mal eingehen.

"mul source". Als Erklärung steht da, das der Wert Source mit dem Inhalt des Registers AX multipliziert wird. Das bedeutet so Spielereien wie "x := y * z;" sind nicht möglich. Ihr müsstet dazu erst y in AX kopieren und dann mul z ausführen.

Habe ich schon die gute Nachricht erwähnt? Ihr könnt, wenn ihr inline Assembler in Pascal verwendet eure definierten Variablen ganz normal (bis auf wenige Ausnahmen wie Records) weiterverwenden. Wenn ihr also eine Variable "Xposition" deklariert habt ist ein "mov eax, Xposition" möglich.

Schauen wir uns jetzt noch einmal die PutPixel Routine an:

mov edi,destiny Bindet den Virtual Screen an das Register edi.
mov eax,x Kopiert den Inhalt von x in das Register eax.
mov ebx,2 Kopiert den Wert 2 in das Register ebx.
imul ebx Multipliziert ebx mit eax (mit anderen Worten: die angegebene X Koordinate mal 2)
add edi,eax Bewegt sich im Virtual Screen um das Ergebnis nach vorne.

mov eax,MaxX Bewegt MaxX nach eax.
imul ebx Multipliziert ebx mit eax (mit anderen Worten: die angegebene MaxX mal 2)
imul y Multipliziert eax mit y (Also MaxX mal 2 mal der angegebenen Y Koordinate).
add edi,eax Bewegt sich im Virtual Screen um das Ergebnis weiter.

mov al,color1 Kopiert den Inhalt von color1 nach al
mov [edi],al Trägt an der Stelle im Pointer den Wert ein, der in al steht.

add edi,1 Bewegt sich im Virtual Screen um eins nach vorne.
mov al,color2 Kopiert den Inhalt von color2 nach al.
mov [edi],al Trägt an der Stelle im Pointer den Wert ein, der in al steht.

Jetzt sollte vieles verständlicher sein, was da eigentlich steht und was gemacht wird. Gehen wir noch einmal kurz auf Schleifen ein..

Schleifen sind vielleicht etwas zu viel gesagt. Es handelt sich vielmehr ähnlich wie in BASIC um Sprünge an Handles, falls eine Bedingung erfüllt bzw. nicht erfüllt ist. Ein Beispiel:

  cmp eax,ebx
  je @@handle1
  jmp handle2

Was passiert da? "cmp" bedeutet "compare", also vergleichen. In diesem Falle wird eax mit ebx verglichen. "je" steht für "jump equal", also "springe wenn gleich". Der Ziel des Sprungs ist das handle1. Wenn eax und ebx nicht gleich sind wird einfach weiter im Code gemacht - also in diesem Falle zu handle2 gesprungen.

  cmp eax,ebx
  jne @@handle2
  jmp handle1

Diese Zeilen machen genau das gleiche wie oben: sie vergleichen eax mit ebx. Nur wird diesmal geprüft, ob die beiden Werte ungleich sind (jne = jump not equal).

Wie vergibt man Handles? Das ist simpel - sie werden einfach an einer beliebigen Stelle des Codes eingefügt:

  @@handle1:
  ... ...
  ... ...
  ... ...

Benennen könnt ihr Handles wie es euch beliebt, nur sollten es keine reservierten Wörter wie mov etc sein. Eine Altertnative ist "jnz" oder "jz" (jump not zero, jump zero). Hierbei wird der Wert geprüft und gesprungen falls er null, bzw. nicht null ist.

Auch möglich ist es "loop" zu nutzen, und so eine Schleife so oft wie in cx/ ecx angegeben durchlaufen zu lassen:

  mov cx, anzahl
  @@schleife1:
  
  ... ...
  ... ...

  loop schleife1

Die Alternative dazu lautet:

  mov cx, anzahl
  @@schleife1:

  ... ...
  ... ...

  dec cx
  mov dx, 0
  cmp cx, dx
  jne @@schleife1

Durchaus logisch - es wird pro Schleifenrunde cx um 1 reduziert und geprüft, ob cx = dex = 0 ist. Wenn nein, dann springe wieder zum Anfang. Logisch kein Unterschied, nur die zweite Variante ist flexibler und schneller.

So viel erst einmal zu Assembler. Jetzt noch ein paar Tipps und Tricks zum Thema Optimierung.

Zuerst einmal: aller Assembler Code der Welt wird euer Programm nicht schneller machen, wenn der Algorithmus schon Mist ist. Anstatt euch Stunden um die Ohren zu schlagen, eure Pascal Routinen in ASM zu übersetzen, solltet ihr die Zeit nutzen und an euren Algorithmen selbst feilen. Das bringt einfach mehr.

Selbst wenn euer Algorithmus eurer Meinung nach schon perfekt ist solltet ihr euch nicht dran machen ihn in Assembler zu übersetzen, nur weil "Assembler immer schneller ist". Moderne Compiler sind eigentlich so fit aus höheren Sprachen effektiven Code zu schneidern. Wirklich lohnen tut sich Assembler Code nur, wenn die Prozedur oft pro Frame aufgerufen wird oder ASM einfach bequemer als die eigentliche Programmiersprache ist (wie bei Pointer handling z.B.).

Wenn ihr wirklich euren ASM Code hyper-hooper optimieren wollt schafft euch eine Liste an, welche Funktionen wie viele Clock Ticks benötigen. Schnappt euch einen Stift und rechnet herum wann eure Prozedur wieviel Clock Ticks benötigt. Ihr könnt sie auch von einer anderen Funktion überwachen lassen (siehe beliebiges Tutorial über optimierung). Somit könnt ihr gezielt auf die Teile des Codes losgehen, die zu viel Zeit benötigen. Aber das ist eher nur was für Leute, die wirklich den letzten Tropfen Blut aus ihrem System quetschen wollen.

Scheut euch nicht globale Variablen zu nutzen. Globale Variablen sind euer Freund! Bei Zeitkritischen Prozeduren ist es nicht unbedingt sinnvoll lokale Variablen zu deklarieren. Anstatt also jeder Prozedur ihre eigenen Schleifenzähler zu geben deklariert sie einfach global. Selbiges gilt für übergebene Werte. Jene müssen nämlich beim Aufruf durch die Gegend gepushed und gepopped werden. Auch hier würden globale Variablen helfen.

Nutzt immer 32 Bit Variablen anstatt 16 oder 8 Bit Variablen. Moderne Prozessoren sind darauf ausgelegt 32 Bit Werte zu verarbeiten. Im Falle der Performance pfeifen wir auf den höheren Speicherverbrauch. Selbiges gibt auch bei Strukturen und Records. Ein Beispiel:

beispielrecord : RECORD
                   x, y : WORD;
                   buchstabe : CHAR;
                 END;

Das macht 2 X 2 byte und ein byte für den CHAR = 5 byte oder 40 Bit. Das ist höflich gesagt Napalm für die Speicheradressierung. Das Optimum sind 32 Byte Strukturen. Wer das wirklich ausreizen will versieht also seine Strukturen zur Not mit Dummy Variablen. Und Hände weg von 8 oder 16 Bit Variablen, selbst wenn ihr nie über den Zahlenraum 0-255 hinaus kommt. Versucht zumindest immer bei Multiplikatoren von 4 Byte (32 Bit) zu bleiben.

Versucht so einfach wie möglich zu coden. Also anstatt alles in eine Zeile zu pressen solltet ihr möglichst einfache Codestrukturen vorziehen, auch wenn das dann den Code in die Länge zieht. Dadurch erhöht sich nicht nur die Lesbarkeit, sondern der Compiler kann auch effektiveren Maschinencode produzieren.

Der wichtigste Tipp ist: optimiert nie und unter gar keinen Umständen während ihr die Prozedur noch entwickelt! Schreibt erst die fertige Prozedur und optimiert dann! Wenn am Schluß die Prozedur nicht funktioniert werdet ihr sonst nie wissen ob das nun an eurem Algorithmus liegt oder an den Optimierungen.

Und wo wir schon dabei sind: nutzt vernünftige Variablennamen und Bezeichner! Anstatt x, x1, x_1 und xx1 nutzt dann lieber SpriteX, PartikelX, ObjektX und TextX oder von mir aus auch Records a la Sprite.X, Partikel.X ...

Zum Abschluß sei gesagt: optimieren macht von Grund auf nicht immer Sinn. Die Zeiten wo man mit ASM Code bis zu Faktor 1000 Geschwindigkeit gewinnen konnte sind vorbei. Und tagelang vor einer Routine zu hocken für 10-20 Prozent bringt auch nur echten Freaks Befriedigung. Die angeschprochenen Faustregeln lassen sich jedoch einfach beachten und bringen somit aus Gewohnheit schon mal ein bischen was.

Bevor ihr mich jedoch mit Mails bombardiert a la "Diese Struktur wäre soundso aber viel besser!" - für die Sources und Algorithmen dieses Tutorials werde ich so wenig wie möglich Optimierung anwenden. Der Grund ist einfach: optimierter Code ist oft schlechter zu lesen als unoptimierter. Es geht darum das ihr es kapiert und nicht die schnellste und beste Lösung zu präsentieren. Schließlich und letztendlich ist Optimierung etwas, was man nur durch Erfahrung und probieren lernen kann.

Delax/ Sundancer Inc.
[delax@sundancerinc.de]

Back to previous page

Useful Links









Link to us