Basic optimieren - Teil 1
mad/os

Die Nachfrage nach einem solchen Kurs ist offensichtlich da, immerhin haben sich vier Leute gemeldet. Ich denke, das reicht. Also los geht's!

Zuerst einmal sollte ich betonen, daß einige der hier genannten Tips nur mit PowerBASIC funktionieren. Sicherlich sind viele Dinge auch auf QBasic anwendbar, aber ich bezweifle, daß es dort etwas bringt. QBasic ist nun einmal nur ein Interpreter und etwa 10 bis 100 mal langsamer als PowerBASIC. Allen Interessierten empfehle ich darum FirstBasic. Diese uneingeschränkte Shareware ist mit PowerBASIC 2.1 identisch. Zu haben ist das gute Stück unter www.powerbasic.com oder bei mir. Doch genug geschwafelt, jetzt geht der Kurs los...

1. MTIMER: Wenn es wirklich auf die Geschwindigkeit ankommt, dann muß man sich die Mühe machen und Zeile für Zeile, Befehl für Befehl durchsehen und optimieren. Nach jeder noch so kleinen änderung muß die Geschwindigkeit gestoppt und verglichen werden. Dabei geht es oft um Millisekunden, die am besten mit einem Basic-Programm gemessen werden:

DEFINT a-z: CLS: MTIMER
FOR i=1 TO 100                'Zählschleife kann angepaßt werden
  ***            'beliebige Funktion oder Unterprogramm einfügen
NEXT
Zeit&=MTIMER: PRINT Zeit&/1000

Damit können einzelne Befehle oder ganze Unterprogramme auf ihren Zeitbedarf hin untersucht und verglichen werden. Ein Beispiel: Die Befehle x=x+1, INCR x,1 und INCR x bewirken das gleiche, aber welcher davon ist der schnellste? (siehe Tip 8) Achtung! Der Befehl MTIMER benötigt selbst sehr viel Zeit! Er sollte nur sparsam verwendet werden.

2. INTEGER: Standardmäßig sind alle Variablen eines Basic-Programmes als Fließkomma (Single) definiert. Dieser Typ ist sehr universell verwendbar, aber auch ziemlich langsam. Der schnellste Datentyp ist Integer oder besser gesagt: Viele Funktionen werden mit Integer-Variablen schneller ausgeführt. Durch einfache änderung des Datentyps kann ein Programm unter Umständen spürbar beschleunigt werden! Einzelne Variablen werden als Integer definiert, indem man ein Prozentzeichen anhängt. Nachteil: Variablen vom Typ Integer müssen ganzzahlig sein und sie müssen im Bereich von -32.768 bis +32.767 liegen.

3. DEFINT A-Z: Ich habe mir angewöhnt, jedes Programm mit diesem Befehl zu beginnen, selbst wenn Bytes genügen würden. Auf diese Weise sind alle Variablen standardmäßig als Integer definiert und somit auch schneller. Ein weiterer Vorteil: Die compilierte EXE-Datei wird ein wenig kleiner! Wenn doch ein paar Variablen mit anderen Datentypen benötigt werden, so hänge ich an diese das entsprechende Zeichen an, oder ich dimensioniere sie. Mit dem Befehl DIM x AS REAL wird z.B. die Variable X als Fließkomma definiert. An das X braucht dann kein Ausrufezeichen mehr angehängt werden.

4. DIVISION: Die Division ist eine der langsamsten Funktionen in Basic. Die Integerdivision wird dagegen fast doppelt so schell ausgeführt! Dazu braucht nur der normale Schrägstrich gegen einen umgekehrten ausgetauscht werden. Aus der Zeile c=a/b wird dann c=a\b. Natürlich hat diese Methode auch Nachteile. Es ist nämlich eine Ganzzahlen-Division, bei der alle Nachkommastellen einfach abgeschnitten werden. Aber bei Integer-Variablen ist das ohnehin egal.

5. FOR-NEXT: In dieser und anderen Schleifenkonstruktionen lohnt es sich besonders, nach unnötigen Bremsen zu suchen. Ein Beispiel:

FOR z=1 TO 25
  FOR s=1 TO 40
    LOCATE z, s*2-1: PRINT "X";
  NEXT
NEXT

Die verschachtelten Schleifen sorgen dafür, daß die Befehle LOCATE und PRINT genau 1.000 mal ausgeführt werden. Und das bedeutet, daß auch die Formel s*2-1 genau 1.000 mal berechnet wird. Dabei ändert sich das Ergebnis nur, wenn sich die Variable S verändert. Die Formel kann also auch an einer anderen Stelle stehen:

FOR s=1 TO 40
  a=s*2-1            'Berechnung wird nur noch 40 mal ausgeführt
  FOR z=1 TO 25
    LOCATE z, a: PRINT "X";
  NEXT
NEXT

Jetzt gibt es zwar eine neue Variable A, aber das Programm ist trotzdem spürbar schneller geworden. Schließlich wird die Formel nur noch 40 mal berechnet, und das macht sich bemerkbar! Ich mußte zusätzlich die beiden Schleifen austauschen, weil die änderung sonst nicht möglich gewesen wäre.

6. IF-THEN-GOTO: Basic bietet viele verschiedene Möglichkeiten, Schleifen zu konstruieren. Die Befehle FOR-NEXT, DO-LOOP und WHILE-WEND sind sicherlich bekannt. Eine solche Schleife kann auch mit einem IF-THEN-GOTO manuell erstellt werden. Meine Tests haben aber gezeigt, daß alle diese Möglichkeiten gleich schnell sind! Es darf also guten Gewissens die schönste und eleganteste Variante gewählt werden.

7. LEN: Oft werden Konstruktionen wie IF a$="" THEN oder IF b$<>"" THEN verwendet. Ich benutze statt dessen die Befehle IF LEN(a$)=0 THEN und IF LEN(b$)>0 THEN. Sie bewirken genau das gleiche, aber sie haben zwei entscheidende Vorteile: Die Varianten mit LEN werden etwa drei mal so schnell ausgeführt, und außerdem wird die compilierte EXE-Datei kleiner!

8. INCR: Die Befehle x=x+1, INCR x,1 und INCR x bewirken genau das gleiche. Ich verwende nur noch die Variante INCR x, weil das zwar nicht immer, aber sehr oft die schnellste ist. Das Gleiche gilt auch für DECR.

9. SIN: Dieses Beispielprogramm zeichnet mit Hilfe der Winkelfunktionen einen Kreis aus Sternen:

FOR i=0 TO 62
  LOCATE SIN(i/10)*12+13, COS(i/10)*30+40
  PRINT "*";
NEXT

Leider sind die Befehle SIN und COS sehr langsam. Da sich eine Kreisbewegung jedoch immer wiederholt, kann man die zeitraubenden Berechnungen in ein Array auslagern:

DIM s!(0:62), c!(0:62)
FOR i=0 TO 62
  s!(i)=SIN(i/10)            'Winkelfunktionen zwischenspeichern
  c!(i)=COS(i/10)
NEXT
FOR i=0 TO 62
  LOCATE s!(i)*12+13, c!(i)*30+40    'nur noch die Arrays nutzen
  PRINT "*";
NEXT

Hier werden die Winkelfunktionen nur ein einziges Mal beim Programmstart ausgeführt. Das Zeichnen des Kreises geht dadurch nahezu doppelt so schnell! Dieser Trick läßt sich natürlich auch auf viele andere, sich wiederholende Berechnungen anwenden. Der Kreis ist jedoch das klassische Beispiel.

10. MID$(A$,I,1): Oft ist das Verarbeiten langer Strings sinnvoller als ein Array, z.B. wenn eine Binärdatei mit GET$ ausgelesen wird. Der String soll dann Zeichen für Zeichen ausgewertet werden. In meinem Beispiel wird jeder ASCII-Code der Zeichenkette A$ um eins erhöht, aus einem R wird so z.B. ein S.

FOR i=1 TO LEN(a$)
  MID$(a$, i)=CHR$(ASC(MID$(a$, i, 1)) + 1)         'umständlich
NEXT

Die vielen String-Operationen in diesem Beispiel sind leider sehr langsam. Abhilfe schafft nur ein direkter Zugriff auf den Arbeitsspeicher. Das folgende Listing führt zum gleichen Ergebnis, es wird jedoch 10 bis 20 mal schneller ausgeführt!

DEF SEG=STRSEG(a$): p&=STRPTR(a$)    'Segment und Pointeradresse
FOR i=0 TO LEN(a$)-1
  POKE p&+i, PEEK(p&+i) + 1        'mit direktem Speicherzugriff
NEXT: DEF SEG

Ich hoffe, es ist verständlich. Erst wird mit DEF SEG die Segmentadresse neu eingestellt, die Pointeradresse wird in der Variablen P& gespeichert. Die einzelnen Zeichen des Strings A$ werden mit den Befehlen PEEK/POKE indirekt gelesen/geschrieben, indem einfach nur der Inhalt des Strings geändert wird. Es sind keinerlei langwierige Basic-Funktionen nötig, der String wird ohne Umwege direkt im Arbeitsspeicher verändert!

11. LOCATE: Mit Hilfe des Befehls LOCATE kann der blinkende Textcursor ein- und ausgeschaltet werden. Wenn er nicht unbedingt gebraucht wird, sollte er mit dem Befehl LOCATE,,0 unsichtbar gemacht werden, weil die Textausgabe dadurch erheblich beschleunigt wird. Es ist allerdings sinnlos, an jeden einzelnen LOCATE-Befehl den Parameter ,0 anzuhängen. Dadurch wird das Programm sogar verlangsamt.

12. PRINT #1: Wenn man an den Befehl PRINT als letztes Zeichen ein Semikolon anhängt, dann bleibt der Cursor hinter dem ausgegebenen Text stehen. Der Befehl PRINT a$; wird dabei schneller ausgeführt als ein normales PRINT a$. Das hat natürlich nur Sinn, wenn man nicht auf den Zeilenumbruch angewiesen ist.

13. PRINT #2: Mit einem PRINT-Befehl können mehrere Textstücke gleichzeitig ausgegeben werden. Die Verkettung erfolgt entweder in der Form PRINT a$;b$ oder PRINT a$+b$. Ich empfehle generell die erste Methode mit dem Semikolon! Die Variante mit + verbindet erst beide Strings zu einem Ganzen, bevor sie ausgegeben werden. Und das kostet Zeit.

14. PRINT #3: Ein PRINT in der untersten Bildschirmzeile bewirkt, daß der Bildschirminhalt nach oben verschoben wird. Dieses sogenannte Scrolling ist wirklich extrem langsam und sollte darum möglichst vermieden werden!

15. SCREEN: Viele Programme verwenden Grafik, warum auch nicht? Die Planungen dafür sollten aber schon beim Grafikmodus beginnen. Denn: Je einfacher der Grafikmodus, umso weniger Bildpunkte sind auf dem Bildschirm und umso schneller wird das Programm laufen. Wenn es also nicht unbedingt sein muß, dann genügt auch der einfache Modus 7 mit 320 x 200 Punkten und 16 Farben.

16. SCREEN 13: Es mag seltsam klingen, aber es ist wahr. Obwohl der Grafikmodus 13 mit 320 x 200 Bildpunkten und 256 Farben arbeitet, ist er schneller als der Modus 7! Das liegt ganz einfach daran, daß ein Byte des Bildschirmspeichers genau einem Bildpunkt entspricht. Und das bedeutet besonders wenig Verwaltungsaufwand. Leider unterstützt PowerBASIC diesen Grafikmodus bis heute nicht, aber das ist auch kein Problem:

DEFINT a-z: REG 1,&H13
CALL INTERRUPT &H10: DEF SEG=&HA000  'Segmentadresse ist wichtig
FOR y=0 TO 199
  FOR x=0 TO 319
    POKE y*320+x, (x-y)\6+49        'zeichnet direkt im Speicher
  NEXT: POKE$ y*320+290, STRING$(20,8)     'POKE$ ist effektiver
NEXT: i$=INPUT$(1)
DEF SEG: REG 1,&H03: CALL INTERRUPT &H10

Mit Hilfe des Interrupts 10h wird der Grafikmodus eingestellt, der Bildschirm wird dann mit Hilfe von POKE und PEEK direkt beschrieben bzw. gelesen. Achtung! Die beiden Zeilen mit DEF SEG sind extrem wichtig, weil sonst falsche Speicherstellen überschrieben werden!

17. WINDOW: Der Befehl WINDOW zur Definition eines neuen Koordinatensystems sollte niemals verwendet werden! Die Grafikausgabe wird extrem verlangsamt, weil jede Zahl erst in die normalen Bildschirmkoordinaten umgerechnet werden muß.

18. STEP: Die Befehle PSET, LINE, CIRCLE und PUT erlauben mit Hilfe des zusätzlichen Parameters STEP auch relative Koordinaten. Diese Möglichkeit sollte vermieden werden, weil die grafische Bildschirmausgabe dadurch ebenfalls verlangsamt wird. Besser ist es, mit absoluten Werten zu arbeiten und eventuelle Verschiebungen selbst zu berechnen.

19. DRAW: Die Makrosprache DRAW ist für manche experimentelle Dinge durchaus geeignet. Es sollte aber besser darauf verzichtet werden, weil DRAW ein kritisches Laufzeitverhalten aufweist. Das heißt, es funktioniert unter Umständen gar nicht.

So, das soll für's Erste genügen! Dieser Kurs wird in der nächsten Ausgabe mit den Compiler-Befehlen von PowerBASIC fortgesetzt.

mad/os