Scripting Performance – Arma Reforger

From Bohemia Interactive Community
Jump to navigation Jump to search
m (Text replacement - "lang="cpp">" to "lang="C#">")
(Add redirection)
Tag: New redirect
 
Line 1: Line 1:
{{TOC|side||3}}
#REDIRECT [[Arma Reforger:Scripting: Performance]]
Performance is an important aspect of scripting, as poor performance can tank the whole gaming experience, if not the game itself.
 
* what is tanking perfs
** crazy requests
** RAM saturation
** redundant code operations
* how to benchmark/identify what slows down things
** what is acceptable?
** what is not?
 
 
== Performance Enemies ==
 
Many if not all of the usual performance issues can see some of these questions asked:
 
* is it needed?
* is it needed ''there?''
* is it needed ''that much?''
* is it needed ''immediately?''
* is it needed ''that frequently?''
 
and the respective solutions to positive replies to these questions would be:
 
* don't do it: quite straightforward, can mean to remove the code or to replace it with a simpler code
* don't do it ''there'': the concerned class might be dealing with responsibilities that are out of its scope
* don't do it ''that much'': the structure can be rethought in order to have fewer operations to be executed
* don't do it ''in one frame'': the calculation can be spread over multiple frames and result delayed and cached
* don't do it ''that frequently'': the code can be set to run every X seconds, result being delayed and cached until the next iteration
 
=== Not Needed ===
 
==== Problem ====
Partially unneeded calculation is done.
 
==== Solution ====
* Remove the unneeded code
* split the code to only do needed processing in each case
 
==== Example ====
{| class="wikitable" style="width: 100%"
! Original
! Optimised
|-
| <syntaxhighlight lang="C#">
// soon™
</syntaxhighlight>
| <syntaxhighlight lang="C#">
// soon™
</syntaxhighlight>
|}
 
=== Misplaced ===
 
==== Problem ====
A calculation/storage is done on every instance whereas calculation could be centralised.
 
==== Solution ====
Move the responsibility and the data to a dedicated class ('''S''' from SOLID, '''S'''ingle '''R'''esponsibility '''P'''rinciple).
 
==== Example ====
{| class="wikitable" style="width: 100%"
! style="width: 50%" | Original
! Optimised
|- style="vertical-align: top"
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref PotentialTarget> m_aObjects = { /* ... */ }; // big list of objects
 
protected array<ref PotentialTarget> GetTargets()
{
array <ref PotentialTarget> result = {};
foreach (PotentialTarget object : m_aObjects)
{
if (object.IsEnemyTo(this.GetFaction()))
{
result.Insert(object);
}
}
return result;
}
}
</syntaxhighlight>
| <syntaxhighlight lang="C#">
class EnemyManager
{
protected ref map<Faction, ref array<ref PotentialTarget>> m_mTargetMap;
 
void EnemyManager(array<ref PotentialTarget> potentialTargets)
{
m_mTargetMap = new map<Faction, ref array<ref PotentialTarget>>();
 
array<ref PotentialTarget> factionTargets;
foreach (Faction faction : Faction.GetAllFactions())
{
factionTargets = {};
foreach (PotentialTarget potentialTarget : potentialTargets)
{
if (potentialTarget.IsEnemyTo(faction))
{
factionTargets.Insert(potentialTarget);
}
}
m_mTargetMap.Set(faction, factionTargets);
}
}
 
array<ref PotentialTarget> GetTargets(Faction faction)
{
return m_mTargetMap.Get(faction);
}
}
 
class PerformanceExample : IEntity
{
protected ref m_EnemyManager;
 
void PerformanceExample(notnull EnemyManager enemyManager)
{
m_EnemyManager = enemyManager;
}
 
protected array<ref PotentialTarget> GetTargets()
{
return m_EnemyManager.GetTargets(this.GetFaction());
}
}
</syntaxhighlight>
|}
 
=== Not Spread ===
 
==== Problem ====
A big array of data is processed at once. The calculation itself is not the issue, the amount of items is.<br>
'''or'''<br>
A heavy calculation is done, and only one such operation should happen per frame.
 
==== Solution ====
Spread the calculation over multiple frames.
 
==== Example 1 ====
Array size issue
{| class="wikitable" style="width: 100%"
! style="width: 50%" | Original
! Optimised
|- style="vertical-align: top"
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
 
void EOnInit(IEntity owner)
{
GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
}
 
int GetNumberOfAliveObjects()
{
int result = 0;
foreach (AliveOrNotObject object : m_aObjects)
{
if (object.IsAlive())
{
result++;
}
}
return result;
}
 
void PrintResult()
{
Print("Alive objects = " + GetNumberOfAliveObjects());
}
}
</syntaxhighlight>
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
protected ref array<ref AliveOrNotObject> m_aObjectsToBeProcessed = {};
protected bool m_bIsCalculating;
protected int m_iTempResult;
protected int m_iAliveCountBuffer;
 
void EOnInit(IEntity owner)
{
Calculate();
GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
}
 
void EOnFrame(IEntity owner, float timeSlice)
{
if (m_bIsCalculating)
{
Calculate();
}
 
// other frame things
// ...
}
 
void Calculate()
{
if (m_aObjectsToBeProcessed.Count() < 1)
{
m_bIsCalculating = true;
m_iTempResult = 0;
m_aObjectsToBeProcessed.Copy(m_aObjects);
}
 
// max 10k items slice
for (int i = 0, int maxCount = Math.Min(m_aObjectsToBeProcessed.Count(), 10000); i < maxCount; i++)
{
AliveOrNotObject object = m_aObjectsToBeProcessed[0]; // always @ index 0
// for we remove the first item later
if (object.IsAlive())
{
m_iTempResult++;
}
m_aObjectsToBeProcessed.Remove(0);
}
 
if (m_aObjectsToBeProcessed.Count() < 1)
{
m_iAliveCountBuffer = m_iTempResult;
GetGame().GetCallqueue().CallLater(Calculate, 1000); // restart calculation in 1 second
m_bIsCalculating = false;
}
}
 
int GetNumberOfAliveObjects()
{
return m_iAliveCountBuffer;
}
 
void PrintResult()
{
Print("Alive objects = " + GetNumberOfAliveObjects());
}
}
</syntaxhighlight>
|}
 
==== Example 2 ====
Heavy calculation issue
 
{| class="wikitable" style="width: 100%"
! style="width: 50%" | Original
! Optimised
|- style="vertical-align: top"
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref Player> m_aPlayers = { /* ... */ };
protected ref map<ref Player, bool> m_mCastResult;
protected BaseWorld world;
 
void PerformanceExample()
{
m_mCastResult = new map<ref Player, bool>();
world = GetGame().GetWorld();
}
 
void CheckLineOfSight()
{
float result;
TraceParam traceParam;
foreach (Player player : m_aPlayers)
{
traceParam = new TraceParam();
traceParam.Start = this.Position();
traceParam.End = player.Position();
result = world.TraceMove(traceParam, null); // a Trace is a very expensive method
m_mCastResult.Set(player, result == 1);
}
}
}
</syntaxhighlight>
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref Player> m_aPlayers = { /* ... */ };
protected ref map<ref Player, bool> m_mCastResult;
protected BaseWorld world;
protected int m_iIndex = -1;
protected bool m_bIsCalculating;
 
void PerformanceExample()
{
m_mCastResult = new map<ref Player, bool>();
world = GetGame().GetWorld();
}
 
void EOnFrame(IEntity owner, float timeSlice)
{
if (m_bIsCalculating)
{
CheckLineOfSight();
}
}
 
void CheckLineOfSight()
{
m_iIndex++;
m_bIsCalculating = m_iIndex < m_aPlayers.Count();
if (!m_bIsCalculating)
{
m_iIndex = -1;
return;
}
 
Player player = m_aPlayers[m_iIndex];
TraceParam traceParam = new TraceParam();
traceParam.Start = this.Position();
traceParam.End = player.Position();
float result = world.TraceMove(traceParam, null); // one Trace per frame
m_mCastResult.Set(player, result == 1);
}
}
</syntaxhighlight>
|}
 
=== Not Delayed ===
 
==== Problem ====
A heavy operation is done frequently.
 
==== Solution ====
Do the heavy operation once, store its result in a member variable and accessible through a getter.
 
==== Example ====
{| class="wikitable" style="width: 100%"
! style="width: 50%" | Original
! Optimised
|- style="vertical-align: top"
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
 
void EOnInit(IEntity owner)
{
GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
}
 
int GetNumberOfAliveObjects()
{
int result = 0;
foreach (AliveOrNotObject object : m_aObjects)
{
if (object.IsAlive())
{
result++;
}
}
return result;
}
 
void PrintResult()
{
Print("Alive objects = " + GetNumberOfAliveObjects());
}
}
</syntaxhighlight>
| <syntaxhighlight lang="C#">
class PerformanceExample : IEntity
{
protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
protected int m_iAliveCountBuffer;
 
void EOnInit(IEntity owner)
{
GetGame().GetCallqueue().CallLater(Calculate, 1000, true); // calculate only every second
GetGame().GetCallqueue().CallLater(PrintResult, 250, true); // displays result every 1/4 second
}
 
void Calculate()
{
int result = 0;
foreach (AliveOrNotObject object : m_aObjects)
{
if (object.IsAlive())
{
result++;
}
}
m_iAliveCountBuffer = result;
}
 
int GetNumberOfAliveObjects()
{
return m_iAliveCountBuffer;
}
 
void PrintResult()
{
Print("Alive objects = " + GetNumberOfAliveObjects());
}
}
</syntaxhighlight>
|}
 
 
== Specific Issues ==
 
=== Big Foreach ===
 
==== Problem ====
Going through one big list of items in one operation.
 
==== Specific Solution ====
 
* use a smaller list by being more precise
* {{hl|continue}} out of the scope if the item is not viable for the operation
 
=== High-Frequency Scripts ===
 
==== Problem ====
OnEachFrame thingies - is it needed?
 
==== Specific Solution ====
 
* less resource-hungry calls
* buffering/caching
 
=== High-Performance Request ===
 
==== Problem ====
Is a 10km raycast really needed?
 
==== Specific Solution ====
 
* reconsider the need
* find a smarter solution
 
 
== Benchmark ==
 
=== Valid Concerns ===
 
* non time-critical operations that are made in the same frame
* high RAM usage
 
=== No Concerns ===
 
* Normal operations that are required and that one cannot do without (yes, check if the player is alive before performing action, otherwise the feature breaks)
* one-time required big operation (spawning a base = data loading, no can do anything about it)
 
 
{{GameCategory|armaR|Modding|Tutorials|Scripting}}

Latest revision as of 15:22, 30 May 2022