Need help understanding performance regression

To preface, I am working on a game where I need to resize quite a few parts at the same time in increments of less than one, unfortunately BasePart:Resize() does not support using increments of less than one.

So my solution was to create a module, the module works fine and allows me to resize in any direction by any amount without any issues.

Here is the code of the original module:

--!strict
local fineResize = {}


function fineResize.FineResize(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z
	
		if resizeVector == Enum.NormalId.Top then
			part.Size = Vector3.new(PartSizeX, (PartSizeY + resizeBy), PartSizeZ)
			part.Position = Vector3.new(PartPosX, (PartPosY + (resizeBy/2)), PartPosZ)
			return true
		elseif resizeVector == Enum.NormalId.Bottom then
			part.Size = Vector3.new(PartSizeX, (PartSizeY + resizeBy), PartSizeZ)
			part.Position = Vector3.new(PartPosX, (PartPosY - (resizeBy/2)), PartPosZ)
			return true
		elseif resizeVector == Enum.NormalId.Left then
			part.Size = Vector3.new((PartSizeX+resizeBy), PartSizeY, PartSizeZ)
			part.Position = Vector3.new((PartPosX - (resizeBy/2)), PartPosY, PartPosZ)
			return true
		elseif resizeVector == Enum.NormalId.Right then
			part.Size = Vector3.new((PartSizeX+resizeBy), PartSizeY, PartSizeZ)
			part.Position = Vector3.new((PartPosX + (resizeBy/2)), PartPosY, PartPosZ)
			return true
		elseif resizeVector == Enum.NormalId.Front then
			part.Size = Vector3.new(PartSizeX, PartSizeY, (PartSizeZ+resizeBy))
			part.Position = Vector3.new(PartPosX, PartPosY, (PartPosZ - (resizeBy/2)))
			return true
		elseif resizeVector == Enum.NormalId.Back then
			part.Size = Vector3.new(PartSizeX, PartSizeY, (PartSizeZ+resizeBy))
			part.Position = Vector3.new(PartPosX, PartPosY, (PartPosZ + (resizeBy/2)))
			return true
		end

	return false
end


return fineResize

This worked fine! However, I felt it was a bit ugly and could run a bit faster, so I decided to use a table with functions, instead of a large amount of if statements, which looks like this:

--!strict
local fineResize = {}

local function Top(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new(PartSizeX, (PartSizeY + resizeBy), PartSizeZ)
	part.Position = Vector3.new(PartPosX, (PartPosY + (resizeBy/2)), PartPosZ)
	return true
end
	
local function Bottom(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new(PartSizeX, (PartSizeY + resizeBy), PartSizeZ)
	part.Position = Vector3.new(PartPosX, (PartPosY - (resizeBy/2)), PartPosZ)
	return true
end

local function Left(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new((PartSizeX+resizeBy), PartSizeY, PartSizeZ)
	part.Position = Vector3.new((PartPosX - (resizeBy/2)), PartPosY, PartPosZ)
	return true
end

local function Right(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new((PartSizeX+resizeBy), PartSizeY, PartSizeZ)
	part.Position = Vector3.new((PartPosX + (resizeBy/2)), PartPosY, PartPosZ)
	return true

end

local function Front(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new(PartSizeX, PartSizeY, (PartSizeZ+resizeBy))
	part.Position = Vector3.new(PartPosX, PartPosY, (PartPosZ - (resizeBy/2)))
	return true
end

local function Back(part: Part, resizeVector: Enum.NormalId, resizeBy: number)
	local PartPosX: number, PartPosY: number, PartPosZ: number = part.Position.X, part.Position.Y, part.Position.Z
	local PartSizeX: number, PartSizeY: number, PartSizeZ: number = part.Size.X, part.Size.Y, part.Size.Z

	part.Size = Vector3.new(PartSizeX, PartSizeY, (PartSizeZ+resizeBy))
	part.Position = Vector3.new(PartPosX, PartPosY, (PartPosZ + (resizeBy/2)))
	return true
end
	
local t = 
	{
		[Enum.NormalId.Top] = Top,
		[Enum.NormalId.Bottom] = Bottom,
		[Enum.NormalId.Left] = Left,
		[Enum.NormalId.Right] = Right,
		[Enum.NormalId.Front] = Front,
		[Enum.NormalId.Back] = Back
	}


function fineResize.FineResize(part: Part, resizeVector: Enum.NormalId, resizeBy: number)

	local func = t[resizeVector]
	func(part, resizeVector, resizeBy)
	
	return false
end


return fineResize

Now, by moving those conditional statements out into a simple table, and indexing it directly, one would expect this new code to run substantially faster, however, it is actually about 20ms slower (in a BEST case scenario, even!) on a task with a runtime of about 220ms. (The task in question is mass-resizing 20000 parts without a wait)

Now no, I do not need to resize 20000 parts at once in my usecase, however, I would still like to understand why this is slower for future reference, thanks!

I presume it has todo with the extra function execution required in the dictionary option and probably some optimizations with vectors and stuff (note I’m not a Roblox Engineer, so the exact reason is probably more complex)


Doing some testing with basic code nets:

0.002574999933131039 - If Statements
0.0010691999923437834 - Pure Dictionary
0.0020321999909356236 - Dictionary with Functions

Which is what you’d expect with if statements losing to dictionaries, however --!native code execution flips this on its head

0.000964100006967783 - If Statements
0.00018259999342262745 - Pure Dictionary
0.0018485999898985028 - Dictionary with Functions

This brings me to believe that there’s a level of automatic optimizations that are implemented with if statements that aren’t implemented with dictionary functions.

Test Code
local setOfRandomNumbers = table.create(100000)
for i=1, 100000 do
	setOfRandomNumbers[i] = math.random(1, 6);
end

-- If statements
local function doStuff(i)
	if i == 1 then
		return true;
	elseif i == 2 then
		return false;
	elseif i == 3 then
		return true;
	elseif i == 4 then
		return false;
	elseif i == 5 then
		return true;
	elseif i == 6 then
		return false;
	end
end

local t = os.clock();

for _, n in setOfRandomNumbers do
	doStuff(n)
end

print(os.clock() - t);

------------------------

local dictionary = {
	[1] = true,
	[2] = false,
	[3] = true,
	[4] = false,
	[5] = true,
	[6] = false,
}

-- Pure Dictionary
local function doStuff2(i)
	return dictionary[i];
end

local t = os.clock();

for _, n in setOfRandomNumbers do
	doStuff2(n)
end

print(os.clock() - t);

-------------------------

local dictionary = {
	[1] = function()
		return true;
	end,
	[2] = function()
		return false;
	end,
	[3] = function()
		return true;
	end,
	[4] = function()
		return false;
	end,
	[5] = function()
		return true;
	end,
	[6] = function()
		return false;
	end,
}

-- Dictionary with Functions
local function doStuff3(i)
	local func = dictionary[i]
	return func(i);
end

local t = os.clock();

for _, n in setOfRandomNumbers do
	doStuff3(n)
end

print(os.clock() - t);

But at the end of the day you should probably use this instead:

--!strict
local fineResize = {}

function fineResize.FineResize(part: Part, normalId: Enum.NormalId, resizeBy: number)
	
	part.Size += (Vector3.FromNormalId(normalId) * resizeBy)
	part.Position += (Vector3.FromNormalId(normalId) * resizeBy * 0.5)

	return true
end

return fineResize

Even if it’s less performant, the increased readability and function size reduction is 100% worth. (But, it might even be more performant due to the use of backend C++ functions in the case of Vector3.FromNormalId who knows)

1 Like

Your findings are really interesting to me, because I found that weather I used native or not, dictionary functions were always slower than if statements, in fact, native didn’t really change my performance much at all. (though it did improve performance when I tried your test code just to make sure my studio wasn’t bugged or something) I’m willing to chalk that up to general native weirdness.

Regardless, it doesn’t matter anyways, your code suggestion is far superior in readability, and slightly superior in performance. (I didn’t know Vector3.FromNormalId() was a thing!) So that’s what I’ll be using, thanks for the help!