In diesem Beitrag möchte ich euch ein paar Grundlagen zur Code Optimierung in Arma 3 näher bringen. Dieser Port basiert auf dem Bohemia Interactive Wiki, sowie vor Allem Erfahrung und privaten Tipps. Auch hier beginnen wir zunächst mit den Basics.
Was bedeutet Code Optimierung
Code Optimierung bezeichnet nichts anderes, als die Anpassung eines Codes (Script), um einerseits die Funktion voll zu gewährleisten und andererseits gleichzeitig möglichst performanceschonend und kurz alles zu vereinen. Das geht am besten Schritt für Schritt, wie in diesem Leitfaden beschrieben.
Mach es funktionsfähig!
Das Hauptaugenmerk sollte zunächst darauf liegen, alles zum Laufen zu bringen. Dabei ist zunächst völlig uninteressant, wie schnell das ganze geht oder ob es performancelastig ist.
"Vorzeitige Optimierung ist die Wurzel allen Übels." - Donald Knuth
Mach es schnell!
Da nun unser Script funktionstüchtig ist, sollten wir anfangen es zu optimieren. Dabei beginnen wir zunächst damit, die Ausführung so schnell wie möglich von Statten zu bekommen, um die Ausführung weiterer Scripte nicht zu behindern.
Funktionen
Benötigen wir die gleiche Funktion mehrfach, so macht es Sinn, diese in eine Funkion zu schreiben. Das kann eine bis zu 20-fach schnellere Ausführung nach sich ziehen, was durchaus ausschlaggebend sein kann. Hier kann man mit dem Befehl preprocessFileLineNumbers arbeiten, um eine Funktion zu erstellen. Beispiel: mein_Funktionsname = compile preprocessFileLineNumbers "filename.sqf" So kann diese Funktion später einfach wieder aufgerufen werden (call/spawn).
Länge
Grundsätzlich kann man sagen, dass sich ein Script oder eine Funktion, dass über mehr als 200-300 Codezeilen erstreckt, noch einmal überdacht werden sollte (Faustregel, ist nicht auf alle Fälle anwendbar). Benötige ich wirklich sämtliche Abfragen und Funktionen in diesem Script, oder würde es sich lohnen Teile davon in ein anderes Script auszulagern?
So banal es auch klingen mag: weniger Statements (Befehle) bedeutet gleichzeitig eine schnellere Ausführung. Unnötige Statements zusammenzufassen kann viel bringen. Nachfolgendes Beispiel ist 1,5x so schnell in der Ausführung als die unoptimierte Variante
//unoptimiert
_arr = [1,2];
_one = _arr select 0;
_two = _arr select 1;
_three = _one + _two;
//optimiert
_arr = [1,2];
_three = (_arr select 0) + (_arr select 1);
Variablennamen
Auch die Variablen und deren Namen haben Einfluss auf die Ausführung des Scripts. So verzögern lange Variablennamen (z.B. _playerNameBecauseThePlayerIsImportantAndWeNeedToKnowWhoTheyAre = "John Smith";) die Ausführung deutlich, im Vergleich zu kurzen Variablennamen (z.B. _pN = "John Smith"; - dauert halb so lang etwa). Bei besonders kurzen und nahezu kryptischen Variablen empfiehlt sich, ihre Bedeutung in einem Kommentar zu erläutern.
Bedingungen
Bedingungsabfragen (If-Then Abfragen) können auch schnell zum [lexicon]Performance[/lexicon]-Killer werden. Es werden immer zunächst alle Bedingungen überprüft, bevor die Ausführung gestoppt wird. Bei langen Verkettungen von Bedingungen (z.B. if (_group knowsAbout vehicle _object > 0 && alive _object && canMove _object && count magazines _object > 0) then { ...};) führt dies erneut zu einer Verzögerung, die man durch eine Einfache Verkettung von mehreren Abfragen vermeiden kann
if (_group knowsAbout vehicle _object > 0) then {
if (alive _object && canMove _object && count magazines _object > 0) then {
//Script Code...
};
};
Eine Alternative dazu, wäre der sogenannte "lazy evaluation Syntax" oder zu gut deutsch der "faule Auswertungs-Syntax". Das bedeutet, Argumente werden in geschweifte Klammern gesetzt, was den selben Effekt wie das Beispiel 2 hat. Das würde in diesem Fall etwa so aussehen: if (_group knowsAbout _vehicle object > 0 && {alive _object} && {canMove _object} && {count magazines _object > 0}) then { ...};.
isNil
Eine isNil-Abfrage gibt uns Auskunft darüber, ob eine Variable definiert ist, also einen Wert besitzt, oder nicht (den Wert "nil" besitzt). Und auch hier spielt der Syntax wieder eine wichtige Rolle: Die isNil-Abfrage eines Strings ist schneller als die eines Codes
Mach es schön!
Hier geht es jetzt vor Allem darum, den Code angenehm lesbar, verständlich und gut dokumentiert darzustellen. Sauberer Code ist guter Code.
If Else If Else If Else...
If-Then und If-Then-Else Abfragen sind schon etwas tolles. Nur sind viele davon verschachtelt, verliert man gern den Überblick. In diesem Fall wäre eine Switch-Case angebracht. Und wenn ich einen Fall ausschließen möchte, für den der Code nicht weiter ausgeführt werden soll, dann ist exitWith eine gute Variante (wichtig: exitWith beendet nur das aktuelle Scope. In einer while-Schleife also nur diese zum Beispiel und nicht das ganze Script, wenn es nicht unmittelbar in diesem ausgeführt wird). Hier geht es auch mit um die Geschwindigkeit der Ausführung
if () then {}
//ist schneller als
if () exitWith {}
//ist schneller als
if () then {} else {}
//ist schneller als
switch () do {}
Im folgenden Beispiel verbindet man die Geschwindigkeit des exitWith mit der Übersichtlichkeit des switch
call {
if (cond1) exitWith {//code 1};
if (cond2) exitWith {//code 2};
if (cond3) exitWith {//code 3};
//default code
};
Konstanten
Benötige ich einen Wert, der sich nicht ändert und mehrfach benötigt wird, so mach eine Konstante Sinn. Durch die preProcessor-Funktion #define wird nicht nur eine schnelle und einfache Änderung der Werte, sondern auch eine geringfügige Verbesserung der Ausführungszeit gewährleistet. Hier ein Beispiel:
//unoptimiert
a = _x + 1.053;
b = _y + 1.053;
//optimiert mit Variable
_buffer = 1.053;
a = _x + _buffer;
b = _y + _buffer;
//optimiert mit Konstante (beste Lösung)
#define BUFFER 1.053
_a = _x + BUFFER;
_b = _y + BUFFER;
Alles anzeigen
Schleifen
Schleifen sind ebenfalls ein wichtiges Werkzeug, um diverse Dinge umzusetzen. In Arma 3 gibt es verschiedene Arten von Schleifen (for-Schleife, forEach-Schleife, while-Schleife), die sich auch wieder in ihrer Ausführungsgeschwindigkeit unterscheiden:
//schnellste Varianten
for "_y" from # to # step # do { ... }; //Kann auf gewisse Anzahl Ausführungen begrenzt werden
{ ... } foreach [ ... ]; //führt einen Code für ein Array von Werten aus, der aktuelle Wert kann im Script mit _x bezeichnet werden
//langsamste Varianten
while { expression } do { code };
for [{ ... },{ ... },{ ... }] do { ... };
Wichtig hierbei ist zu wissen, dass eine while-Schleife in einem "Non-scheduled Environment" (aufgerufen durch call) auf 10.000 Ausführungen begrenzt ist und danach die Ausführung abbricht. Dazu aber mehr im folgenden.
Mit einer waitUntil-Abfrage kann man außerdem das Script anhalten, bis eine Bedingung erfüllt ist (nur in einem "scheduled Environment").
_handle = [] execVM "meinTollesScript.sqf";
waitUntil {scriptDone _handle}; //wartet, bis "meinTollesScript.sqf" abgearbeitet ist
//weiter im Code
Threads
Wie bereits ein paar Mal kurz erwähnt kommen wir nun auch zu den Scheduled und Non-Scheduled Environments (die Übersetzungen erspare ich uns, da die lächerlich wären...).
Fangen wir beim Urschleim an: Arma 3 läuft in einem Scheduled Environment. Für Scripts gibt es zwei Möglichkeiten der Ausführung: Scheduled und Non-Scheduled. In welcher Weise der Code ausgeführt wird, wird durch die Art des Aufrufes des Codes bestimmt. So sind spawn, execVM und addAction immer eine Scheduled Environment und z.B. call oder remoteExecCall immer Non-Scheduled. Scheduled Code hat die Funktion, Verzögerungen innerhalb des Scripts zu unterstützen. Allerdings kann die Dauer der Ausführung durch Systemlast und niedrige Frames verzögert werden.
Die 3ms-Laufzeit
Ein Code in einem Scheduled Environment besitzt eine sogenannte "3ms-Laufzeit". Das bedeutet: Der Code wird für 3ms (=0,003s) ausgeführt und danach auf "Standby" gesetzt, um im nächsten Frame weiter ausgeführt zu werden. Hier wird nun die Abhängigkeit zu den FPS klar, da bei 20FPS (Frames per Second/Bilder pro Sekunde) dieser "Standby"-Zustand 50ms lang ist (1Sekunde:Anzahl der Frames). Im Klartext heißt das, dass die Ausführung des Script, sofern es nicht nach 3ms abgeschlossen werden konnte, sich auf unbestimmte Zeit verlängern kann, abhängig von System- und Engineauslastung, weiteren Non-Scheduled Script und den FPS.
Scheduled Scripts laufen immer mit einer gewissen Verzögerung an, bedingt durch die Game-Engine.
Wann erstelle ich einen neuen Thread?
Befehle wie spawn oder execVM beispielsweise erstellen einen neuen Thread in der Schedule des Spiels. Grundsätzlich wird dies angewendet, wenn sleep oder waitUntil Befehle genutzt werden innerhalb eines Scripts. Vermieden werden sollte dies allerdings, wenn das nicht der Fall ist. Eine Scheduled Environment "einfach weil ich kann" kann Scriptverzögerungen von bis zu einer Minute nach sich ziehen und sich deutlich auf die [lexicon]Performance[/lexicon] niederschlagen (gleiches gilt für while-Schleifen ohne Ende).
Verschachtelungen O(n^2)
Ach ja, wer kennt es nicht. Ich benötige verarbeitete Werte eines forEach Arrays und muss diese weiterbearbeiten... also noch ein forEach Array! Oder?
Die Antwort hierauf lautet eindeutig: nein! Je mehr ich ForEach Arrays verschachtle, desto länger dauert die Ausführung. Es gilt: O=(n^2) - oder auf deutsch: Die Ausführungsdauer einer Verschachtelung ist die Ausführungsdauer des ersten ForEach's multipliziert mit dem Quadrat der Anzahl aller verschachtelten ForEach's im Code.
Sind also zwei davon verschachtelt, dauert die Ausführung schon viermal so lang, drei Arrays schon neunmal und so weiter. Also: Verschachtelungen vermeiden, soweit es möglich ist!
Veraltete/Langsame Befehle & Syntax
Jetzt kommen wir zu einer Kategorie, die mir persönlich sehr am Herzen liegt. Immer wieder sehe ich Tutorials mit Code, den man von der Ausführungsgeschwindigkeit deutlich verbessern könnte, würde man ein paar einfache Regeln beachten. Deswegen hier das wichtigste zusammengefasst.
Elemente zu einem Array hinzufügen
//_a = das bestehende Array
//_v = der neue Wert
_a = _a + [_v]
//langsamste Variante
_a set [count _a,_v]
//2x schneller
_a pushBack _v
//4x schneller als Variante 1
Alles anzeigen
Elemente aus einem Array entfernen
ARRAYX set [0, objnull]; //Definition des Arrays
ARRAYX = ARRAYX - [objnull]; //objnull entfernen
systemChat str ARRAYX; //Ausgabe: "[0]"
//schnellere Variante:
_array = [1,2,3] //Definition des Arrays
_array deleteAt 1; //Stelle 1 des Arrays löschen (Nummerierung von vorn, bei 0 beginnend)
systemChat str _array; //Ausgabe: "[1,3]"
Alles anzeigen
Arrays verbinden
arr1 = [1,2,3,4,5,6,7,8,9,0]; arr2 = arr1; arr1 + arr2;
//0.016 ms
arr1 = [1,2,3,4,5,6,7,8,9,0]; arr2 = arr1; arr1 append arr2;
//0.015 ms
//'append' ist schneller als '+' und führt zum gleichen Ziel
Alles anzeigen
Vergleichen von Werten
Hierbei gibt es grundsätzlich zwei Varianten: == und isEqualTo, wobei letzteres der schnellere Befehl in der Ausführung ist. Syntax: Wert1 isEqualTo Wert2 - Ergebnis: true oder false
Vergleichen von Werten nach Typ
Prüfen auf leeres Array
Hierzu gibt es wieder zwei Möglichkeiten: count _arr == 0 (langsam) oder _arr isEqualTo []. Ergebnis ist true oder false
Positionen abfragen
getPosWorld player
//0.0014 ms
getPosASL player
//0.0014 ms
getPosATL player
//0.0015 ms
visiblePositionASL player
//0.0014 ms
visiblePosition player
//0.0048 ms
getPos player
//or
position player
//0.005 ms
Alles anzeigen
Config-Pfad Operatoren
nearEntities vs nearestObjects
Bei Radius-Werten über 100m empfiehlt sich, nearEntities statt nearestObjects zu verwenden, da dies performanter ist. Allerdings ignoriert nearEntities alle zerstörten Objekte und ignoriert statische Objekte und Gebäude.
forEach vs count
Count kann auch an Stelle von ForEach genutzt werden. Beide Befehle gehen den gegebenen Code Schritt für Schritt mit jeder bereitgestellten Variable durch, wobei der aktuelle Wert durch _x wiedergegeben wird. Count hat allerdings keinen _forEachIndex und erwartet im Code true, false oder nil als Ergebnis
{diag_log _x} count [1,2,3,4,5,6,7,8,9];
//ist schneller als
{diag_log _x} forEach [1,2,3,4,5,6,7,8,9];
_someoneIsNear = {_x distance [0,0,0] < 1000} count allUnits > 0;
//ist schneller als
_someoneIsNear = {
if (_x distance [0,0,0] < 1000) exitWith {true};
false
} forEach allUnits;
format/composeText vs '+'
Wenn mehr als 2 Strings zusammengefügt werden, ist format oder composeText schneller in der Ausführung
a = format ["Hi, mein Name ist %1%2","Bob, wie heißt du","?"]
//0.004 ms
a = composeText ["Hi, mein Name ist ","Bob, wie heißt du","?"]
//kein Wert leider...
a = "Hi, mein Name ist " + "Bob, wie heißt du" + "?"
//0.0043 ms
Lange Strings zusammenfügen
Hat man zwei kurze Strings, ist dies mit dem '+'-Operator kein Problem, allerdings bei langen Strings wird dies sehr lange dauern. Die Lösung: Wir verwenden ein Array und wandeln es mit joinString um.
s = ""; for "_i" from 1 to 10000 do {s = s + "123"}; //30000 chars @ 290ms
s = []; for "_i" from 1 to 10000 do {s pushBack "123"}; s = s joinString ""; //30000 chars @ 30ms
select vs if
Wenn zwischen zwei Fällen unterschieden werden soll, so bietet sich in einigen Fällen select an, da es schneller ist
a = "You're " + (["a loser","awesome!"] select true)
//0.0046 ms
a = "You're " + (if true then [{"awesome!"},{"a loser"}])
//0.0054 ms
Im Auto oder nicht?
Um zu prüfen ob sich ein Spieler innerhalb eines Fahrzeuges befindet, gibt es auch eine schnellere Variante:
isNull objectParent player
//0.0013 ms
//schneller als das traditionelle...
vehicle player == player
//0.0022 ms
Alles anzeigen
createVehicle(Local)
Und auch hier kann man sparen. Einfach den Alternativen Syntax nutzen und schon ist das ganze 200x schneller...
_obj = 'Land_Stone_4m_F' createVehicle [0,0,0]; //also createVehicleLocal
_obj setPos (getPos player); //0,03ms (100 Durchläufe)
_obj = 'Land_Stone_4m_F' createVehicle (getPos player); //also createVehicleLocal
_obj setPos (getPos player); //5,9ms (100 Durchläufe)
createSimpleObject vs createVehicle
Sparen ist heute unser Motto! Allerdings ist hier zu bedenken, dass wir uns hier auch die Texturen sparen, und rein das Objekt erstellen und wir den Pfad zum Modell benötigen. Gerade bei VR Blöcken ist dies allerdings eine gute Alternative.
createVehicle ["Land_VR_Shape_01_cube_1m_F",[0,0,0],[],0,"none"];// ~3.5 ms
createSimpleObject ["a3\structures_f_mark\vr\shapes\vr_shape_01_cube_1m_f.p3d",[0,0,0]];// ~0.08 ms
private ["_var"] vs private _var
So unglaubwürdig es klingt, auch hier kann man Zeit einsparen! Lässt man die Klammern weg und weißt der Variable direkt den Wert zu, geht dies doppelt so schnell!
private ["_a", "_b", "_c", "_d"];
_a = 1; _b = 2; _c = 3; _d = 4;
// 0.0040 ms
private _a = 1; private _b = 2; private _c = 3; private _d = 4;
// 0.0023 ms
[] call vs call | [] spawn vs 0 spawn
Ein weiterer Punkt der mir wichtig ist. Wenn für ein spawn oder call Befehl keine Variablen oder Werte übergeben werden müssen, sondern lediglich ein Code aufgerufen wird, sollte man die eckigen Klammern nicht schreiben. Diese verlängern aufrund der Größe der MP-Pakete die Übertragungsdauer und verzögern so minimal, aber dennoch unnötig. Bei spawn muss allerdings ein Wert angegeben werden, im Idealfall einfach '0'. Wird nur eine Variable übergeben, so können die Klammern ausgelassen werden (Variable wird als _this übergeben)
[] call life_fnc_meineFunktion //normal
call life_fnc_meineFunktion //angepasst
_var call life_fnc_meineFunktion //nur eine Variable
[] spawn life_fnc_meineFunktion //normal
0 spawn life_fnc_meineFunktion //angepasst
_var spawn life_fnc_meineFunktion //nur eine Variable
Löse alle Scriptfehler
Der wohl wichtigste und meistgenannte Punkt: Entferne sämtliche Scriptfehler, egal wie banal sie erscheinen! Jeder Scriptfehler verzögert die Ausführung weiterer Scripte und sollte daher dringendst entfernt werden. Mit der Zeit können so heftige Lags und Desyncs entstehen.
systemChat "123"; // ~0.00271ms
systemChat 123; // Syntax-Fehler, ~0.172206ms, 63x langsamer!
Soviel von mir zu diesem Thema. Sollte es Fragen oder Ergänzungen geben, gerne einfach her damit! Ich halte natürlich weiterhin meine Ohren steif und werde diesen Post updaten, sollte ich neue Erkenntnisse auf diesem Gebiet machen.
Bei Fragen zu Befehlen: Bohemia Interactive Community - einfach den Befehl suchen und alle wichtigen Informationen werden angezeigt
Quellennachweis:
Code Optimisation - BI Community Wiki: Code Optimisation - Bohemia Interactive Community
alle wichtigen Verlinkungen im Text