The Factorio Benchmark Website

test-000010 : A deeper dive into the results of test-000009. Why did more beacons perform worse?

Factorio Version 0.16.51

The TLDR

ElectricNetworkManager::update() consumes signifigantly more time on the maps with additional beacons.

The Question

Based on the results of test-000009, we saw that having more beacon sharing (ie fewer beacons but the same coverage) had a signifigant performance uplift. It was speculated at that time that three possible causes could have caused that performance disparity.

The first suspected cause was the additional power network overhead. While power generation by solar panels and accumulators are grouped so that the calculation is effectively O(1), power consumption very likely is not grouped in that same manner.

The second suspected cause was more efficienct updating of beacon entities and effects with fewer entities. Said another way, by having fewer entities providing effects, it's likely more performant.

A third possible cause was beacons incurring additional UPS cost when entities are in effect range, even if the entities in range are not affected by the beacon effects. For instance a beacon in range of another beacon could cause that other beacon to do checks, even though it is not affected by the modules.

The Test

In order to test these attributes, we need another tool. Valgrind/Callgrind are performance profilers. A profiler essentially runs a program and records how long each function takes. Since the Factorio developers included debugging symbols in their executables, these functions can have human readable and useful names. We will profile all our maps to generate a call graph, which we can extrapolate information from. A quick bash script lets us automate it, that script is available here.

We will also need another map. We will take our more shared design and add some beacons off to the side, where they are not in range of each other or any other entities (except power poles). We will add roughly 32,000 beacons in this manner to bring the total number to approximately the same as our less shared map. By benchmarking this map we should be able to reasonably know if the beacon effect range has any effect on performance. This map should effectively only add the performance penalty of the power network, not any beacon effects.

The Data

First up we will benchmark our new variant map. Doing 3 runs of the map at the same duration as test-000009, we get 9.98 average ms/tick. Compared to our results then, the less shared version got 9.93 average ms/tick. Effectively these two are within margin of error. This would lend credence to the theory that the performance cost is due to the power network overhead. Moreso, it indicates that effect application is either constant or irrelevant.

To further dive into the behavior, we can review our performance profiles. kcachegrind is a useful tool to review the profiles. Data is presented in terms of Ir. Ir stands for instruction fetch. Essentially, functions that have a bigger Ir took longer to execute.

The most relevant function we can look at is Map::update(). Or, to be specific, the children to Map::update(). There are three functions here that take 99% of the time. Surface::update(), ElectricNetworkManager::update(), and TransportLine::update(). Since our profiles are done by benchmarking for 100 ticks (which given the similar nature of the maps, should be enough time), we don't have to worry about the number of calls, rather the Ir per call.

Less beacon sharing
More beacon sharing
More beacon sharing with extra beacons in the map

For all profiles the Ir per call of Surface::update() is roughly ~18.5M. Surface::update() contains updating all the entities in the surface. This would be a strong indcator that entity behavior between saves is the same or very similar.

Meanwhile ElectricNetworkManager::update() consumes ~6.9M Ir per call for our two maps with the same number of beacons, but only 5.2M Ir per call for our map that shares beacons effectively. This is a huge gulf which appears to be responsible for the performance loss.

As for TransportLine::update(), it appears to be exactly as we expect, because the transport lines in every map should be identical (barring any positional symmetry inconsistencies).

With this information, it is pretty clear that the cause of the reduced performance is due to the electric network overhead. Unfortunately we can't dive deeper into ElectricNetworkManager, since we get hit a stopping point at useDemand() and getDemand(). These two functions don't have any useful child functions we could extract further meaning from.

Closing

Based on the findings of these tests, it's clear that the additional beacons harm performance due to electric network overhead, by at least an order of magnitude over the next closest possible cause. Further diving into entity behavior under Surface::update() could be done, but most likely would not yield any interesting result in this case.

Full profiles available inside the raw-data folder for this test.