Skip to content

Performance

LemonSignal does two main things differently which gives it an edge in performance over similar implementations.

Implementation

LemonSignal uses a doubly linked list implementation because it offers the most features out of the others, to show you that I made a barebones version of singly linked list, doubly linked list, unordered array and dictionary signals and benched them using boatbomber's bechmarker plugin.

INFO

The execution times are not meant to measure the absolute time it takes for a method to run, they are only meant to be used relative to eachother to see which one runs faster.

.new

new

:Connect

connect

:Fire

fire

:Disconnect

disconnect

:Reconnect

reconnect

All of the above

all

Conclusion

From the benchmarks above, we can conclude that a doubly linked list strikes the best balance out of the other 3 by having:

  • Ordered fire
  • Fast iteration making :Fire run as fast as an array
  • Solves singly's O(n) disconnect by making it O(1) which makes it as fast as the other 2

All the signal implementations as ModuleScripts are here signals.rbxm
All the .bench ModuleScripts that the benchmarker plugin uses are here benches.rbxm

Thread recycling

Recycling a thread aka a coroutine helps task.spawn and coroutine.resume run significantly faster, about 70%, because those functions wont need to go through the trouble of creating a thread.

GoodSignal popularized that pattern for signals but it can be improved, when you fire your signal and your connection's callback is asynchronous (yields), the next connection in queue will be forced to create a new thread and the when the previous one is finally free, it'll just get GC'ed.

lua
local signal = GoodSignal.new()

signal:Connect(function()
    print("sync")
end)

signal:Connect(function()
    task.wait()
    print("async")
end)

signal:Connect(function()
    print("sync")
end)

signal:Fire()
-- The signal will be forced to create two threads because
-- the middle connection will not return the thread in time

So what can we do about this? LemonSignal simply caches every thread that gets created by the signal so that next time an async connection fires, it'll most likely find a free thread to run on therefor not wasting any work done, so the longer your game runs the higher the chances there's a free thread for the asyncs and the better performance, this benchmark simulates exactly that and shows the notable ~33% speed increase:

recycling

Memory

Does this negatively affect memory you might wonder? To answer this, we first need to know when do we create and cache a thread? A thread gets created when a free one isnt available, and that can only happen when our connection is asynchronous because it keeps that free thread for itself until it's done with it, at which point it caches for another connection that needs it, and even if every connection was asynchronous you'll eventually reach an equilibrium where no new threads will be created and will be exclusively recycled.

To give you a perspective on how little memory the caching uses, it'd take 100k cached threads to raise the heap by a measly ~100mb! And your connections will create nowhere near that amount, so you can rest assured that the memory is being used in a worthy manner.