(lol this ended up longer than I meant it to, but I guess I had a lot to say on the topic)
While this is technically correct that there is a hit on performance, I think calling it an INSANE hit is overselling a bit.
I wrote a benchmark to measure this with these results over a million iterations
Benchmark Code
task.wait(5) --let's roblox calm down
local ITERATIONS = 1e6
local HEAVY = 500
------------------------------[[META WORK FUNCTIONS]]--------------------------------------------
local meta = {internalCount = 0}
meta.__index = meta
function meta:InternalAdd()
self.internalCount += 1
end
function meta:HeavyInternal()
for i=1, HEAVY do
self.internalCount += 1
end
end
------------------------------[[FUNCTIONAL WORK FUNCTIONS]]--------------------------------------------
function Add(t)
t.internalCount += 1
end
function HeavyAdd(t)
for i=1, HEAVY do
t.internalCount += 1
end
end
------------------------------[[DRY AS MUCH AS POSSIBLE WITHOUT SKEWING RESULTS]]--------------------------------------------
local function measureTime(str, func)
local t = os.clock()
func()
local r = os.clock() - t
print(str .. (r))
task.wait(2) --let's let roblox do whatever it needs to before the next test
return r
end
------------------------------[[LIGHT WORK BENCHMARKS]]--------------------------------------------
local obj = setmetatable({}, meta)
local metaA = measureTime("metatable counter: ", function()
for i = 1, ITERATIONS do
obj:InternalAdd()
end
end)
local t = {internalCount = 0}
local funcA = measureTime("function counter: ", function()
for i = 1, ITERATIONS do
Add(t)
end
end)
t = {internalCount = 0} --Just for completeness
local inline = measureTime("inline: ", function()
for i = 1, ITERATIONS do
t.internalCount += 1
end
end)
------------------------------[[HEAVY WORK BENCHMARKS]]--------------------------------------------
local metaB = measureTime("meta heavy internal: ", function()
for i = 1, ITERATIONS do
obj:HeavyInternal()
end
end)
t = {internalCount = 0}
local funcB = measureTime("heavy function: ", function()
for i = 1, ITERATIONS do
HeavyAdd(t)
end
end)
--I don't bother with inline heavy because the result is basically just the same story as comparing function vs meta
------------------------------[[LET'S PUT IT INTO A MULTIPLIER]]--------------------------------------------
print("\n\n")
print(string.format("inlining is %.4fx faster than functions and %.4fx faster than meta", funcA / inline, metaA / inline))
print(string.format("light work functions are %.4fx faster", metaA / funcA))
print(string.format("heavy work functions are %.4fx faster", metaB / funcB))
RESULTS
metatable: 0.01828370000293944
function: 0.011764800001401454
inline: 0.0067952999961562455
metatable heavy: 3.3275229999999283
function heavy: 3.301007699992624
inlining is 1.7313x faster than functions and 2.6906x faster than metatables
light work functions are 1.5541x faster
heavy work functions are 1.0080x faster
So the end is the most important part. Worst case scenario for metatables, the direct function equivelant is 1.5541 times faster here. But when there is enough work inside the function to normalize the results, the speed gain drops quickly as you would expect (1.0080x faster with a 500 item loop inside). Most of the time spent in these lightweight functions, unsurprisingly, is the actual function call itself. Ultimately though I am claiming that the performance cost here is negligible except tight loops and systems that do a ton of work. Most of the time though when people are using an OOP style they are keeping those systems that do a ton of work optimized just encased in the object or as a component housed in the object. And when they don’t more often than not the best place to focus on optimizations are things like acceleration structures, not the way the functions are being called.
This is not true. Programming is for getting a computer to do a task within the budgets set by the project. For a video game the budgets tend to often be hitting tight timelines for getting frames out at the target framerate. While a higher target framerate is often good realistically anything 60 and above is fantastic and you only need 30fps for it to be playable. More is better though. The trouble is what limits framerates is often some bottleneck at very specific points and more often than not it’s something running at a time complexity of note. Best to spend your time on any bottlenecks preventing targets from being hit or working on new features or something else if the game runs at targets.
If you don’t have any cpu idle time at all and are not hitting perf targets then yes. If you are exceeding targets or have idle time then any extra headroom is usable space for unoptimal algorithms so long as they still are hitting target. Often the unoptimized approach is easier to write.
Micro optimizations can be very readable for sure, but not always. And they definitely can limit extensibility if they drive you to make choices that are not easy to extend or end up tightly coupled. Loose coupling often(not always) necessitates some level of separation that could be sped up were it not there for example which means it will be harder to add new features or swap out parts if you focus only on performance.
Inheritance is evil and should never be used. I’m kind of joking there, but I hate having anything inherit from anything else. I much prefer to keep my objects as encapsulated containers that hold things they are composed of and let each container have systems that manage how they run. My OOP style is very close to ECS in a lot of ways. I only care about OOP so far as encapsulation goes and polymorphism sometimes. OOP like syntax is useful because it cleanly and logically groups components while coming with built in decoupling since for anything using my object, it uses my interface. You can almost consider my OOP style as a facade over ECS (though my style often doesn’t directly fall under ECS). But that’s it, that’s why I’ll continue using it, and recommending it, despite the fact some things are technically faster. This is basically how I program in cpp too, that just has the benefit of doing this without the overhead.