I was basically finishing a non-metatable signal module and decided to benchmark it against existing ones. I was shocked to see that my module lost against other modules by a significant margin. I tried to find the reason in performance and metatables, but simply couldn’t. I thought it was maybe something to do with namecalls. But no.
Please, I’m desperate on finding out the actual reason behind this margin.
I looked it through. It is overall quite a good code. I thought about it for a while… The worst scenario could occur at the Event_Fire. You might want to instead iterate it through once, rather than shifting on the elements with table.remove. This in the very worst scenario could cause you a O(n^2) time complexity. Instead open a secondary queue in Event_Fire and that will solve the issue.
local function Event_Fire(self:Event, ...) : ()
local queue = self._queue
local newQueue = {}
for i, v in ipairs(queue) do
if type(v) == "function" then
coroutine.resume(coroutine.create(v), ...)
newQueue[#newQueue + 1] = v -- keep the function event for future fires
else
coroutine.resume(v, ...)
-- Thread events are run only once; do not reinsert them.
end
end
self._queue = newQueue
end
There is also one more that I would point out. It is a bit more memory requiring, but would perform better than using a table.remove. That can be achieved by using a swap-and-pop technique within your Event_Pop. The order will still be preserved and that function will be faster as well rather than when using table operations and it is more cache friendly.
local function Event_Pop(self: Event, func: (...any) -> () | thread)
-- Track the position where we write kept elements
local keep_pos = 1
-- Scan through all elements
for scan_pos = 1, #self._queue do
-- If the current element is NOT the one to remove, keep it
if self._queue[scan_pos] ~= func then
self._queue[keep_pos] = self._queue[scan_pos]
keep_pos += 1
end
end
-- Clear leftover elements after the last kept position
for i = keep_pos, #self._queue do
self._queue[i] = nil
end
end