|
|
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}}
| |