Divinity wrote: ↑Wed Dec 18, 2024 4:29 pm
also, careful with organized tables loops (such as ipairs), because when you remove an item while iterating (like on your line 25), the next iteration index has been moved 1 down. I would recommend iterating end-to-start
After reading your comment (great catch btw), I was going to suggest "
use a linked list, it's very fast for removing arbitrary items".
But after profiling a bit, there's some considerations re: performance:
Code: Select all
Starting...
0.0088169407157843 <-- LINKED-LIST (REMOVE MIDDLE)
0.015085826617797 <-- TABLE (REMOVE MIDDLE)
0.0037212258294996 <-- TABLE (REMOVE LAST)
0.017078480483006 <-- LINKED-LIST (ITERATE)
0.0058362494843696 <-- TABLE (ITERATE)
Finished.
- It's faster to remove the first item in the sequence using a linked list, than removing the first item with table.remove().
- Removing the last item is by far the fastest with table.remove() than with a linked list (like when you don't use an index, it pops the last item).
- Iterating the entire sequence is faster when done with an index FOR loop and the array part of a table, than using a WHILE loop and iterating all nodes in the linked list.
The code I used for those timings is this, the double-linked list implementation is from me:
Code: Select all
io.stdout:setvbuf('no')
-- Used for making DOUBLY-LINKED LISTS of nodes.
--
-- The list itself is a table that stores a reference to the first
-- and last nodes, if the list isn't empty:
-- {[1] = FIRST NODE, [2] = LAST NODE}
--
-- For maximum speed, the nodes are array-only tables in this format:
-- {[1] = VALUE, [2] = PREVIOUS NODE, [3] = NEXT NODE}
local LinkedList = {}
function LinkedList.new(self, ...)
local newList = {
nil, -- [1] = Reference to the first node, if any.
nil, -- [2] = Reference to the last node, if any.
-- Functions.
add = LinkedList.add,
disconnect = LinkedList.disconnect,
clear = LinkedList.clear,
}
return newList
end
setmetatable(LinkedList, {__call = LinkedList.new})
function LinkedList.add(list, value)
local newNode = {value, list[2], nil}
if list[2] then
list[2][3] = newNode
else
-- If there's no last node then there's no first node either, because
-- a one-node list has the same node in both first and last places.
list[1] = newNode
end
-- Set the new node as the last.
list[2] = newNode
return newNode
end
function LinkedList.disconnect(list, node)
-- Reconnect the neighboring nodes, if any, so as to remove the
-- input node from the list.
--
if node[2] then -- If the node has another behind it (so this isn't the first node).
node[2][3] = node[3]
if node[3] then -- If the node has another ahead of it (so this isn't the last node).
node[3][2] = node[2]
else
list[2] = node[2]
end
elseif node[3] then -- If this (first node) has another in front of it.
node[3][2] = nil
list[1] = node[3]
else -- Otherwise this was both the first and last node, the list is now empty.
list[1] = nil
list[2] = nil
end
-- For garbage-collection safety, remove references to other nodes.
node[2] = nil
node[3] = nil
end
function LinkedList.clear(list)
local node = list[1]
local oldNode
while node do
oldNode = node
node = node[3]
-- Remove references to other nodes.
oldNode[2] = nil
oldNode[3] = nil
end
list[1] = nil
list[2] = nil
end
-- =====================
-- MEASURE TIMINGS
-- =====================
function deltaTime(func)
local shortestTime = math.huge
for x = 1, 3 do
local startTime = love.timer.getTime()
for y = 1, 100000 do
func()
end
local endTime = love.timer.getTime() - startTime
if endTime < shortestTime then
shortestTime = endTime
end
end
return shortestTime
end
local list = LinkedList()
local middleNode
for x = 1, 26 do
local node = list:add(x)
if x == 13 then
middleNode = node
end
end
local listDisconnect = list.disconnect
local listAdd = list.add
function workListMiddle()
local middleValue = middleNode[1]
listDisconnect(list, middleNode)
middleNode = listAdd(list, middleValue)
end
function workListIterate()
local result = 0
local node = list[1]
while node do
result = result + node[1]
node = node[3]
end
return result
end
local array = {}
for x = 1, 26 do
array[x] = x
end
local tableRemove = table.remove
local tableInsert = table.insert
function workTableMiddle()
local lastValue = array[13]
tableRemove(array, 13)
tableInsert(array, 13, lastValue)
end
function workTableIterate()
local result = 0
for x = 1, 26 do
result = result + array[x]
end
return result
end
function workTableLast()
local lastValue = array[26]
tableRemove(array)
tableInsert(array, lastValue)
end
print('Starting...')
love.timer.sleep(2)
print(deltaTime(workListMiddle), '<-- LINKED-LIST (REMOVE MIDDLE)')
love.timer.sleep(1)
print(deltaTime(workTableMiddle), '<-- TABLE (REMOVE MIDDLE)')
love.timer.sleep(1)
print(deltaTime(workTableLast), '<-- TABLE (REMOVE LAST)')
print()
love.timer.sleep(1)
print(deltaTime(workListIterate), '<-- LINKED-LIST (ITERATE)')
love.timer.sleep(1)
print(deltaTime(workTableIterate), '<-- TABLE (ITERATE)')
print('Finished.')
function love.keypressed(key)
if key == 'escape' then
love.event.quit()
end
end
So I don't know, iterating the sequence vs removing items seem to cancel each other out in terms of time taken on each structure, and it ends up not making a difference? Or rather, since most of the time you'll only be iterating the sequence to update projectiles, and any deletions will be occasional (only when a projectile hits something etc), then that makes the "end-to-start looping" on a raw array table the fastest solution on average.
But this is stuff that probably won't make a perceptible difference, you're not gonna have tens of thousands of projectiles on screen.