Can parallel be used here?

For my game Metallurgia, this is the method for the main mechanic (i.e interactions between elements, since it is a metallurgy game after all)

composition decoded from StringValue into a dictionary → loops through all possible reactions, deciding if the reaction can occour based on its requirements, if it can happen, it will be added into a list of possible reactions → the ingridients are modified to make sure no material is created or destroyed → this returns a new dictionary containing the new composition post-reaction → this new composition is incoded back into a string format and stored in the StringValue as: "index:value|index:value|..."

Or a simplified explanation:

Composition & Temperature values read → decoded (C only) → reactions calculated → encoded → set

1 Like

I’m guessing you’re talking about Parallel Luau. You never really elaborated on what exactly you’re trying to do.

For your particular use case it’s not worth parallelizing it, even if it will be of any benefit at all.

JSON encode/decode functions, or any string serialization algorithm in general cannot be parallelized because there’s no feasible way to split the workload among independent workers. It has to read the entire string first and parse it as a whole. The best you can do is parallelize encoding/decoding separate strings.

image

Not sure why you wanted to make your own serialization, you could definitely just use JSON instead like I mentioned earlier

You can use a hashmap for this, resulting in O(1) time complexity. That should be more than fast enough.

2 Likes

image
is this the correct usage then?

2 Likes

update:
image
systemLoop function is called via runService.Heartbeat:ConnectParrarel()
(the counters dictionary is just for statistics)

other info:
image
frequency is 2.5 (i.e 2.5 reaction refreshes / second / melt)

1 Like

You should remove the last task.desynchronize(), as it does nothing

Seems to me like it should work however,

The workload seems like it would be insufficient to be worth parallelizing, the overhead caused by parallel lua is likely to be worse than the benefits you would get. If the value you are decoding is very big and the interaction script is very heavy, then it might be worth it.

For parallel lua to be effective, you need multiple scripts running in parallel at the same time (otherwise you don’t get the benefits or running code on multiple threads since there is no code to run on those threads). If that piece is code is being executed many, many times, then it is beneficial, especially if you have hundreds or thousands of them, and you combine multiple of them into a single actor, limiting the number of actors and ensuring each actor are doing a meaningful amount of work. The overhead in this case will be a lot smaller than the benefits gained from using parallel lua

Here is the implementation of parallel lua in one of my games, using a module I made


I basically have giant strings that can reach a maximum of almost 50k games stored in datastores (storing the basic information of those games). That information is compressed, so I need to deserialize it to use it but, the information is separated by the “_” string, so I am able to effectively divide the workload. It is set to run on 6 actors (which is the number that gave me the best results, on a roblox server). All the compressed game data is distributed to those 6 actors, then deserialized, and put back together in a simple lua table. Doing this has greatly reduced the time it takes to decompress the data

1 Like

Oh I just noticed, the for look keeps synchronizing and desynchronizing, which is ineffective. For code to run in parallel, they must be in different actors. Currently, how it will behave is, it will run code on a single parallel thread (instead of multiples), then it will go back to serial, then back to parallel, over and over, not taking advantage of multiple threads

1 Like

Also for context, here is the entire source:

local meltTag = "meltPart"
local collect = game:GetService("CollectionService")
local replica = game:GetService("ReplicatedStorage")

local intfol = game.ServerScriptService.resourceInteractions
local subint = require(intfol.substanceInteractions)
local encdec = require(replica.encdec)
local resInfo = require(replica.elementData)

local arrayLinked : {{m: any, c: StringValue, t: NumberValue}} = {}
local frequency = 2.5

local sumPerform = 0
local lastArray = 0

local counters = 
	{
		reactionsDone = 0,
		totalTime = 0,
	}

local function activateMelt(melt)
	local cinfo : StringValue = melt.composition
	local tinfo : NumberValue = melt.Parent.celcius
	table.insert(arrayLinked, {m = melt, c = cinfo, t = tinfo})
end

local function systenLoop(deltaTime)
	sumPerform += deltaTime * frequency * #arrayLinked
	
	counters.totalTime += deltaTime
	
	if sumPerform >= 1 then
		local todo = math.floor(sumPerform)
		sumPerform -= todo
		for i = 1, todo do
			lastArray += 1
			counters.reactionsDone += 1
			if lastArray > #arrayLinked then lastArray = 1 end
			local here = arrayLinked[lastArray]
			
			local comp = encdec.comp.decode(here.c.Value) 
			
			local newcomp = subint.generateInteractions(comp, here.t.Value)
			task.synchronize()
			here.c.Value = encdec.comp.incode(newcomp)
			task.desynchronize()
		end
	end
end


collect:GetInstanceAddedSignal(meltTag):ConnectParallel(function(melt)
	activateMelt(melt)
end)

for _, m in pairs(collect:GetTagged(meltTag)) do activateMelt(m) end
game["Run Service"].Heartbeat:ConnectParallel(systenLoop)

Since it basically only writes on StringValues, how can I take advantage of parrarel threads, with 1 calculating and the other writing?

1 Like

image
Is this how actor heirachy works?

1 Like

image
You can’t have it write to the string value while another thread is calculating. Writing to a string value can only be done during a serial phase, and during that phase, there is not parallelization going on

If you want to take advantage of every thread available, you would have to take the blue part, send that code to different actors (basically splitting up the for loop and instead of doing the iteration of the code one after the other, you do that on multiple threads)

However, I am pretty certain the bottleneck in your code is writing to that StringValue object. Writing to the properties of objects is surprisingly slow in roblox, and if you could get rid of that step by writing that information in a lua table accessible to other scripts instead of the StringValue, that would likely give a significant boost to performance

This is only relevant if there are a lot of these objects with the string value. If you only have 100 of them, running the code in parallel is probably not necessary (but removing the string value might still be a good idea)

This is how it would look like using my module:

Main script
image

Module script aka WorkerModule (in which code can run in parallel using my module)
image

The code in blue will all run at the same time, in the same parallel phase, then once the parallel phase ends, the code bellow task.synchronize is ran in serial

In this case, the workload is not being merged to reduce the overhead. My module automatically merges the workload but it can only do so much. It is even more important to manually merge the workload if todo if a very high number, and the code in blue runs very fast.

1 Like

Yeah kind of

However, you do not have to have two scripts to run code in parallel and serial. A script inside an actor will still run in serial by default, but what an actor allows you do to is switch from serial execution to parallel execution and vise-versa.
One actor represent one “parallel thread”. Roblox will under the hood take these “parallel threads” and assign them to your cpu threads. Let’s say your cpu has 8 threads, if you have 8 actors doing work in parallel in the same parallel phase, roblox will assign 1 actor to each of your cpu threads. This is why having a single actor doesn’t do anything since that actor will be assigned to 1 cpu thread, but the other cpu thread wont have anything assigned to them
If you have more actors then the number of threads on your cpu, roblox assigns multiple of them to a single cpu thread, basically spreading the workload between all the cpu threads. But having thousands of actors, while allowing for a good spread of the work, is not very efficient and will cause a lot of overhead

1 Like

here.c.Value means setting the value of a stringValue, isn’t that serial only?

2 Likes

Yes

In the example I provided, here.c.Value is being set after task.synchronize, aka in serial. The blue line is code running in parallel, the orange line is code running in serial

However, the first thing I would do to improve performance, before making the code work with parallel lua, would be to remove that string value like I stated.
How many objects with string values are you expecting in your game?

alr, basically implemented what you suggested but w/ no external modules!
Seems to work well, btw would it be more effiecent to calculate all in todo at once?

Nice!

I don’t know, do you mean running everything in serial?
That will depend on the workload you have, how many times does the for loop run?

To figure out what is faster and where the bottleneck is, especially with parallel lua, it is very useful to use the micro profiler


Left to right is the time passing
Up and down are different RBX Workers and different stuff, here the processes that are on top of each other are different threads running in parallel. Using the micro profiler will remove the guessing from all this

I mean something like this:
image

If you have only a single actor, this is slower than running it in serial since it can only run on one cpu thread and isn’t spreading the work over to multiple cpu threads

Is this the case?

Also, what is the length of herelist

here’s the serial side
image

So there is only one actor right?
With only one actor, this is slower than running it in parallel guaranteed, unless some other code also uses an actor

is it meant to be like this?


edit: some more

If there aren’t multiple lines stacked vertically, this means it is only using parallel on a single cpu thread, which basically makes it slower than serial

You can also see that your serial code is the bottleneck, so you should focus on that instead of parallelization

You can further analyze the performance by putting debug.profilebegin(“string”) and debug.profileend, and it will display the portion of the code in between the two in the micro profiler