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