This is my first open source module, I appreciate all feedback and constructive criticism!
Is your game suffering from low framerates? Is your microprofiler not looking so good? A potential solution would be to finally implement Parallel Luau.
In short, parallel lua speeds up your scripts by running things at the same time in multiple CPU cores, as opposed to just 1. (the default option).
However, the set-up process can be really tedious. I had to go through a lot of pain learning parallel lua too, which is why I made this module. It has very simple & comprehensible API, with almost unnoticable overhead, which makes it quite versatile.
Documentation & API
1. Constructor
SimpleParallel.new(module: ModuleScript, workerCount: number, timeout: number?, name: string?) -> SimpleParallelInstance
Creates a new SimpleParallelInstance
object.
- Arguments:
-
module
: AModuleScript
instance which must return a function when required. -
workerCount
: Amount of actors (CPU threads) to be allocated, of which sole purpose is to execute the module and return the results. -
timeout
(optional) (defaults to 0, aka the next frame): If for whatever reason you want to yield your code, you may want to specify atimeout
parameter. AfterExecuteTasks
is called and your code hasn’t finished before that, an error will be thrown. -
name
(optional): The tag that will be shown on the microprofiler, so you can identify specificSimpleParallel
instances.
-
2. Scheduling function calls (“Tasks
”)
SimpleParallelInstance:ScheduleTask(...)
Pass any arguments that you want to pass to your ModuleScript
’s returned function. The task will be scheduled in an internal table for future execution through the :ExecuteTasks
function, listed below.
3. Executing scheduled tasks
SimpleParallelInstance:ExecuteTasks() -> {[number]: typeof(require(ModuleScript)(...))}
All tasks that were scheduled from :ScheduleTask()
will be equally distributed across all Luau VMs. For the code to actually be ran in parallel, you need to use task.desynchronize()
somewhere in your module.
This function yields until all tasks return a value. It returns an ordered table containing the returned value of each task.
4. Discarding ParallelInstance
s
SimpleParallelInstance:Destroy()
Destroys the actors and wipes off the table’s properties from memory.
Example Usage
-- ReplicatedStorage.FuncToExecute
local function work(count)
local a, b = 1, 0
for i = 1, count do
a, b = b, a
end
end
return function(count)
local a, b = 0, 1
task.desynchronize() -- Hop into parallel execution mode
debug.profilebegin("Desync work")
work(count)
debug.profileend()
task.synchronize() -- ... and back into single-threaded mode
debug.profilebegin("Synced work")
work(count)
debug.profileend()
return math.random()
end
-- StarterPlayer.StarterPlayerScripts.LocalScript
--!strict
local SimpleParallel = require(game.ReplicatedStorage.SimpleParallel)
local funcToUse = game.ReplicatedStorage.FuncToExecute -- (dont require it!)
local newParallelInstance = SimpleParallel.new(funcToUse, 4, 0, "HeavyWork")
task.wait() -- (only necessary in Deferred SignalBehavior. More details in the Precautions section in the devforum post)
while true do
for i = 1, 4 do -- Best practice is to schedule as many tasks as there are actors, or less.
newParallelInstance:ScheduleTask(75000)
end
local results = newParallelInstance:ExecuteTasks() -- {number}
task.wait()
end
Microprofiler:
Precautions
1: Don’t call ExecuteTasks()
at the same frame the ParallelInstance
was created.
This is because of Roblox’s deferred signal behavior. When script.Enabled
is set, the scripts aren’t immediately executed. Create your ParallelInstances on startup and use them later, or use task.wait()
before using them.
Tips
1: Don’t overdo it
This module has (although relatively small) overhead due to bindable events. In cases where code is rather fast, it’s more beneficial to not use this module and just execute it in a singular thread.
2. Don’t spam task.(de)sync too much
Especially if you have more tasks than CPU cores (and actors), switching between parallel and serial mode may cause tasks to take more time to finish executing. Prefer switching from parallel ← → serial as little as possible in your function.
3: Limit the amount of tasks below or equal to the amount of actors assigned
Even though the tasks get evenly distributed among all actors, the overhead added by internal bindable events & task.desync()
calls accumulates. For best performance and benefits, assign less tasks.