Niche Perfomance Details in Programming

Introduction

This is a compiled list of multiple performance quirks, suggestions and gotchas in programming that I have accumulated throughout the usage of the engine. This list ranges from small style choices to entire anti-patterns.

Use Table Literals

Table Literals are when you explicitly describe your keys/values inside the brackets as opposed to outside. This allows LuaU to activate some optimization.

-- Bad
local Table = {}
Table.Pizza = true

-- Good
local Table = {
	Pizza = true
}

Optimize Unknown-Sized Arrays with table.create

If a Table Literal isn’t possible and you need to manually insert values into it, you should use table.create to pre-allocate memory so you only need to pay for allocation once.

-- Bad
local Numbers = {1, 2, 3, 4}
local Evens = {}

for _, v in Numbers do
	if 2 % v == 0 then
		table.insert(Evens, v)
	end
end

-- Good
local Numbers = {1, 2, 3, 4}
local Evens = table.create(#Numbers)

Use Vector3 Constants inside CFrame.new when possible

This one should speak for itself, but it is actually faster to use Vector3 constants for the constructor instead of just writing the XYZ out.

-- Bad
local CF = CFrame.new(0, 10, 0)

-- Good
local CF = CFrame.new(Vector3.one * 10)

Avoid Lambda Functions when working with Callbacks

Sometimes a single lambda function won’t make any difference in terms of performance, however if you are constantly connecting and disconnecting functions to a callback or have two callbacks to an identical function, you are running the risk of creating duplicate functions in memory if it contains UpValues that are not constant (As seen the example below).

-- Bad
local UpValue = 1

EventA:Connect(function(...)
	UpValue = 2
end)

EventB:Connect(function(...)
	UpValue = 2
end)

-- Good
local UpValue = 1

local function OnEvent(...)
	UpValue = 2
end

EventA:Connect(OnEvent)
EventB:Connect(OnEvent)

This can also be extended to other fields such as coroutines and the task library, where you have the freedom to actually pass arguments into the pre-defined function.

local function Add(A, B)
	print(A + B)
end

task.spawn(Add, 1, 2)

Cache Re-Usable Objects

Too often do I see amateur programmers create new Tweens, AnimationTracks and other objects meant to be reused in coding. You should keep these objects as constants instead of repeatedly creating and destroying them.

-- Bad
local function Disappear()
	local Tween = TweenService:Create(Part, TweenInfo.new(1), {Transparency = 1})
	Tween:Play()
end

-- Good
local TWEEN_INFO = TweenInfo.new(1)
local TWEEN_DISAPPEAR = TweenService:Create(Part, TWEEN_INFO, {Transparency = 1})

local function Disappear(Size)
	TWEEN_DISAPPEAR:Play()
end

Avoid unnecessary interaction with the DataModel

The impact of this can range from small performance to colossal change. You should try to keep references to instances from the DataModel instead of continuously traversing for them.

-- Bad
local Model = ...

Model.Part.BrickColor = BrickColor.Red()
Model.Part.Anchored = false

-- Good
local Model = ...
local Part = Model.Part

Part.BrickColor = BrickColor.Red()
Part.Anchored = false
19 Likes

Can you go into detail about what some of these issues and optimizations are instead of basically saying “this is good” or “this is bad”. Some of them are relatively common knowledge, but others not so much.

It’s not necessarily “bad”. It’s just not as good. Both ways work, and the impact is minuscule in most cases.

Using the suggested methods in this thread will make your code faster in the long run, but it’s not mandatory.

The reason these are optimizations is that they can remove duplicates and unnecessary calculations. Some of the points listed here are also optimizations just because it’s faster. For example, multiplying in C# is faster than dividing (visualized in Lua):

print(3 * 0.5) --> this is actually faster
print(3 / 2) --> this is slower

It’s a micro-optimization but it’s good if you use it when plausible.

3 Likes

I think it might be a good idea to also provide benchmarks that back up these claims in Roblox Luau. Also important to keep in mind it may perform differently on different devices.

Micro optimizing is only good if you’re doing things in a fast running loop, otherwise, it’s pretty useless.

This isn’t really a good way to describe it though. You should provide information on what these optimizations or issues are specifically and why the “good-er” ways perform better.

You showed code examples but didn’t really explain why the “this is good” code is actually better for performance. You made “you should…” statements without the “because…” is my point (for some of them).

The device doesn’t matter here. The code will just execute slower on slower devices.

Dividing is slower (not prevalent in Luau). In C#, dividing is ~11x slower than multiplying, subtracting, or adding.

Micro-optimizations will add up. Many small things will eventually become quite significant, especially in bigger projects.

Not really. You wouldn’t say, do this:

local a = 10 * 0.081300813

over something like this:

local a = 10 / 123

The former is faster but isn’t readable.

2 Likes

Micro optimizations hardly save on any performance unless it’s in a fast running loop. The difference between not doing micro optimizations vs doing micro optimizations outside of fast running loops is almost nothing, which is not worth it. Optimization is a choice unless it becomes significant.

I actually am wondering as to why is this a better usage?

It should be noted that because dictionaries also count as tables, the table.create optimization is only meant for arrays and mixed tables with an array portion.

Some of these feel like obvious optimizations versus “niche” optimizations. But, if we should suggest any obvious optimizations, event-based yielding vs polling is an absolute-must.

Because Vector3.zero, xAxis, yAxis, zAxis and one are constants, AKA are only created once, but Vector3.new will create a new Vector3 every time you call it. Plus it’s just more readable…

1 Like

I would only connect non-lambda functions if I’m not using the same function anywhere else. Therefore, it’s just for that one connection. I would make the function defined if I want it for multiple events or if I want to call it. Is this also what you’re doing?

What about if your making a module?

-- which looks better?
local module = {}
function module:Run()

end

-- or
local module = {
    Run = function(self)
        
    end
}
-- I prefer the first one.
1 Like

Most likely the same thing applies, a module is really just a variable if you think about it, but it’s probably not too big of a performance difference and is probably only a few instructions faster.

This actually solves with my map rotational but the most confused thing i saw is

How is this a bad practice? like every script i did were using this one and i haven’t heard anyone talking about it too.

The engine will have to construct the CFrame from all three arbitrary X,Y,Z arguments, which is slower if it were to just construct it from one Vector3 constant.

It’s like a 0.01 second difference, but I think it’s worth it since it kind of improves readability in some cases.