Page 1 of 1
Manual: balance load in on_tick
Posted: Thu Nov 17, 2016 2:40 pm
by Optera
Since i couldn't find something similar and I guess most modders will use on_tick I'm going to start this thread as help to reduce load from on_tick events.
If you have a way to distribute load across multiple ticks, please add it to this thread.
single modulo
Code: Select all
if game.tick % update_interval then
for k, sensor in pairs(global.Sensors ) do
Worker(sensor)
end
end
Added only for completion.
Con:
- no load distribution among ticks
modulo compare
Code: Select all
for k, sensor in pairs(global.Sensors ) do
if k % update_interval == game.tick % update_interval then
Worker(sensor)
end
end
Pro:
- spreads load from Worker evenly across corresponding modulos
Con:
- works only with global.Sensors as array
unordered sets will break this method
Re: Manual: balance load in on_tick
Posted: Thu Nov 17, 2016 3:06 pm
by Klonan
With the modulo compare, you can rearrange it so you only need a single modulo:
Code: Select all
function on_tick(event)
local tick = game.tick
for k, v in pairs (global.table) do
if (k + tick) % interval == 0 then
do_something(v)
end
end
end
Re: Manual: balance load in on_tick
Posted: Thu Nov 17, 2016 5:14 pm
by Optera
Klonan wrote:With the modulo compare, you can rearrange it so you only need a single modulo:
That's smart.
Do you have something working for unordered sets by chance?
Re: Manual: balance load in on_tick
Posted: Thu Nov 17, 2016 5:53 pm
by Rseding91
Optera wrote:Klonan wrote:With the modulo compare, you can rearrange it so you only need a single modulo:
That's smart.
Do you have something working for unordered sets by chance?
In your loop increment it by 1 and use it as the index.
Re: Manual: balance load in on_tick
Posted: Fri Nov 18, 2016 2:12 am
by Mooncat
After reading
FFF-161, I have an idea but still haven't tried it yet (or maybe I did for the creative chests in CM unintentionally):
When you add items into the global table, separate them into sub-tables indexed by integer: for the first item, you add it into the table at [1], next at [2], and so on...
Code: Select all
function insert_sensor_at(i, sensor)
if not global.Sensors[i] then global.Sensors[i] = {} end
table.insert(global.Sensors[i], sensor)
end
1st sensor: insert_sensor_at(1, sensor)
2nd sensor: insert_sensor_at(2, sensor)
n-th sensor: insert_sensor_at(n % interval + 1, sensor)
Then, in on_tick, you only loop through the sub-table at particular index:
Code: Select all
function on_tick(event)
local tick = game.tick
local index = tick % interval + 1
for k, sensor in pairs(global.Sensors[index])
Worker(sensor)
end
end
Although it is still O(n), but I guess it can improve UPS a little bit?
Re: Manual: balance load in on_tick
Posted: Fri Nov 18, 2016 9:04 am
by Optera
Mooncat wrote:When you add items into the global table, separate them into sub-tables indexed by integer: for the first item, you add it into the table at [1], next at [2], and so on...
Code: Select all
function insert_sensor_at(i, sensor)
if not global.Sensors[i] then global.Sensors[i] = {} end
table.insert(global.Sensors[i], sensor)
end
Is there any advantage in having to add checks so it wont become an unordered set? When you can do this
Code: Select all
table.insert(global.Sensors, sensor)
or if you want to add in the middle of a table. With insert taking care of moving all following indexes up 1.
Code: Select all
table.insert(global.Sensors, index, sensor)
Another thing I stumbled across:
According to this
stackoverflow thread table[#table+1] = value is a little bit faster for appending at the end of tables than
table.insert(table, value)
Re: Manual: balance load in on_tick
Posted: Fri Nov 18, 2016 10:15 am
by Mooncat
Did you mean the if check? It is just for ensuring the sub-table exists so it doesn't show error. It doesn't matter if you use sorted or unsorted set, the trick is to separate the whole big global.Sensors into smaller global.Sensors[1 ~ interval].
As stated in that FFF, the problem of having one global.Sensors is that you still need to loop through the whole set and check
Code: Select all
if (k + tick) % interval == 0 then
for each item. Maybe the problem is smaller than the FFF because the numbers are straightforward in our case. But still, my idea can reduce the number of modulo operations so, in theory, it can save some UPS.
Maybe we should carry out a test for them. Also for table[#table+1].
Re: Manual: balance load in on_tick
Posted: Sat Nov 19, 2016 8:45 am
by Optera
I meant checks to make sure i in function insert_sensor_at(i, sensor) is always continuous. If you happen to remove just the right sensors it might end up like this: global.Sensors[1], global.Sensors[2], global.Sensors[4] which behaves a bit odd. I think #global.Sensors would return 2 for example.
You'd have rebuild the array on every create/remove. Depending on how often you update vs rebuild the array it may not improve performance at all.
Re: Manual: balance load in on_tick
Posted: Sun Nov 20, 2016 4:11 pm
by Choumiko
Mooncat wrote:Maybe we should carry out a test for them. Also for table[#table+1].
Someone more or less already has:
https://springrts.com/wiki/Lua_Performance
Re: Manual: balance load in on_tick
Posted: Sun Nov 20, 2016 5:59 pm
by Optera
Thanks for linking this. I had no idea for loops in lua had such huge performance differences. Getting rid of for pair loops should yield a lot more than any other optimization.
Re: Manual: balance load in on_tick
Posted: Mon Nov 21, 2016 2:07 am
by Mooncat
Optera wrote:I meant checks to make sure i in function insert_sensor_at(i, sensor) is always continuous. If you happen to remove just the right sensors it might end up like this: global.Sensors[1], global.Sensors[2], global.Sensors[4] which behaves a bit odd. I think #global.Sensors would return 2 for example.
You'd have rebuild the array on every create/remove. Depending on how often you update vs rebuild the array it may not improve performance at all.
hm... you don't need to make sure i is continuous. But I think some more works are needed in order to make the sensors to be evenly distributed. Like another array of indexes sorted by the array sizes.
And no, don't use #global.Sensors because they are arrays. #global.Sensors doesn't represent the number of sensors, but the number of sensor arrays.
That's a good article. Thanks for share.
Re: Manual: balance load in on_tick
Posted: Mon Nov 21, 2016 2:42 am
by Supercheese
Optera wrote:
Thanks for linking this. I had no idea for loops in lua had such huge performance differences. Getting rid of for pair loops should yield a lot more than any other optimization.
Pretty sure Factorio has a custom implementation of pairs(), which could make those performance tests invalid for Factorio applications.
Re: Manual: balance load in on_tick
Posted: Mon Nov 21, 2016 8:19 am
by Optera
It seems factorio performance behaves similar, but the effects are neglectably small.
For Inventory Sensor 1.1.3 i changed every possible for pairs to for i, #table and table.insert to table[#table+1] performance only went up by 0.06 UPS at best on my test map and oddly enough stayed exactly the same on some other maps.
in Numbers on the same save:
Inventory Sensor 1.1.1 ~0.28
Inventory Sensor 1.1.3 ~0.24
Re: Manual: balance load in on_tick
Posted: Mon Nov 21, 2016 11:58 am
by bobingabout
I was thinking of something along the lines of
Code: Select all
if tick % sizeof(global.table) == 0 then
but then it hit me.... how do you even get the size of a table?
Re: Manual: balance load in on_tick
Posted: Mon Nov 21, 2016 12:19 pm
by Optera
bobingabout wrote:I was thinking of something along the lines of
Code: Select all
if tick % sizeof(global.table) == 0 then
but then it hit me.... how do you even get the size of a table?
If it's an array you can use #table, if it's an unordered set you have to iterate through the entire thing with for pairs and count = count+1
For anything mostly run inside on_tick arrays seem the better storage strategy.
Using unordered sets does have advantages though.
If you store entities indexed by unit_number you don't have to use for loops when you get events triggered by entities and can access directly with global.entities[event.entity.unit_number].
Inventories are also unordered sets, with item name being the key which makes it a bit slower to read everything, but makes it very fast updating the count of a single item.
Re: Manual: balance load in on_tick
Posted: Tue Nov 22, 2016 11:01 am
by bobingabout
In that case, you'd probably end up with something like...
Code: Select all
-- Count tracker. would definitely be better to keep track of the size externally to on_tick
global.count = 0
for k, sensor in pairs(global.Sensors) do
global.count = global.count + 1 -- would be so much easier if we could do count++ like in C
end
--in on_tick
local count = 0
for k, sensor in pairs(global.Sensors) do
count = count + 1
if game.tick % global.count == count then
--do stuff with sensor or global.Sensors[k]
end
end
Alternatively, a slightly less safe but faster method, assuming you use an indexed table, and global.count is updated external to the tick routine:
Code: Select all
i = game.tick % global.count
--do stuff to global.Sensors[i]
Re: Manual: balance load in on_tick
Posted: Wed Nov 23, 2016 4:37 pm
by Macros
I use a
while loop during on_tick. This method iterates over 1/60th of the table each tick, starting over from the beginning once 60 ticks have passed:
Code: Select all
local TICK_INTERVAL = 60
script.on_event(defines.events.on_tick, function(event)
local size = #global.ArrayOfThings
local i = global.Counter -- global.Counter starts at 1, and increments by 1 every tick, resetting to 1 when global.Counter == TICK_INTERVAL
while i <= size do
local thing = global.ArrayOfThings[i]
if thing ~= nil then
<...do stuff...>
end
i = i + TICK_INTERVAL
end
global.Counter = (global.Counter % TICK_INTERVAL) + 1
end)
for is more efficient when iterating over an entire table, but my tests have indicated that
while is more efficient when you only want a small fraction of the table on any given tick.
Re: Manual: balance load in on_tick
Posted: Wed Nov 23, 2016 5:40 pm
by DedlySpyder
Optera wrote:It seems factorio performance behaves similar, but the effects are neglectably small.
For Inventory Sensor 1.1.3 i changed every possible for pairs to for i, #table and table.insert to table[#table+1] performance only went up by 0.06 UPS at best on my test map and oddly enough stayed exactly the same on some other maps.
in Numbers on the same save:
Inventory Sensor 1.1.1 ~0.28
Inventory Sensor 1.1.3 ~0.24
I've had similarly small results from testing between the two. Powered entities had a big freeze (several seconds on a large map) and changing between the two had fairly inconclusive results (the minor variation in the search itself was about the same as what I might have saved).
As for this topic in general, I am going to need something like this soon, and here is my take on it:
Every time I add something to the global table I will recalculate a global variable (offset). This offset will be the number of items I need to check divided by how often (so 120 items divided by 60 ticks) rounded (not sure if I should floor or ceiling yet). Then, every tick take the modulo of the tick by the interval (60) times my offset, and check the offset in items. So if we're on tick 42 from above, we check 84 and 85.
I've yet to actually try this, but it seems to make sense in my head and written out.