Arma 3 Scripting

Topic in Scripting

Code Optimisation

Also available as Markdown and JSON, or read the original wiki page.

Note

This page is about Code Optimisation. For conception optimisation, see Mission Optimisation.

This article will try to be a general guide about improving your code and its performance.

Rules

In the domain of development, any rule is a rule of thumb. If a rule states for example that it is better that a line of code doesn't go over 80 characters, it doesn't mean that any line must not go over 80 characters; sometimes, the situation needs it. If you have a good structure, do not change your code to enforce a single arbitrary rule. If you break many of them, you may have to change something. Again, this is according to your judgement.

With that being said, here are the three basic rules to get yourself in the clear:

  1. Make it work
  2. Make it readable
  3. Optimise then

Make It Work

Premature optimization is the root of all evil.

Your first goal when coding is to make your code do what you want it does. A good way to reach this objective is to read and getting inspired by other people's code. If you understand it by reading it once, it is probably a good source of inspiration.

Make It Readable

Whether you are cleaning your code or a different person's, you must understand the code without twisting your brain:

Note

See Code Best Practices for more information.

See the following code: _w=[]; {_w pushbackunique primaryweapon _x} foreach((allunits+alldeadmen) select{_x call BIS_fnc_objectside==east});

The same example is far more readable with proper spacing, good variable names and intermediate results: _weaponNames = []; _allUnitsAliveAndDead = allUnits + allDeadMen; _allEastAliveAndDead = _allUnitsAliveAndDead select { _x call BIS_fnc_objectSide == east }; { _weaponNames pushBackUnique primaryWeapon _x } forEach _allEastAliveAndDead;

Constants

Using a hard-coded constant more than once? Use preprocessor directives rather than storing it in memory or cluttering your code with numbers. Such as:

a = _x + 1.053;
b = _y + 1.053;

And

_buffer = 1.053;
a = _x + _buffer;
b = _y + _buffer;

Becomes

#define BUFFER 1.053 // note: no semicolon
_a = _x + BUFFER;
_b = _y + BUFFER;

Note

Using the #define macro only works within the current SQF file. Such definition will not propagate anywhere else.

Global "constants" can be defined via a Description.ext declaration, though, and accessed using getMissionConfigValue command:

Declaration in Description.ext:

var1 = 123;
var2 = "123";
var3[] = { 1, 2, 3 };
rand = __EVAL(random 999);

Usage in code:

hint str getMissionConfigValue "var1"; // 123                                    // 0.0007 ms
hint str getMissionConfigValue "var2"; // "123"                                    // 0.0008 ms
hint str getMissionConfigValue "var3"; // [1,2,3]                                // 0.0017 ms
hint str getMissionConfigValue "rand"; // constant random, for example 935.038    // 0.0007 ms

The getMissionConfigValue command searching Description.ext from top to bottom, it is better for a matter of performance to put all your definitions at the top of the file.

Optimise Then

Once you know what is what, you can understand your code better.

{ _opforUnitUniform = uniform _x; systemChat _opforUnitUniform; } forEach _allOpforUnits;

becomes

{ _uniform = uniform _x; systemChat _uniform; } forEach _allOpforUnits;

Code Optimisation

Important

Please note: Tests and benchmarks were done with the latest Arma 3 version at the time Arma 3 v1.82 with Tank DLC. Game engine performance may have changed since. Benchmark result in milliseconds (ms) is an average for 10000 iterations.

Note

● means you must change your ways today, or with us you will ride…

● means you may want to look at it if you are targeting pure performance

● means the gain is little to insignificant. Going through your code for this replacement is not worth it. You may only consider it for future code.

Scheduled and Unscheduled Environment

There are two code environment types, scheduled and unscheduled.

Note

See Scheduler for more information.

● Variable Assignment

private _myVar = [33, 66] select false;                                            // 0.0013 ms
private _myVar = if (false) then { 33; } else { 66; };                            // 0.0020 ms
private "_myVar"; if (false) then { _myVar = 33; } else { _myVar = 66; };         // 0.0025 ms

● Lazy Evaluation

In SQF the following code will evaluate every single condition, even if one fails:

if (a && b && c) then {};

Even if a returns false (and thus the entire Boolean expression can no longer become true), b and c will still be executed and evaluated regardless.

To avoid this behaviour, one can either imbricate if statements or use lazy evaluation. The latter is done like so:

if (a && { b && { c } }) then {};

In the example above, condition evaluation stops once any condition evaluates to false.

Influence on Semantics

Depending on the arrangement of the curly brackets, lazy evaluation can change the precedence (and therefore the semantics) of a condition. Consider this example:

Expression Condition Value Evaluated Conditions Result
a b c
sqf
a && b || c
false any Boolean true a, b, c true
sqf
a && {b} || {c}
a, c true
sqf
a && {b || {c}}
a false
Performance

Using lazy evaluation is not always the best way as it can both speed up and slow down the code, depending on the current condition being evaluated:

["true  || { false  || {false}}", nil, 100000] call BIS_fnc_codePerformance;    // 0.00080 ms
["true  ||  {false} || {false} ", nil, 100000] call BIS_fnc_codePerformance;    // 0.00105 ms
["false ||   false  ||  false  ", nil, 100000] call BIS_fnc_codePerformance;    // 0.00123 ms
["true  ||   false  ||  false  ", nil, 100000] call BIS_fnc_codePerformance;    // 0.00128 ms
["false ||  {false} || {false} ", nil, 100000] call BIS_fnc_codePerformance;    // 0.00200 ms

● Concatenating Strings

myString = myString + otherString works fine for small strings, however the bigger the string gets the slower the operation becomes:

myString = ""; for "_i" from 1 to 10000 do { myString = myString + "123" };        // 290 ms

The solution is to use a string array that you will concatenate later:

strings = [];
for "_i" from 1 to 10000 do { strings pushBack "123" };
strings = strings joinString "";                                                // 30 ms

● Array Manipulation

Add Elements

New commands append and pushBack hold the best score.

_array = [0,1,2,3]; _array append [4,5,6];                                        // 0.0020 ms
_array = [0,1,2,3]; _array = _array + [4,5,6];                                    // 0.0023 ms
_array = [0,1,2,3]; { _array set [count _array, _x]; } forEach [4,5,6];            // 0.0080 ms
_array = [0,1,2,3]; _array pushBack 4;                                            // 0.0016 ms
_array = [0,1,2,3]; _array = _array + [4];                                        // 0.0021 ms
_array = [0,1,2,3]; _array set [count _array, _x];                                 // 0.0022 ms
Iterate Elements

If _x is not required, for is twice as fast as forEach - otherwise, use forEach (see results below).

private _list = allUnits;                                                                // 64 units                256 units

// if the amount of loops is known
for "_i" from 0 to 63 do {};                                                            // 0.0120 ms            0.0460 ms
for "_i" from 0 to count _list -1 do {};                                                // 0.0170 ms            0.0480 ms
for "_i" from 0 to count _list -1 do { private _x = _list select _i };                    // 0.0500 ms            0.1896 ms <
for "_i" from 0 to count _list -1 do { private _x = _list select _i; _x setDamage 0 };    // 0.107 ms                0.39 ms

{} forEach _list;                                                                        // 0.0250 ms            0.098 ms
{ _x setDamage 0 } forEach _list;                                                        // 0.0770 ms            0.312 ms

_list apply {};                                                                            // 0.0250 ms            0.098 ms
_list apply { _x setDamage 0 };                                                            // 0.0770 ms            0.312 ms

private _i = 0;
while { _i < 64 } do { _i = _i + 1; };                                                    // 0.048 ms                0.200 ms

// counts array every loop
private _i = 0;
while { _i < count _list } do { _i = _i + 1; };                                            // 0.062 ms                0.247 ms

private _i = 0;
while { _i < 64 } do { private _x = _list select _i; _i = _i + 1; };                    // 0.086 ms                0.42 ms
Remove Elements
_array = [0,1,2,3]; _array deleteAt 0;                                                                    // 0.0015 ms
_array = [0,1,2,3]; _array set [0, objNull]; _array = _array - [objNull];                                // 0.0038 ms
_array = [0,1,2,3]; _array deleteRange [1, 2];                                                            // 0.0018 ms
_array = [0,1,2,3]; { _array set [_x, objNull] } forEach [1,2]; _array = _array - [objNull];            // 0.0078 ms

● Multiplayer Recommendations

Note

See Multiplayer Scripting for more information.

Equivalent Commands Performance

● call

call without arguments is faster than call with arguments:

call {};                                // 0.0007 ms
123 call {};                            // 0.0013 ms

Since the variables defined in the parent scope will be available in the called child scope, it could be possible to speed up the code by avoiding passing arguments all together, for example writing:

player addEventHandler ["HandleDamage", { call my_fnc_damage }];

instead of:

player addEventHandler ["HandleDamage", { _this call my_fnc_damage }];

● execVM and call

Important

Using execVM multiple times make the game read the file and recompile it every time.

If you use the script more than once, store its code in a variable or better, make it a Function!

// myFile.sqf is an EMPTY file
private _myFunction = compile preprocessFileLineNumbers "myFile.sqf"; // compile time is done only once
call _myFunction;                // 0.0009 ms
execVM "myFile.sqf";            // 0.275 ms

// myFile.sqf is BIS_fnc_showRespawnMenu
private _myFunction = compile preprocessFileLineNumbers "myFile.sqf"; // compile time is done only once
["close"] call _myFunction;        // 0.0056 ms
["close"] execVM "myFile.sqf";    // 0.506 ms

● loadFile, preprocessFile and preprocessFileLineNumbers

// myFile.sqf is an empty file
loadFile "myFile.sqf";                    // 0.219 ms
preprocessFile "myFile.sqf";            // 0.353 ms
preprocessFileLineNumbers "myFile.sqf";    // 0.355 ms
// myFile.sqf is BIS_fnc_showRespawnMenu
loadFile "myFile.sqf";                    // 0.3516 ms
preprocessFile "myFile.sqf";            // 2.75 ms
preprocessFileLineNumbers "myFile.sqf"; // 2.73 ms
// myFile.sqf is a missing file
loadFile "myFile.sqf";                    // 0.692 ms
preprocessFile "myFile.sqf";            // 0.6225 ms
preprocessFileLineNumbers "myFile.sqf";    // 0.6225 ms

Important

The comparison of loadFile with preprocessFile* is not exactly fair as loadFile doesn't preprocess the file's content.

On the other hand, the loaded file cannot contain any // or /* */ comments nor any preprocessor instructions (including debug information like line numbers or file information).

● if

if (condition) then { /* thenCode */ };                                    // 0.0011 ms
if (condition) exitWith { /* exitCode */ };                                // 0.0014 ms
if (condition) then { /* thenCode */ } else { /* elseCode */ };            // 0.0015 ms
if (condition) then [{ /* thenCode */ }, { /* elseCode */ }];            // 0.0016 ms

● if and select

Use [array] select boolean instead of the lazy-evaluated if.

_result = ["false result", "true result"] select true;                    // 0.0011 ms
_result = if (true) then { "true result"; } else { "false result"; };    // 0.0017 ms

● if and switch

_result = call {
    if (false) exitWith {};
    if (false) exitWith {};
    if (true)  exitWith {};
    if (false) exitWith {};
    if (false) exitWith {};
};                            // 0.0032 ms
_result = switch (true) do {
    case (false): {};
    case (false): {};
    case (true) : {};
    case (false): {};
    case (false): {};
};                            // 0.0047 ms

● if else and switch

_mode = "killed";

switch _mode do {
    case "init": {};
    case "killed": {};
    case "respawned": {};
};                            // 0.0019 ms
_mode = "killed";

if (_mode == "init") then {} else {
    if (_mode == "killed") then {} else {
        if (_mode == "respawned") then {};
    };
};                            // 0.0019 ms

● in vs find

// String search
"bar" in "foobar"                                    // 0.0008 ms
"foobar" find "bar" > -1                            // 0.0012 ms
// array search - case-sensitive
"bar" in ["foo", "Bar", "bar", "BAR"];                // 0.0012 ms
["foo", "Bar", "bar", "BAR"] find "bar" > -1;        // 0.0016 ms

● for

The for..from..to..do is twice as fast as its alternative syntax, for..do.

for "_i" from 0 to 10 do { /* forCode */ };                                        // 0.015 ms
for [{ _i = 0 }, { _i < 100 }, { _i = _i + 1 }] do { /* forCode */ };            // 0.030 ms

● forEach vs count vs findIf

Both forEach and count commands will step through all the array elements and both commands will contain reference to current element with the _x variable. However, count loop is a little faster than forEach loop, but it does not benefit from the _forEachIndex variable.

Also, there is a limitation as the code inside count expects Boolean or Nothing while the command itself returns Number. This limitation is very important if you try to replace your forEach by count. If you have to add an extra true/false/nil at the end to make count work, it will be slower than the forEach equivalent.

{ diag_log _x } count   [1,2,3,4,5];                                            // 0.082 ms
{ diag_log _x } forEach [1,2,3,4,5];                                            // 0.083 ms
// with an empty array
_someoneIsNear = (allUnits findIf { _x  distance [0,0,0] < 1000 }) != -1;        // 0.0046 ms
_someoneIsNear = { _x distance [0,0,0] < 1000 } count allUnits > 0;                // 0.0047 ms <
_someoneIsNear = {
    if (_x distance [0,0,0] < 1000) exitWith { true };
    false
} forEach allUnits;                                                                // 0.0060 ms
// with a 30 items array
_someoneIsNear = (allUnits findIf { _x  distance [0,0,0] < 1000 }) != -1;        // 0.0275 ms
_someoneIsNear = { _x distance [0,0,0] < 1000 } count allUnits > 0;                // 0.0645 ms <
_someoneIsNear = {
    if (_x distance [0,0,0] < 1000) exitWith { true };
    false
} forEach allUnits;                                                                // 0.0390 ms

● findIf

findIf stops array iteration as soon as the condition is met.

[0,1,2,3,4,5,6,7,8,9] findIf { _x == 2 };                                        // 0.0050 ms
{ if (_x == 2) exitWith { _forEachIndex; }; } forEach [0,1,2,3,4,5,6,7,8,9];    // 0.0078 ms
_quantity = { _x == 2 } count [0,1,2,3,4,5,6,7,8,9];                            // 0.0114 ms

● format vs str

str 33;                // 0.0016 ms
format ["%1", 33];    // 0.0022 ms

● + vs format vs joinString

non-String data:

[33, 45, 78] joinString "";                // 0.0052 ms - no length limit
format ["%1%2%3", 33, 45, 78];            // 0.0054 ms - limited to ~8Kb
str 33 + str 45 + str 78;                // 0.0059 ms - no length limit

String data:

["str1", "str2", "str3"] joinString "";        // 0.0015 ms - no length limit
format ["%1%2%3", "str1", "str2", "str3"];    // 0.0015 ms - limited to ~8Kb
"str1" + "str2" + "str3";                    // 0.0012 ms - no length limit

● private

Direct declaration (private _var = value) is faster than declaring then assigning the variable.

private _a = 1;
private _b = 2;
private _c = 3;
private _d = 4;
// 0.0023 ms
private ["_a", "_b", "_c", "_d"];
_a = 1;
_b = 2;
_c = 3;
_d = 4;
// 0.0040 ms

However, if you have to reuse the same variable in a loop, external declaration is faster.

The reason behind this is that a declaration in the loop will create, assign and delete the variable in each loop.

An external declaration creates the variable only once and the loop only assigns the value.

private ["_a", "_b", "_c", "_d"];
for "_i" from 1 to 10 do
{
    _a = 1; _b = 2; _c = 3; _d = 4;
};
// 0.0195 ms
for "_i" from 1 to 10 do
{
    private _a = 1; private _b = 2; private _c = 3; private _d = 4;
};
// 0.0235 ms

● isNil

isNil "varName";                    // 0.0007 ms
isNil { varName };                    // 0.0012 ms

● isEqualType and typeName

isEqualType is much faster than typeName

"string" isEqualType 33;            // 0.0006 ms
typeName "string" == typeName 33;    // 0.0018 ms

● isEqualTo and count

// with a items array
allUnits isEqualTo [];                // 0.0040 ms
count allUnits == 0;                // 0.0043 ms

● select and param

[1,2,3] select 0;                    // 0.0008 ms
[1,2,3] param [0];                    // 0.0011 ms

● createSimpleObject vs createVehicle

// createSimpleObject is over 43× faster than createVehicle!
deleteVehicle createSimpleObject ["a3\structures_f_mark\vr\shapes\vr_shape_01_cube_1m_f.p3d", [0,0,0]];    // ~0.08 ms
deleteVehicle createSimpleObject ["Land_VR_Shape_01_cube_1m_F", [0,0,0]];                                // ~3.2 ms
deleteVehicle createVehicle ["Land_VR_Shape_01_cube_1m_F", [0,0,0], [], 0, "CAN_COLLIDE"];                // ~2.7 ms
deleteVehicle createVehicle ["Land_VR_Shape_01_cube_1m_F", [0,0,0], [], 0, "NONE"];                        // ~78 ms

● objectParent and vehicle

isNull objectParent player;            // 0.0013 ms
vehicle player == player;            // 0.0022 ms

● nearEntities and nearestObjects

nearEntities is much faster than nearestObjects given on range and amount of objects within the given range. If range is over 100 meters it is highly recommended to use nearEntities over nearestObjects.

// tested with a NATO rifle squad amongst solar power plant panels on Altis at coordinates [20762,15837]
getPosATL player nearEntities [["CAManBase"], 50];        // 0.0075 ms
nearestObjects [getPosATL player, ["CAManBase"], 50];    // 0.0145 ms

Important

nearEntities only searches for alive objects and on-foot soldiers.

In-vehicle units, killed units, destroyed vehicles, static objects and buildings will be ignored.

● Global Variables vs Local Variables

If you need to use global variable repeatedly in a loop, copy its value to local variable and use local variable instead:

SomeGlobalVariable = [123];
for "_i" from 1 to 100 do
{
    SomeGlobalVariable select 0;
};
// 0.13 ms

is noticeably slower than

SomeGlobalVariable = [123];
private _var = SomeGlobalVariable;
for "_i" from 1 to 100 do
{
    _var select 0;
};
// 0.08 ms

● Config path delimiter

>> is slightly faster than / when used in config path with configFile or missionConfigFile.

configFile >> "CfgVehicles";        // 0.0019 ms
configFile  / "CfgVehicles";        // 0.0023 ms

Note

A config path can be stored in a variable for later use, saving CPU time: _cfgVehicles = configFile >> "CfgVehicles".

● getPos* and setPos*

getPosWorld                            // 0.0015 ms
getPosASL                            // 0.0016 ms
getPosATL                            // 0.0016 ms
getPosASLW                            // 0.0023 ms
getPos                                // 0.0030-0.0300 ms; performance depends on where this command is used - see its documentation
position                            // same as getPos
getPosVisual                        // same as getPos
visiblePosition                        // same as getPos
setPosWorld                            // 0.0060 ms
setPosASL                            // 0.0060 ms
setPosATL                            // 0.0060 ms
setPos                                // 0.0063 ms
setPosASLW                            // 0.0068 ms
setVehiclePosition                    // 0.0077 ms with "CAN_COLLIDE"
                                    // 0.0390 ms with "NONE"

● toLower/toUpper vs toLowerANSI/toUpperANSI

// _myString is a 100 chars "aAaAaA(…)" string
toLowerANSI _myString;                // 0.0006 ms
toLower _myString;                    // 0.0016 ms

toUpperANSI _myString;                // 0.0006 ms
toUpper _myString;                    // 0.0016 ms

Equivalent Data Structures Performance

Key-Value Data Structures

private _hashMap = createHashMapFromArray [["id", 123], ["name", "player name"], ["unit", player]]; // since Arma 3 v2.02
private _goodFormat = [["id", "name", "unit"], [123, "player name", player]];
private _slowFormat = [["id", 123], ["name", "player name"], ["unit", player]];
private _name = _hashMap get "name"; // 0.0018ms

// this takes 0.0038ms:
private _index = _goodFormat select 0 find "name"; // loop in engine
private _name = _goodFormat select 1 select _index;

// this takes 0.0116ms:
private _index = _slowFormat findIf { _x select 0 == "name" }; // loop in script
private _name = _slowFormat select _index select 1;

Note

Database Functions use a slow format.

Conversion From Earlier Versions

Each iteration of Bohemia games (Operation Flashpoint, Armed Assault, Arma 2, Take On Helicopters, Arma 3) brought their own new commands, especially Arma 2 and Arma 3.

For that, if you are converting scripts from older versions of the engine, the following aspects should be reviewed.

Loops

Array Operations

result = (arrayOfNumbers select { _x % 2 == 0 });    // 1.55 ms
result = [];
{
    if (_x % 2 == 0) then { result pushBack _x; };
} forEach arrayOfNumbers;                            // 2.57 ms

Vector Operations

private _vector = [102, 687, 1543];
private _factor = 53;

_vector vectorMultiply _factor;                    // 0.0028 ms
[_vector, _factor] call BIS_fnc_vectorMultiply;    // 0.0145 ms

_vector vectorMultiply (1 / _factor);            // 0.003 ms - but beware of 0 divisor
[_vector, _factor] call BIS_fnc_vectorDivide;    // 0.017 ms

String Operations

String manipulation has been simplified with the following commands:

Number Operations

Type Comparison

Multiplayer

Note

See also Multiplayer Scripting.

Parameters