Scripting: Performance – Arma Reforger
Lou Montana (talk | contribs) m (Text replacement - "<syntaxhighlight lang="C#">" to "<enforce>") |
Lou Montana (talk | contribs) m (Fix examples code) |
||
(3 intermediate revisions by the same user not shown) | |||
Line 42: | Line 42: | ||
==== Solution ==== | ==== Solution ==== | ||
Move the responsibility and the data to a dedicated class ('''S''' from {{ | Move the responsibility and the data to a dedicated class ('''S''' from {{Link|https://en.wikipedia.org/wiki/SOLID}}, '''S'''ingle '''R'''esponsibility '''P'''rinciple). | ||
==== Example ==== | ==== Example ==== | ||
Line 67: | Line 67: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
class EnemyManager | class EnemyManager | ||
Line 100: | Line 100: | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
{ | { | ||
protected ref m_EnemyManager; | protected ref EnemyManager m_EnemyManager; | ||
void PerformanceExample(notnull EnemyManager enemyManager) | void PerformanceExample(notnull EnemyManager enemyManager) | ||
Line 112: | Line 112: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
|} | |} | ||
Line 150: | Line 150: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
Line 166: | Line 166: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
|} | |} | ||
Line 192: | Line 192: | ||
void EOnInit(IEntity owner) | void EOnInit(IEntity owner) | ||
{ | { | ||
GetGame().GetCallqueue().CallLater(PrintResult, 250, true); | |||
} | } | ||
Line 211: | Line 211: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
Line 240: | Line 240: | ||
void Calculate() | void Calculate() | ||
{ | { | ||
int objCount = m_aObjectsToBeProcessed.Count(); | |||
if (objCount < 1) | |||
{ | { | ||
m_bIsCalculating = true; | m_bIsCalculating = true; | ||
Line 248: | Line 249: | ||
// max 10k items slice | // max 10k items slice | ||
for (int i = 0, | for (int i = 0, maxCount = Math.Min(objCount, 10000); i < maxCount; i++) | ||
{ | { | ||
AliveOrNotObject object = m_aObjectsToBeProcessed[0]; // always @ index 0 | AliveOrNotObject object = m_aObjectsToBeProcessed[0]; // always @ index 0 | ||
// for we remove the first item later | // for we remove the first item later | ||
if (object.IsAlive()) | if (object.IsAlive()) | ||
m_iTempResult++; | m_iTempResult++; | ||
m_aObjectsToBeProcessed.Remove(0); | m_aObjectsToBeProcessed.Remove(0); | ||
objCount--; | |||
} | } | ||
if ( | if (objCount < 1) | ||
{ | { | ||
m_iAliveCountBuffer = m_iTempResult; | m_iAliveCountBuffer = m_iTempResult; | ||
Line 277: | Line 278: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
|} | |} | ||
Line 291: | Line 292: | ||
protected ref array<ref Player> m_aPlayers = { /* ... */ }; | protected ref array<ref Player> m_aPlayers = { /* ... */ }; | ||
protected ref map<ref Player, bool> m_mCastResult; | protected ref map<ref Player, bool> m_mCastResult; | ||
protected BaseWorld | protected BaseWorld m_World; | ||
void PerformanceExample() | void PerformanceExample() | ||
{ | { | ||
m_mCastResult = new map<ref Player, bool>(); | m_mCastResult = new map<ref Player, bool>(); | ||
m_World = GetGame().GetWorld(); | |||
} | } | ||
Line 308: | Line 309: | ||
traceParam.Start = this.Position(); | traceParam.Start = this.Position(); | ||
traceParam.End = player.Position(); | traceParam.End = player.Position(); | ||
result = | result = m_World.TraceMove(traceParam, null); // a Trace is a very expensive method | ||
m_mCastResult.Set(player, result == 1); | m_mCastResult.Set(player, result == 1); | ||
} | } | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
Line 319: | Line 320: | ||
protected ref array<ref Player> m_aPlayers = { /* ... */ }; | protected ref array<ref Player> m_aPlayers = { /* ... */ }; | ||
protected ref map<ref Player, bool> m_mCastResult; | protected ref map<ref Player, bool> m_mCastResult; | ||
protected BaseWorld | protected BaseWorld m_World; | ||
protected int m_iIndex = -1; | protected int m_iIndex = -1; | ||
protected bool m_bIsCalculating; | protected bool m_bIsCalculating; | ||
Line 326: | Line 327: | ||
{ | { | ||
m_mCastResult = new map<ref Player, bool>(); | m_mCastResult = new map<ref Player, bool>(); | ||
m_World = GetGame().GetWorld(); | |||
} | } | ||
Line 332: | Line 333: | ||
{ | { | ||
if (m_bIsCalculating) | if (m_bIsCalculating) | ||
CheckLineOfSight(); | CheckLineOfSight(); | ||
} | } | ||
Line 351: | Line 350: | ||
traceParam.Start = this.Position(); | traceParam.Start = this.Position(); | ||
traceParam.End = player.Position(); | traceParam.End = player.Position(); | ||
float result = | float result = m_World.TraceMove(traceParam, null); // one Trace per frame | ||
m_mCastResult.Set(player, result == 1); | m_mCastResult.Set(player, result == 1); | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
|} | |} | ||
Line 399: | Line 398: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
class PerformanceExample : IEntity | class PerformanceExample : IEntity | ||
Line 435: | Line 434: | ||
} | } | ||
} | } | ||
</ | </enforce> | ||
|} | |} | ||
Line 471: | Line 470: | ||
== Benchmark == | == Benchmark == | ||
=== What Should | === What Should Be Of Concern === | ||
* Non time-critical operations that are made in the same frame | * Non time-critical operations that are made in the same frame | ||
Line 493: | Line 492: | ||
|- style="vertical-align: top" | |- style="vertical-align: top" | ||
| <enforce> | | <enforce> | ||
</ | </enforce> | ||
| <enforce> | | <enforce> | ||
</ | </enforce> | ||
|} | |} | ||
--> | --> |
Latest revision as of 17:41, 6 July 2023
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 |
---|---|
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;
}
} |
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 EnemyManager m_EnemyManager;
void PerformanceExample(notnull EnemyManager enemyManager)
{
m_EnemyManager = enemyManager;
}
protected array<ref PotentialTarget> GetTargets()
{
return m_EnemyManager.GetTargets(this.GetFaction());
}
} |
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 |
---|---|
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
}
} |
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
}
} |
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 |
---|---|
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());
}
} |
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()
{
int objCount = m_aObjectsToBeProcessed.Count();
if (objCount < 1)
{
m_bIsCalculating = true;
m_iTempResult = 0;
m_aObjectsToBeProcessed.Copy(m_aObjects);
}
// max 10k items slice
for (int i = 0, maxCount = Math.Min(objCount, 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);
objCount--;
}
if (objCount < 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());
}
} |
Example 2
Heavy calculation issue:
Original | Optimised |
---|---|
class PerformanceExample : IEntity
{
protected ref array<ref Player> m_aPlayers = { /* ... */ };
protected ref map<ref Player, bool> m_mCastResult;
protected BaseWorld m_World;
void PerformanceExample()
{
m_mCastResult = new map<ref Player, bool>();
m_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 = m_World.TraceMove(traceParam, null); // a Trace is a very expensive method
m_mCastResult.Set(player, result == 1);
}
}
} |
class PerformanceExample : IEntity
{
protected ref array<ref Player> m_aPlayers = { /* ... */ };
protected ref map<ref Player, bool> m_mCastResult;
protected BaseWorld m_World;
protected int m_iIndex = -1;
protected bool m_bIsCalculating;
void PerformanceExample()
{
m_mCastResult = new map<ref Player, bool>();
m_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 = m_World.TraceMove(traceParam, null); // one Trace per frame
m_mCastResult.Set(player, result == 1);
}
} |
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 |
---|---|
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());
}
} |
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());
}
} |
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)