The app draws according to the user input which creates a single vector, from there you calculate everything using that one vector that was created. That’s different from creating a new vector for every single operation which is extremely unrealistic.
Try adding “–!optimize 2” at the top of your script, which is what the game normally runs with. Studio has optimization level 1 by default.
Metatables being applied whenever an object is created doesn’t work the same with natively builtin types as it does with actual tables which require you to set their metatable every time you create a new table.
It creates a single vector for every user input. But sure.
For the sake of testing faster, I’ve decreased number of iterations from 100 million to 10 million (I’m not willing to wait a whole minute for a test .-.).
Testing for: two numbers
Took a total of 0.9112083129584789ms for 10000000 iterations.
With an average of 9.112083129584789e-08 ms/iteration.
Testing for: Vector3
Took a total of 3.0777601695153862ms for 10000000 iterations.
With an average of 3.077760169515386e-07 ms/iteration.
Still slower. Though, not as slow as before. I’ve ran all these through the command bar. Just to be sure that in-game it wouldn’t be any different, I ran the game (and ofc added a wait before to exclude Studio loading time). Here are the results:
Testing for: two numbers
Took a total of 0.8625850056996569ms for 10000000 iterations.
With an average of 8.62585005699657e-08 ms/iteration.
Testing for: Vector3
Took a total of 3.0970254158601165ms for 10000000 iterations.
With an average of 3.097025415860116e-07 ms/iteration.
Still slower.
I doubt you and I would know how they actually work. since we don’t have access to their source code, so we better not make assumptions about whether or not they use metatables.
Wait, slight correction. The last code I tested (with --optimize 2) had Vector3s being constructed for every operation, apologies. Here are the tests “fixed”:
Testing for: two numbers
Took a total of 0.8814006745815277ms for 10000000 iterations.
With an average of 8.814006745815277e-08 ms/iteration.
Testing for: Vector3
Took a total of 1.2641102833440527ms for 10000000 iterations.
With an average of 1.2641102833440527e-07 ms/iteration.
And in-game:
Testing for: two numbers
Took a total of 0.8169687063200399ms for 10000000 iterations.
With an average of 8.1696870632004e-08 ms/iteration.
Testing for: Vector3
Took a total of 1.1896188297541812ms for 10000000 iterations.
With an average of 1.1896188297541813e-07 ms/iteration.
No? Vector3 is a part of Roblox, not luau. If you try running Vector3.new using the luau exe, it would error.
I would like to see yours, too.
Code (to avoid bloating the message)
--!optimize 2
local ITERATIONS = 10_000_000
type Fn = () -> ()
local function timeIteration(fn: Fn)
local start = os.clock()
fn()
local finish = os.clock()
return finish - start
end
local function timeFunction(name: string, fn: Fn)
local totalTime = 0
for i = 1, ITERATIONS do
totalTime += timeIteration(fn)
end
print(`Testing for: {name}`)
print(`Took a total of {totalTime}ms for {ITERATIONS} iterations.`)
print(`With an average of {totalTime / ITERATIONS} ms/iteration.\n`)
end
timeFunction("two numbers", function()
local x = 1
local y = 1
x += 10
y += 10
x -= 10
y -= 10
x *= 10
y *= 10
x /= 10
y /= 10
end)
local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)
timeFunction("Vector3", function()
position += offset
position -= offset
position *= offset
position /= offset
end)
I’m not sure why you’re assuming that but I can help you understand how it actually works.
As you might know a few years ago there was an announcement about how Vector3 was added as a built-in type. This specifically mentioned that it was already added to Luau for a while, but Roblox just hasn’t been using it with Vector3 yet:
Internally, our VM has supported a built-in ‘vector’ value type for some time now with built-in vector operations, but the Roblox Vector3 userdata class didn’t use that machinery.
Luau currently does not allow you to create this internal data type as it does not offer a standard library yet for constructing this data type. Though internal functions for doing so do exist, as you can find in the language’s source code
This constructs an object without any metatables directly attached. Roblox attaches a metatable to all vectors initially by doing a single internal setmetatable call on a vector all the way at the start, which causes the global metatable for all vector types to be set to whatever metatable Roblox passes. Because this metatable is global, no other setmetatable calls are required to add the behavior required to make vectors work like they do in Roblox. That’s just because only tables and userdata can have their own metatable, all other data types share a single global metatable.
Now to explain why your benchmark code is giving different results, I’ll just give you my benchmark so you can test it for yourself. I rewrote it quickly with some comments to explain certain decisions since I’m not home right now.
my benchmark code
--!optimize 2
local ITERATIONS = 1e8
local function testNumber()
-- define the numbers in such a way that luau considers them "variable" instead of constant
-- this would normally be optimized which isn't realistic for a real world scenario
local x, y; x, y = 0, 0
local ox, oy; ox, oy = 1, 1
for i = 1, ITERATIONS do
x = x + ox
y = y + oy
x = x - ox
y = y - oy
x = x * ox
y = y * oy
x = x / ox
y = y / oy
end
return x, y
end
local function testVector()
-- because
local vec; vec = Vector3.new(0, 0)
local vecO; vecO = Vector3.new(1, 1)
for i = 1, ITERATIONS do
vec = vec + vecO
vec = vec - vecO
vec = vec * vecO
vec = vec / vecO
end
return vec
end
local function bench(name, func)
local start = os.clock()
func()
local time = os.clock() - start
print("benchmarking " .. name)
print(string.format("\tresult: %-.2f nanoseconds", time / ITERATIONS * 1_000_000_000))
end
bench("number", testNumber)
bench("vector", testVector)
I wouldn’t argue about the VM and actual Luau code, since I personally am not a C++ person. But you mentioned that Luau doesn’t allow me to create that type:
So basically it’s not there?
Looking at your testing code, the implementation is different, but the core code seems to be exactly the same?
How are the results different? I tried editing my code to match yours yet the results stayed the same; Vector3s were slower.
There is no library for it that you can directly access, but there are internal functions that can be used to create this built-in vector types. That’s just how it is but this might change soon.
The difference is that the benchmark uses a single big for loop instead of functions, which have a function call overhead each time you iterate. This overhead makes it harder to see the difference between the two and might cause the benchmark ran after to run slower because of reasons I wouldn’t be able to explain properly. Also doing it this way allows us to find out how long the benchmark took in nanoseconds a lot more accurately, since os.clock is otherwise not precise enough to measure the time difference. Your benchmark made numbers and vector operations take somewhere around the 50 to 100 nanoseconds, which isn’t actually the time it takes to execute these operations in practice, unless you had a function which only did this operation and nothing else.
I’m aware of function overheads, but the overhead is found for both of them, which equals out, but either way, I also checked the function overhead when trying to match your code, results:
Testing for: two numbers
Took a total of 0.29839400004129857ms for 10000000 iterations.
With an average of 29.839400004129857 ns/iteration.
Testing for: Vector3
Took a total of 0.7638640000950545ms for 10000000 iterations.
With an average of 76.38640000950545 ns/iteration.
Code
--!optimize 2
local ITERATIONS = 10_000_000
type Fn = () -> ()
local function timeIteration(fn: Fn)
local start = os.clock()
fn()
local finish = os.clock()
return finish - start
end
local function timeFunction(name: string, fn: Fn)
local totalTime = 0
--for i = 1, ITERATIONS do
totalTime += timeIteration(fn)
--end
print(`Testing for: {name}`)
print(`Took a total of {totalTime}ms for {ITERATIONS} iterations.`)
print(`With an average of {totalTime / ITERATIONS * 1_000_000_000} ns/iteration.\n`)
end
timeFunction("two numbers", function()
local x = 1
local y = 1
for i = 1, ITERATIONS do
x += 10
y += 10
x -= 10
y -= 10
x *= 10
y *= 10
x /= 10
y /= 10
end
end)
local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)
timeFunction("Vector3", function()
for i = 1, ITERATIONS do
position += offset
position -= offset
position *= offset
position /= offset
end
end)
I heavily recommend both parties to use Boatbomber’s Benchmarking plugin when measuring performance on such an infinitesimal scale. Here are the results from the benchmarker:
--!optimize 2
return {
ParameterGenerator = function()
return function() end
end,
Functions = {
["Two numbers"] = function(Profiler)
local x = 1
local y = 1
for i = 1, 100_000 do
x += 10
y += 10
x -= 10
y -= 10
x *= 10
y *= 10
x /= 10
y /= 10
end
end,
["Vector3"] = function(Profiler)
local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)
for i = 1, 100_000 do
position += offset
position -= offset
position *= offset
position /= offset
end
end,
},
}
timeFunction("Vector3", function()
local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)
for i = 1, ITERATIONS do
position += offset
position -= offset
position *= offset
position /= offset
end
end)
It’s slower because you defined the vectors as upvalues which make them a lot slower to access.
Ah. This final edit indeed makes it faster. Pretty interesting. Tbf I’m not sure which of those to count as a more real-world situation, as the performance, as we saw, heavily depends on how you define and use the vectors. Now for the final and arguably a very important issue: API. Using Vector3 will be a bit weird, don’t you think? If there’s a good way around it, we’ll finally do the switch to it. I tried with Vector2s and they seem waaaaaaaaaay slower, so they’re out of the question,
You can always consider internally using Vector3, and still allowing the user to pass regular numbers instead. You should just test things out and see what ends up being most performant. It might not work well everywhere but the ones that can make use of it could probably get a pretty good performance boost out of it.
This update contains an internal rewrite of the library, allowing more modularity. Also switch to using the brand new buffer as a pixel storage, yielding MUCH better performance (200%)!