Native Luau Vector3 Beta

Also, regarding native CFrame, wouldn’t they already get some sort of performance boost? Since they are really just glorified Vector3s.

3 Likes

This is amazing! My capsule collider that’s designed with a triangle-vs-capsule algorithm uses a lot of Vector3 math, now it’s way faster!

Comparison:

Old Vector3 behavior:
image

New Vector3 behavior:
image

16 Likes

I’ll be honest, I always treated Vector3 like a stack-allocated value. It caught me off guard when I read they were heap-allocated! This is unquestionably a change for the better and I look forward to its complete implementation. Are there any other “value types” (I put quotes for this exact reason) that are still heap-allocated which will receive this change?

It’s awesome to see steps being taken to optimizing things like this. Are there any plans on improving GUI performance? (Stuff like not re drawing everything when a property is updated)

I’ve found UI performance to be limiting me many times recently, and I’d really like to see some work done on it.

1 Like

Im curious why Roblox datatype objects have their properties locked. Wouldn’t it be more efficient to have them unlocked so they can be reused without having to create an entirely new Vector.

We’ve already seen this is possible with RaycastParams

2 Likes

We do have plans to improve Vector2/CFrame performance as well as other Roblox types in the future, but those will use a different implementation strategy.

20 Likes

I hope the same optimization will be done to other value types like CFrames, UDim2 and Vector2.
The performance gains are quite significant:

local cl = os.clock()
local vec3 = Vector3.new()
for i = 1, 10^7 do
	vec3+=Vector3.new(i,i,i)
end
print("Vector3 addition: ", os.clock()-cl)


local cl = os.clock()
local vec2 = Vector2.new()
for i = 1, 10^7 do
	math.random(0,i)
	vec2+=Vector2.new(i,i)
end
print("Vector2 addition: "..os.clock()-cl)


local cl = os.clock()
local cf = CFrame.new()
for i = 1, 10^7 do
	cf+=Vector3.new(i,i,i)
end
print("CFrame/Vector3 addition: "..os.clock()-cl)


local cl = os.clock()
local cf = CFrame.new()
for i = 1, 10^7 do
	cf*=CFrame.new(i,i,i)
end
print("CFrame Multiplication: "..os.clock()-cl)

The resulting benchmarks with the old Vector3s:

Vector3 addition:  1.6341197000002 
Vector2 addition: 1.8228185999906  
CFrame/Vector3 addition: 1.6889015000197 
CFrame Multiplication: 1.8462797999964 

The same with the new Vector3s:

Vector3 addition:  0.26172300000326
Vector2 addition: 1.8144201000105
CFrame/Vector3 addition: 1.0896642000007
CFrame Multiplication: 1.8471370000043 

Vector2 maths are now significantly slower than the native Vector3s.
CFrame math is quite a bit faster as long as it involves Vector3s but are otherwise just as fast/slow as before.
I hope that that the same/similar optimizations will be done to CFrames especially.

13 Likes

This is actually a good post.

local p = Instance.new("Part")
p.Position = Vector3.new(math.random(1, 10), math.random(1, 10), math.random(1, 10))
print(rawequal(p.Position, p.Position))

Since Vector3 is now a native type I get true in the console, since __index is not returning a new Vector3 each time (old behavior). This could (and I think Roblox should) allow for mutability of Vector3, since part.Position.[X/Y/Z] = num would actually do something now (with the old behavior it would seem like it does nothing, since it would just edit the new Vector3 since it isn’t cached).

2 Likes

CFrames can’t have the exact same thing done unfortunately: Vector3s are small enough that we can afford to fit them inside of the same storage as the other value types, but CFrames are significantly larger (4x as large as the next largest type), so they would bloat up the size of every other value type if they used the exact same approach that Vector3 does.

6 Likes

How does this affect garbage collection? Since they are no longer traditional Userdata, do they behave like strings where new Vector3s created during runtime are subject to GC but ones loaded in as constants kinda just sit? Are they even loaded as constants?

2 Likes

Vector3 being a native type gets stored like a number, 'tisn’t a separate object but is stored as part of the value.

local a
a = 1 -- 1 is stored in a
a = 2 -- 2 is stored in a
-- the numbers aren't separate objects that are pointed to by a
-- they are contained in a

If Vector3 were to become mutable, then storing the Vector3 inside of the value wouldn’t be possible, it would need to be a different object which is referenced by the value. Essentially, if Vector3 was mutable then there would be little if any benefit to making it a special type distinct from userdata.

local a = Vector3.new(1,2,3)
local b = a

-- with Vector3 being a value type, b holds the same data as a

-- with Vector3 being mutable, then b references the same object as a
-- b can't reference a, because of lifetimes of variables not being dependant
-- upon references like normal objects

Vector3s being a native type like this means that they aren’t cached in the same way that numbers aren’t cached, they both hold the same data (but not references to the same objects or something).

Caching and mutability are also a very weird combination, if vectors were mutable but cached then would updating one vector update all vectors with the same contents?

local v1 = Vector3.new(1,2,3)
local v2 = Vector3.new(1,2,3)
local v3 = v1
print(rawequal(v1,v2)) -- assume true, because of caching
print(rawequal(v1,v3)) -- always true
v1.X = 2
print(rawequal(v1,v2)) -- would this be true?
print(rawequal(v1,v3)) -- always true

rawequal(v1,v2) on a stateful object implies that updating v1 will be observed by v2.

A modification to a Vector3 updating an Instance is also very weird (why does Vector3 need to manipulate state of other objects??).

Finally. I can’t tell you amount of times I have been disappointed by user data returning instead of the actual type. This will make life much easier. Been a fan of the changes recently.

This is so underrated. This is incredibly useful.

Vector3 with NaN component(s) being used as a table key is very weird with this change.

local t = {}
local v = Vector3.new(0/0,0/0,0/0)
t[v] = 1

print(t[v]) --> nil
print(next(t)) --> -nan(ind), -nan(ind), -nan(ind) 1
for _ in next,t do end -- nothing (Luau optimizations i presume)
for _ in next,t,nil do end -- invalid key to 'next'
print((next(t,(next(t))))) -- invalid key to 'next'
while next(t) do t[next(t)] = nil end -- infinite loop

Vector3s with NaN components should either error when using them as the key in assignments (like when using NaN as a key in assignments), or there should be a special case for NaN components (for compatibility).

4 Likes

We have a fix for this in the pipeline, which indeed generates an error when you use it. I think it ships next week.

3 Likes

I believe they plan on doing what they did here on Vector2s (as they are very similar) if all goes well here. Vector2s still currently use the old system.

For part.Position.X = 1 to do what you want it to do, it needs to translate to a mutation of Position property. There’s no way for us to implement this - part.Position isn’t a reference to the internal part’s memory, it’s a computed property. Immutability of Vector3 is good for both reference types and value types, but for slightly different reasons; you will see languages like C# that have value types (structs) strongly recommend immutable structs for much of the same reason, as in C# when you say foo.Bar.Y = 5, and Bar is a struct, you aren’t mutating a field of foo - you are mutating a copy of that field.

5 Likes

Very very excited for this to roll out! Don’t have anything meaningful to contribute, but my game is very math heavy and will definitely benefit performance-wise in pretty much every area of code. My most intensive algorithms are all written in x y z variables, which is obviously very bad for readability.

I really hope CFrames can get similar treatment soon, too. If that happens, things like welds and character rigs will become significantly faster to update, making a lot of complex animation structures way more viable performance wise. I do a lot of CFrame operations with my Rthro IK system, which takes a substantial amount of time, so def looking forward to native CFrames or something like that.

2 Likes

If you have performance-heavy code that uses vectors/cframes, please consider submitting it to us. We will use it for science.

(that is to say, our performance improvements are usually motivated by specific examples, so it would help us understand the remaining deficiencies wrt cframe math if we had a non-synthetic example of code that is performance intensive)

4 Likes

Is caching a method of a Vector3 and using it on an unrelated Vector3 affected by this?

For example, I think there may be a couple scripts in my game that do something like this:

local Dot = Vector3.new().Dot
-- ...
local a = Dot(b, c) -- where b is not the same vector from where Dot was extracted

Will this still behave as it did previously? Is this even good practice? Does this even boost performance like it used to?

I am just wondering if I need to purge my code of this. Thanks.

This should still work. Performance-wise it should be more or less equivalent to b:Dot(c).

1 Like