AI Command Performance Benchmarks

TheMaximalBeing made benchmark tests on various AI commands back in 2020 to test their overall impact on game performance and posted them in the AI Scripters forums here: Link. Knowing the impact of these commands on performance can help scripters avoid lag. In DE, any script pass that lasts less than 20 milliseconds will not cause lag, and you can use up-get-precise-time to track script pass durations. Here is the article TheMaximalBeing posted about command performance:

Introduction

Hi everyone,

For a long time, I've suspected that some of my DUC loops might be a bit slow. But just how slow are they? How many times can you issue a command before it causes a noticeable lag? I've decided to test this objectively!

For these tests I set up a loop over a command of interest. The loop was timed in *real* milliseconds, and i also found the time between rule passes (also in real milliseconds). I measured how long the loop took as a percentage of total time between rule passes.

Then the number of loops was increased until it took roughly 50% of the time between consecutive rule passes. At this point there is an obvious lag. The number of loops it took to reach this point for each command + setup is listed below.

All testing done on fast speed, WK. If you know of some other commonly used command + setup that could be slow but is not in the table then please let me know.

Rule of Thumb: Make sure you are doing less than 1/10 of the number of iterations for these commands in each pass of your AI (compared to the numbers in the table below).

Results Summary

#DescriptionSetupLoops for 50% LagSpeed
0Empty LoopJust an empty loop body11,000,000Very Fast
1Chat Messages
(chat-local-to-self)
Chat to self100 *Very Slow
2ABasic DUC Search using localList
(up-find-local)
Find 240 non-existent units
Only 1 unit on map for each player
4,500,000Very Fast
2BFind 240 non-existent units
400 enemy units on map
Only 1 self unit exists
4,500,000Very Fast
2CFind 240 non-existent units
400 of my units on map
300,000Fast
2DFind 1 non-existent unit
400 of my units on map
300,000Fast
2EFind 1 existent unit
400 of them on map and nothing else
800,000Fast
2FFind 240 existent units
400 of them on map and nothing else
80,000Medium
2GFind 40 existent units
400 my units on map
400,000Fast
2HFind 240 existent units by class rather than id
400 of my units on map
80,000Medium
3AFiltered DUC Search using localList
(up-find-local)
Find 240 non-existent units
Filter distance (up-filter-distance)
400 my units on map
300,000Fast
3BFind 240 existent units
Filter distance
400 my units on map
No units in radius
50,000Medium
3CFind 240 existent units
Filter distance
400 my units on map
All units in radius
80,000Medium
3DFind 240 existent units
Filter include with cmd-id set to find only military (up-filter-include)
80,000Medium
4AControl Units with DUC
(up-target-point)
Command 1 unit to move at random600,000Fast
4BCommand 240 units to move at random individually400 units **Very Slow
5ARemoving Stuff from LocalList
(up-remove-objects)
list size 240
remove base-type
(no matches)
250,000Fast
5Blist size 40
remove base-type
(no matches)
1,300,000Very Fast
5Clist size 240
remove exact distance
(no matches)
120,000Fast
5Dlist size 240
remove exact precise-x
(no matches)
200,000Fast
5Elist size 240
remove base-type
all matches
find inside loop
50,000Medium
6ASorting the LocalList
(up-clean-search)
Sort list size of 240
2 different object-data's
Uncorrelated object-data
3,000 (x2)Slow
6Blist size 240
Sort and unsort
same object-data
7,000 (x2)Slow
6Clist size 40
Sort and unsort
Uncorrelated object-data
45,000 (x2)Medium
6Dlist size 20
Sort and unsort
Uncorrelated object-data
300,000 (x2)Fast
6Elist size 5
Sort and unsort
Uncorrelated object-data
1,000,000 (x2)Very Fast
7ADUC search using RemoteList
(up-find-remote)
Find 40 enemy units
undiscovered but existent
Enemy has 400 units
4,000,000Fast
7BFind 40 enemy units
discovered and existent
Enemy has 400 units
400,000Fast
7CFind 40 enemy units
Enemy has 0 units
4,000,000Fast
7DFind 40 enemy units
discovered and existent
Enemy has 40 units
500,000Fast
7EFind 40 ally units
discovered and existent
Ally has 400 units
400,000Fast
8Loading a stored list back into RemoteList
(up-set-group)
Group size = 40
Loop over(up-set-group) to set the RemoteList
5,000,000Very Fast
9APathing
(up-get-path-distance)
Pathing distance for 1 unit to point far away (200 tiles)
No obstacles
6,000Slow
9BPathing distance for 1 unit to point far away (200 tiles)
Lots of obstacles
4,500Slow
9CPathing distance for 1 unit to point far away (200 tiles)
Path does not exist
4,500Slow
9DPathing distance for 1 unit to point nearby (3 tiles)
No obstacles
1,000,000Very Fast
9EPathing distance for 1 unit to point extremely far (340 tiles)
No obstacles
1,700Slow
10ABuilding at points(up-can-build-line)
For palisade-wall
Illegal point
1,700Slow
10B(up-can-build-line)
For palisade-wall
Legal point
1,400,000Very Fast
10C(up-build-line)
For palisade-wall
Illegal point
1,700,000Very Fast
10D(up-build-line)
For palisade-wall
Legal point
240 tiles placed ***Very Slow
11Object Data (any unit)
(up-get-object-type-data)
All IDs in the range 0 to 1400 tested
unit lines tested
Several object-data types tested
5,000,000Very Fast
12Creating / clearing search Groups
(up-create-group)
(up-reset-group)
Started with 40 militia in local list. Then repeatedly created and reset group of 40 units (GroupId = 0).4,000,000Very Fast
13Setting the group flag
(up-modify-group-flag)
Same as above but also setting the Ctrl group flag each time.1,700,000Very Fast
14DUC unit training
(up-target-point)
Using up-target-point with action-train to train militia. Local list consisted of 240 barracks, so there were 240 militia trained each for each train command. Then this whole process was repeated in a loop. Then cancelling them with up-reset-building.60,000***Medium
15Getting map point contents
(up-get-point-contains)
Scanning at random map points to see what unique IDs are located there. Tested with all-units-class (-1) and with several units.1,500,000Very Fast
16ADUC unit movement
(up-target-point)
Moving groups of units to map border
(group size = 1)
400 groups ****Very Slow
16BMoving groups of units to map border (group size = 5)400 groups ****Very Slow
16CMoving groups of units to map border (group size = 20)400 groups ****Very Slow

General Comments

Test0: The main point of this test was to check the point at which the bottleneck is the for-loop itself. None of the other tests got too close to this point.

Test1: This test checked the performance of chat messages, which turned out to be extremely poor. The lag is clearly outside the rule pass so it's hard to quantify exactly the 50% point. 100 chats per rule pass was quite slow. Players who can't see the messages are not affected.

Test2: These tests were designed to test the performance of DUC searches without any filters. We can see that the performance of up-search-local is only affected by YOUR units and depends on the number of units you have. Though once the list is full (or meets the number of requested units) it may stop the search early. If you try to find a unit which does not exist, then it will have to check every unit to prove it doesn't exist. On the other hand, if the unit is abundant the search may find all the units you requested early - which is why the case for searching for 1 existent unit is way faster than 1 non-existent unit.

Test3: These tests where designed to check whether adding filters affected the performance of DUC searches. If the units didn't exist in the first place, then the filters clearly do not affect performance. But event in cases where the units did exist and the filter requirements were met, then there was no difference in performance compared to the no filter case. Clearly performance it limited by adding units to the list rather than the filter.

Test4: This time we control the units by targeting a point on the map. Interestingly, controlling the same unit many times did not cause poor performance. I don't think the game actually controls them until after the AI loop, and in this case, it probably only uses the most recent command (it just overrides it each time in the AI loop). Controlling many units does cause poor performance though. Again, the Lag is outside the rule pass itself. I found it quite laggy when moving 240 units randomly around them map (each controlled individually). In this case the lag is probably caused by path calculations.

Test5: These tests were designed to test the performance of up-remove objects. The first 3 tests didn't actually remove stuff from the list. In cases where you don't need to remove much, the performance depends on the particular object-data you use. Removing ALL units from the list each time looks slower, but in this case the performance is actually limited by the fact that I had to refill the list each loop.

Test6: These tests were designed to test the performance on sorting the lists. I had to do two sorts per loop on two different object-data's so that we are not sorting an already sorted list. Sorting algorithms usually have an n.log(n) complexity, so we expect that halving list size yields more than double the performance. This was clearly the case here - sorting 20 units is quite fast while 240 units is extremely slow.

Test7: This time we do DUC searches on the remote list. These results must be interpreted carefully. The AI keeps a list of known units for each player. If it hasn't discovered the units then it doesn't need to search through them. Performance is the same as for the local list if we are doing an identical search.

Test8: Rather than looping over an identical search (assuming that we need to do this), we might want to use up-create-group and in the loop up-set group to avoid searching again. This clearly leads to a massive performance boost.

Test9: We test up-get-path-distance. Interestingly, obstacles on the map only slightly affected the performance (even a maze had little effect!). Instead the performance is primarily determined by how far away the object must move - and the difference can be huge!

Test10: This time we test the performance of up-can-build-line and up-build line at a specific point. The up-build line check was extremely fast, as was up-build-line whenever it can't actually build it (it probably does the check first). But actually building them is a bit more complex. I found that building 240 palisades in a single rule pass was a bit slow - but the performance primarily depended on how many foundations had already been placed. The first rule pass was in fast quite fast, but it gets slower each time. After 8 rule passes or so (with 8*240 foundations) it was almost a standstill. The lag was outside the rule pass.

Test11: This test was designed to test the performance of "up-get-object-type-data" (as requested by Marathon). First the data-type was set to "object-data-train-site" and a variety of units and unit-lines were tested. It turns out that all units gave close to the same performance. I also ran another test to pick out the slowest performing unit ID in the range 0 to 1400 but it turns out that there was negligible difference in performance. Note that invalid unit IDs simply return -2. I also tested this with other data including "object-data-train-time", "object-data-base-attack" and "object-data-hitpoints" and all gave identical performance. In all cases the command is very fast.

Test12: The test was designed to test the performance of creating search groups. The performance is very good and we should not have to worry about this command.

Test13: This test was designed to test the performance of setting the group flag. The performance is very good and we should not have to worry about this command.

Test14: This test was designed to test the performance of unit training via DUC. The format of this command looks like:

(up-target-point 0 action-train typeOp inOpUnitId)

This will request that the unit be trained by all objects in the local-list. Note that it cannot queue units beyond the housing cap. But it does ignore sn-enable-training-queue. So it is allowed to keep queueing until the queue is full (15 units) or until you have reached the housing cap.

To test performance, I set the pop-cap to 1000 and trained militia in 240 barracks simultaneously (with 240 barracks in the local-list). This was done in a loop. The AI could comfortably train 1000 units in a single rule pass and we can only make the command lag if we try to train ~60000 units, in which case 59000 are blocked. In practise this command should never cause poor performance.

Test15: This command was designed to test the performance of "up-get-point-contains". It turned out to be surprisingly fast. I originally thought it might be looping over a large number of units. But it seems more likely that the game is storing the contents of each tile somewhere. You should be able to scan tiles without ever having to worry about performance.

Test16: This test was designed to test the performance of moving units in groups rather than individually. I wanted to know if moving say 20 units as a group is faster than moving them individually. It turned out that moving them in large groups was much faster but the reason is not so obvious.

From my testing, it became apparent that rather than processing all of the unit move commands straight away, the game is storing them in a queue. When a new move command is issued the unit will immediately stop. Then it must wait until its new move command gets processed. If you do too many move commands in a single rule pass (say 400) then there will be a delay of around 3" before some units start moving.

The units that were put into the start of the queue will move straight away, and the delay gradually increases as you move further down the queue.

This make performance hard to test. The command won't actually cause poor performance - instead units will become unresponsive because the queue is getting too long. If you try to move say 400 units each rule pass then many units will stutter (except the lucky ones at the start of the queue).

It seems that the game processes unit movement per group rather than per unit. So, it's much more responsive if you move your units in large groups rather than individually. Its best if you see this for yourself. I've uploaded a demo scenario and 3 scripts below which move units in groups towards the edge of the map every 2". You will see that the stutter is much worst when we try to move them individually.

Link to DUC Movement Test scenario and AI scripts used for Test 16: DUCmoveTest.zip

Conclusions

I am actually surprised by how good the performance of DUC is. Honestly, I've probably been worrying about it too much. It's generally OK to loop over a DUC search, even a double loop (of say 40 x 40) is OK in many cases. Although watch out for the things marked slow / very slow. Some ways to improve include:

  1. If you need to loop over a sort, try cutting down the list first.
  2. Don't find more units than what you need.
  3. By aware that finding a few units that are abundant is much faster than finding 1 unit that doesn't exist.
  4. If you need to loop over an identical search the consider using up-create-group outside the loop and up-set-group inside the loop.