Scripting: Performance – Arma Reforger
Lou Montana (talk | contribs) m (Text replacement - "lang="cpp">" to "lang="C#">") |
Lou Montana (talk | contribs) m (Text replacement - "<syntaxhighlight lang="C#">" to "<enforce>") |
||
Line 49: | Line 49: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 68: | Line 68: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
class EnemyManager | class EnemyManager | ||
{ | { | ||
Line 129: | Line 129: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 151: | Line 151: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 185: | Line 185: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 212: | Line 212: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 286: | Line 286: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 314: | Line 314: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 371: | Line 371: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 400: | Line 400: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
Line 492: | Line 492: | ||
! Optimised | ! Optimised | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| < | | <enforce> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| < | | <enforce> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
|} | |} |
Revision as of 19:17, 30 July 2022
Performance is an important aspect of scripting, as poor performance can tank the whole gaming experience, if not the game itself.
- What is tanking performances
- Crazy requests
- Memory 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
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, Single Responsibility Principle).
Example
Original | Optimised |
---|---|
<enforce>
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> |
<enforce>
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> |
Ill-Conceived
Problem
A big array of data is processed at once. Going through all the data is not needed.
Solution
Stop as soon as an acceptable result has been obtained.
Example
Original | Optimised |
---|---|
<enforce>
class PerformanceExample : IEntity { protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects protected int GetNumberOfAliveObjects() { int result = 0; foreach (AliveOrNotObject object : m_aObjects) { if (object.IsAlive()) result++; } return result; } bool IsAnObjectAlive() { return GetNumberOfAliveObjects() > 0; // heavy: goes through all objects to know if -one- is alive } } </syntaxhighlight> |
<enforce>
class PerformanceExample : IEntity { protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects bool IsAnObjectAlive() { foreach (AliveOrNotObject object : m_aObjects) { if (object.IsAlive()) return true; // stops on the first occurence } return false; // goes through all instances to reach false - less likely } } </syntaxhighlight> |
Spread
Problem
A big array of data is processed at once. The calculation itself is not the issue, the amount of items is.
or
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:
Original | Optimised |
---|---|
<enforce>
class PerformanceExample : IEntity { protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects void EOnInit(IEntity owner) { G etGame().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> |
<enforce>
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:
Original | Optimised |
---|---|
<enforce>
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> |
<enforce>
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> |
Immediate Calculation
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
Original | Optimised |
---|---|
<enforce>
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> |
<enforce>
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
- Continue out of the scope if the item is not viable for the operation
High-Frequency Scripts
Problem
OnEachFrame calculations - are they needed?
Specific Solution
- Less resource-hungry calls
- Buffering/caching
High-Performance Request
Problem
Is a 10km raycast really needed?
Solution
- Reconsider the need
- Find a smarter solution
Benchmark
What Should be of Concern
- Non time-critical operations that are made in the same frame
- High RAM usage
What Should Not Be Of Concern
- 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)