How would I optimize this cellular automata script?

Another note, using ipairs instead of pairs on an array makes a very small difference. Numerical for loops are faster than the two, but it optimizes very little.

local childTable = Instance:GetChildren()
for n = 1, #childTable do
    local child = childTable[n]
end

is an example of how you would use a numerical loop with GetChildren()

FindFirstChild() is actually 30% slower than the dot operator

Do this:

for i,v in pairs(Cells) do
     local childStringValue = v:FindFirstChildOfClass("StringValue")
     local Hates = game.Lighting.Hates[childStringValue.Name]:GetChildren()
     for i2,v2 in pairs(Hates) do
        v2:Clone().Parent = childStringValue
     end
     runService.Heartbeat:Wait()
end

Instead of

for i,v in pairs(Cells) do
	local Hates = game.Lighting.Hates:FindFirstChild(v:FindFirstChildOfClass("StringValue").Name):GetChildren()
	for i2,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
	wait()
end

numerical loops are not recommended because of the fact ipairs caches the value. Indexing a key in a table is not instantaneous.

-- Not Cached Automatically
for i = 1, #array do
    local value = array[i]
     print(value)
end
-- Cached
for index, value in ipairs(array) do
      print(value)
end
local array = {}
for i = 1, 100 do
	array[#array + 1] = math.random(1, 10)
end

for i = 1, 100 do
	local numericTime = nil
	local ipairsTime = nil
	
	local startTime = os.clock()
	for i = 1, #array do
		local value = array[i]
	end
	numericTime = os.clock() - startTime
	startTime = os.clock()
	for index, value in ipairs(array) do
	end
	ipairsTime = os.clock() - startTime
	
	print(numericTime < ipairsTime and ('numeric is faster: %s'):format(numericTime) or ('ipairs is faster: %s'):format(ipairsTime))
end

i’ve tested this benchmark numerous times, and the generic is faster than numeric over 95% of the time. Only time I would use numeric is when iterating through an array backwards or by certain multiples, etc… If you are concerned about speed use ipairs.

Your “simple trick” is slower than the native .Magnitude property of all Vector3s. Look at these benchmarks:

local ITERATIONS = 1e7;
local TEST_VECTOR = Vector3.new(1,2,3);

local function GetMagnitude(vec: Vector3): number
	local x,y,z = vec.X,vec.Y,vec.Z;
	return math.sqrt(x*x+y*y+z*z);
end

local startTick = tick();
for _=1,ITERATIONS do
	local magnitude = GetMagnitude(TEST_VECTOR);
end
local timeGetMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = TEST_VECTOR.Magnitude;
end
local timeNativeMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = GetMagnitude(Vector3.new(1,2,3));
end
local timeInstantiateGetMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = Vector3.new(1,2,3).Magnitude;
end
local timeInstantiateNativeMagnitude = tick()-startTick;

print(string.format("Timing Comparisons:\n\nGetMagnitude(): %fs\nNative .Magnitude: %fs\nGetMagnitude with new Vector3s: %fs\nNative .Magnitude with new Vector3s: %fs\n\nMagnitude = %f",timeGetMagnitude,timeNativeMagnitude,timeInstantiateGetMagnitude,timeInstantiateNativeMagnitude,TEST_VECTOR.Magnitude));

Timing Comparisons:

GetMagnitude(): 1.105992s
Native .Magnitude: 0.385828s
GetMagnitude with new Vector3s: 1.905560s
Native .Magnitude with new Vector3s: 1.138401s

Magnitude = 3.741657

2 Likes

must be a newer update then :man_shrugging: , ipairs used to be slower.

1 Like

Seems like it has been optimized, pretty sure it used to be expensive.

1 Like

.Magnitude is a key not a function. how can indexing a key be more expensive than calling a function?

1 Like

It’s internally calculated whenever you index it and that internal calculation is expensive.

1 Like

A calculation on top of that internal calculation is redundant

1 Like

It is because every time you use a vector3.new operator, the magnitude is internally calculated. Using your function would just use more performance, since you’re recalculating the magnitude again.

1 Like

Using your function would just use more performance, since you’re recalculating the magnitude again.

No, .Magnitude is calculated when you try to index it.

1 Like

No we’re not on the same page here, I know your function uses less performance. However, just by doing Vector3.new() the magnitude was already calculated. What’s the use of calculating it again with your function.

1 Like

the magnitude was already calculated. What’s the use of calculating it again with your function.

It isn’t like I said, it is only when you try to index it. Why would it calculate the magnitude unnecessarily, it will do if it is needed and that’s by indexing it. >.<

1 Like

Do you have the source on where I can find this?

1 Like

If the operation was less expensive, then it would run faster. Your argument makes no sense. Keep in mind that .Magnitude is also cached once it’s accessed once; your “simple trick” of a method does no such thing AND takes longer to compute while adding unnecessary code to the script environment.

Your entire statement is false.

@Pxxm Magnitude is actually calculated when it is indexed for performance reasons, but as stated above in this post the value is cached in case you end up indexing Magnitude multiple times from the same Vector3. Source

2 Likes

Thanks for the source, I never knew that .Magnitude is cached when indexing it but there is still no direct answer whether it is or not.

2 Likes

Just wanted to note that, even when disregarding Magnitude caching, your function still runs slower than the native .Magnitude calculation (compare the third and fourth times in my timing comparisons).

A general rule of thumb is that if something is implemented natively, just rely on the work the Roblox engineers have done for you rather than trying to reinvent the wheel.

(General because TweenService replicating from the server to the client is probably the best exception to this)

2 Likes

Just wanted to note that, even when disregarding Magnitude caching, your function still runs slower than the native .Magnitude calculation (compare the third and fourth times in my timing comparisons).

Yeah, if this is done in C, this should explain why it’s faster. A program in C runs faster than a program in Lua.

1 Like

Everyone here has mostly been focusing on micro-optimizations and haven’t really been looking at the big picture. In your code, you have a loop that iterates over each cell, and for each cell iterates again over each cell.

This is an O(n^2) approach (as the number of iterations is proportional to n*n).

I really don’t know what your code is supposed to do, but it looks like it’s somewhat like a force-directed graph? ie; each cell repulses each other based on distance or something.

Instead of naively iterating over each cell for every cell, you could instead implement some sort of spatial partitioning algorithm to intelligently only look at pertinent cells. In n-body simulations, an O(n^2) approach can be transformed into an O(n log n) via Barnes-Hut simulation

While your code may not exactly be applicable to Barnes-Hut, it should serve as a stepping stone into looking into algorithmic-optimizations rather than micro-optimizations.

4 Likes

The reason FindFirstChild is slower is primarily because there is some overhead to the mechanism behind it on top of the fact that . indexes have gotten considerably faster in luau all around.

However, I really don’t recommend using :FindFirstChild() OR the . operator. You should almost always use :WaitForChild(), while its probably the slowest of the three, you shouldn’t need to do this in a loop ever. The benefit is that you can rely on things being completely reliable regardless of how replication works, on top of the fact that you won’t run into property collisions, which I believe in the past has broken some games. If you’re using any of these you almost always only ever need to do it once (outside of functions).

:GetChildren() is also quite fast, if not faster than indexing for higher child counts, though, I’ve not checked.

The exception is if you’re accessing children by variables as names, though, generally there are a lot better ways to do that, such as by mapping the instances you want to look at to their “names” with a hashmap somehow. You really shouldn’t be doing this regardless though unless you have no other option, since, this is hard to optimize. (Also, a["b"] is significantly slower than a.b in luau)

I have also noticed that for loops are as slow as the are. I’m not sure why its the case, but, you almost always seem to achieve better speeds if using a loop like so:

local i = 1
while i <= #table do -- Use ipairs, this is just an example
    local child = table[1]
    i += 1
end

I would assume this comes down to how it compiles, though, I really couldn’t tell you.

As for why ipairs and pairs are so much faster, its again, because of luau, both have been very very well optimized, its often times always faster to use ipairs so long as you have an existing table.

Lastly, the reason .Magnitude is actually faster now is (shocker) because of luau again. The reason from what I can tell is basically that the largest portion of time actually comes from indexing. Everything else takes less time so, in the case of any of the rootless distance checks I could come up with in the post @XxELECTROFUSIONxX linked in their edit, since there are three indexes, the time is almost exactly 3x slower than .Magnitude when you look at times over many iterations.

Also I’m fairly sure .Magnitude is calculated upon the first index and cached, though, I can’t verify that. That’s what @SilentsReplacement was saying. This sort of behaviour is actually used a lot in the engine, every Roblox instance is a (special) userdata and has metatables, which has C metamethods. That’s generally how most lua based games work (and is also how Roblox works). When you index a property any sort of function-like behavior can happen before you get a result. I believe that that’s exactly how RobloxLocked instances work. In fact, I believe the way all class security stuff probably works is via those metamethods.

2 Likes