I’ve always used OOP for my games and frameworks, the traditional:
local foo = {}
foo.__index = foo
function foo.bar()
local self = setmetatable({}, foo)
return self
end
return foo
But I’ve been more active on the DevForums recently, and I’ve seen mentions of this style of programming being poor for LuaU due to the language not being designed for it, citing performance issues among other things. Is there much truth to this? Should I adopt another method or am I likely fine?
Right, I’m fully competent on metatables, and I apologize if my post doesn’t fully explain what I’m trying to say.
I’m wondering if this traditional way of setting up Object-Orientated Modules has issues with performance, and the severity of them if so. I’ve heard that LuaU isn’t a natively Object-Orientated programming language and this is a cheap workaround to get that functionality.
OOP isn’t inherently bad practice or wrong. OOP can greatly help with organization and the benefit of organization typically overpowers any performance overhead.
Using long inheritance chains is typically not recommended. You can read more on this at:
If OOP assists you in creating clean and scalable code, by all means, go for it!
There’s also another programming paradigm called ECS which I believe is an alternative to OOP and has less performance overhead but I don’t have much knowledge nor experience on ECS so take whatever I say about this with a grain of salt but I still recommend you check it out.
@Yarik_superpro could probably provide more information on ECS.
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))
And the output
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 there is a cost to using it. But the more work a function does the less it matters. In the worst case it does take ~1.5x longer than not using a metatable, but it’s not bad. And the more work you do inside the function the less this matters too since the work outweighs the call cost. Even with a 1.5x though, that cost is mostly negligable unless you are trying to get every single ounce of performance out. I should also note that you can run over 5 million per second on a single thread of my computer to give a bit more context. (7.7mil for the direct function version but remember this is if you are basically doing nothing in the functions, the difference gets smaller the more work you do)
(inline is just not calling a function and running the code directly since a function call has quite a bit it needs to do)
Also this benchmark does use only 1 deep metatables. I haven’t tested but I would imagine it scales linearly at about the same rate as the difference between a straight function call and a metastable call per layer you have.
tldr: don’t worry about it too much, if this is the difference between it being playable and not you probably have bigger issues.
Also I guess I’ll briefly answer this too. Entity Component System (ECS) is about Entities (containers) that have components and systems that work with components.
So an entity is like an object almost if the object was purely composition. Often times an entity is literally just treated as a container and the components inside drive what happens completely. It really just holds the components and sometimes has ways to query for specific components or groups inside it.
A component is generally a piece of data that holds something like state. These are usually broken up into logical states and stored in a continuous array. So like say we make a Health Component for each Creature in our game. Creatures would be a container holding the health component.
And systems are things that work with components. Like regenerating health might be a system that just takes health components and adds stuff to them. If the components are stored in a flat array this is very, very fast to do. A lot of more serious ECS patterns use a lot of work to really speed up how fast you can send the correct components through systems. And have a fast way to query combinations of components that an entity may hold.
This is a pattern that is very flexible (composition) and can be very fast because it is optimizing data access patterns based on how things tend to use them in groups.
Note though that if you are not updating values in bulk, entity component systems aren’t specifically faster. They work best when you are applying a system to a whole set of objects as that’s what they are generally optimized for.
It also doesn’t hit the minor overhead of metatables because they don’t tie the table to the object. Though for this in particular, you don’t strictly have to use ECS for that, you simply have to keep the function table and data tables separate for that benefit. Though if you start optimizing the layout of the data tables you basically just hit ECS anyways even if you aren’t using it in the most optimal patterns it shines in.
And since I just mentioned layout, it’s worth noting that memory layout optimization doesn’t help a ton in lua(u), but because of the specifics of how CPUs work it is very impactful in other languages like c/c++. You just don’t have enough control over lua for that to matter that much, but it’s part of the draw of ECS if you dig deeper. (If you want to know more about why data layout can matter so much, look into “cpu cache”, but remember it won’t help with lua programming)
Would a shiny rock revive someone dead? Hence why I don’t think organization can revive a dead optimization
ECS is an extension of functional programming, de facto functional programming with a state.
It does not have any performance overhead when done right (since it has no bloat)
There’s often better ways without it, and as Luau doesn’t support opaque types and macros, a lot of those “better ways” are more cumbersome. So, it really depends: Is this a place where you can sacrifice performance? Then go for it. But if you need blazing performance, try to find a solution without metatables. That’s not to say metatable OOP can’t be performant, just that the overhead of a metatable makes it slightly harder to make performant. And the more inheritance the less performant.
Luau is a wonderful language that gives you many ways you can approach problems with. You can try a lot of different solutions to a certain problem, and see if one works the best within your own system.
It’s not a matter of “Are prototype based classes bad?”, it is “Are prototype based classes beneficial for my system?”. Do you see yourself using a certain paradigm even though it poses no benefits to your system? It will simply make the implementation harder for you.
I personally do not have sentimentality (unlike some other users) towards a certain paradigm. I use almost all of them in different projects and environments. You have to know almost all of them to a certain extent if you wish to land a job in today’s industry.
So the answer to your question is, it is only bad practice if you do not know your alternatives and refuse to grow up. Be it OOP, be it ECS. Do not be like the others.
I’ve worked on a pretty big game and we used this sort of OOP. We’ve never had a problem with performance, since we had our own custom version of StreamingEnabled that one of our scripters made.
I think in some cases, especially on big games, it’s much more practical to use this or even impossible not to use it.
Programming isn’t a contest to see who can read the most crappily-written code. Conventions say you shouldn’t make your variables two letter abbreviations or placeholder letters because it’s not clear to someone who hasn’t worked on it what it means. It would get you points deducted on a computer science exam.
Also, no one is going to help you if you don’t tell them what the problem is. That is why the standard format for posts on here isn’t just “can you help me with this [code block].”
You also shouldn’t hijack someone else’s post. You can create your own or if you just want a specific person’s help, you can send them a message.
I know that you want to use it to avoid marketplace regex, isnt it?
Nuh uh I’m not helping that lil bro. I remember you talking about that in another discussion.
Bro, you are just weak; you just can’t handle his sigma using instance metamethods dirrectly
You absolutely know no optimization and the great new luau order.
I think I was the person that started the whole ‘OOP doesn’t belong in Lua’ thing.
Performance of this idiom isn’t bad, and the Luau VM was actually designed with it in mind, so you realistically shouldn’t have to care. The only time performance would even matter is with per-frame code, which you wouldn’t want to allocate any tables in regardless.
The more compelling reason I think people should shy away from this style is because it just has bad type-checking support. You’ll have to sprinkle a bunch of type annotations everyone to make it work, and even then, you’ll run into trouble with things like subtyping because Luau has bad recursive type support. Sticking with simple functions and tables seems to be better supported.
The other side of the coin is that I think this style promotes bad organization hygiene, which is ironic because that’s what you’re taught OOP solves. You tend to cross-reference objects around and break encapsulation which leads to hard-to-reason-with code. For this I tend to stick with pure functional programming, which alleviates most of my needs for imperative data structures and is easier to understand.