Aus anderen Sprachen habe ich den Reflex mitgenommen, Variablen so eng wie möglich zu scopen. In MMBasic geht das mit LOCAL innerhalb von Subs und Funktionen. Sauberer Code, keine Kollisionen mit globalen Namen, alles richtig gemacht, dachte ich. Bis ich mein Spiel profiliert habe und gesehen habe, was mich das in der Hotloop kostet.
Was ich gemessen habe
Beim Profilen meines Spiels ist mir aufgefallen, dass eine SUB, die einmal pro Frame pro Asteroid aufgerufen wird, mehr Zeit verbrennt als ich erwartet hätte. Der Code in dem SUB war einfach, der Aufwand passte nicht zum Ergebnis.
Verdächtigt habe ich am Ende die LOCAL-Deklarationen. Bei jedem Aufruf werden die Variablen frisch reserviert und initialisiert. In einer SUB, die 40 mal pro Frame und damit 2400 mal pro Sekunde läuft, kostet allein das Anlegen.
Globale Arbeits-Arrays statt LOCAL
Mein Umbau lief darauf hinaus, die internen Arbeits-Arrays für Routinen, die in der inneren Schleife liegen, einmal global zu deklarieren und in der SUB einfach zu wiederverwenden.
Vorher:
SUB DrawAsteroid(idx)
LOCAL FLOAT src_x(15), src_y(15)
LOCAL FLOAT tmp_x(15), tmp_y(15)
' ... rotate, translate, draw ...
END SUB
Nachher:
DIM FLOAT da_src_x(15), da_src_y(15)
DIM FLOAT da_tmp_x(15), da_tmp_y(15)
SUB DrawAsteroid(idx)
' nutzt da_src_x, da_src_y, da_tmp_x, da_tmp_y
END SUB
Den Präfix da_ (für DrawAsteroid) verwende ich, damit klar ist, zu welcher Routine die Arbeits-Arrays gehören. Das ersetzt nicht das Scoping vom Compiler, aber für Lesbarkeit und um Kollisionen mit anderen Subs zu vermeiden, reicht es.
Funktionen wie LOCAL FLOAT i, j, k für Schleifenzähler habe ich ebenfalls aus heißen Subs rausgenommen und stattdessen ein paar globale Loop-Variablen, die ich überall wiederverwende.
In meinen Game-Loop-Subs hat das spürbar Frame-Zeit gespart, vor allem in DrawAsteroid und der Particle-Update-Routine. Damit will ich LOCAL nicht generell schlechtmachen — in normalem Code ist es richtig und macht den Code lesbarer. Aber in den drei oder vier Subs, die hunderte Male pro Sekunde durchlaufen, ist die Reservierung pro Aufruf zu teuer, und genau dort lohnt das Auslagern in globale Buffer. Den Rest meines Codes habe ich nicht angefasst, weil sich der Effekt außerhalb der Hot-Paths nicht messen lässt und der Namensraum sonst schnell unübersichtlich wird.