Native Luau Vector3 Beta

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.

6 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

For some reason I have the beta feature but enabling it has had no effect. type still returns userdata, and, I see no performance improvements, not even within a few percent or so. Is this not enabled yet, or, do I need to do something to get it to work? I’m a little confused. Turns out I had to restart studio, I’m enrolled in the beta channel so the flag was already enabled but it only got enabled after start up.

But, this is absolutely perfect for a few issues I had with Vectors!

This is possible!! Hype moment, I’ve never been so excited over a data structure that holds three numbers :eyes: One question, will the following be possible with this change? I am hoping this will be the case as this is a pretty big reason the performance for some of my code is so slow. I reference cells by their position and not being able to map cells to positions in this way means I have to implement a cache system which is fairly slow.

local someTable = {
    [Vector3.new(1, 2, 3)] = 123
}
print(someTable[Vector3.new(1, 2, 3)]) -- I would love if this printed 123 and not nil

One problem I’m assuming this will fix was the memory usage and based on this announcement, that sounds like much less of a problem. Another was that vector operations were generally just extremely slow, especially accessing the X, Y and Z components of vectors. Again, sounds like this isn’t much of a problem anymore!

Overall this fixes a lot of the issues I had with vectors, and, I’m super excited to get testing and see what exactly this does to performance.

I do have one concern though, and, that’s that this could end up breaking my code sandbox if type ends up behaving differently for some other types. My code sandbox relies on a limited amount of types existing and if a new type comes about that I don’t take into account and I have no sufficient fallback this could allow for some really nasty sandbox escapes. The concern for me is if some type comes about that could be used to manipulate the sandbox, and then having no way to push a patch out to anyone that uses it. The sandbox is still in its early stages so I can certainly work around this in the future, but, yeah, that’s just one concern I have. (I could for example explicitly disallow use of unknown types by default so that the sandbox user would have to update to get support for it)

There appears to be some particularly poor performance when you generate a table with Vector3 keys that have “similar” / “close” values. My presumption is that there is a large number of hash collisions occurring. Here is an example

local start = os.clock()
do
	local _10 = 0
	while _10 < 250_000 do
		local i = _10
		local _12 = Vector3.new(math.random(1, 5000), math.random(1, 5000), math.random(1, 5000))
		positions[_12] = {
			Identifier = 0,
			VisibleFaces = {},
		}
		_10 = i
		_10 += 1
	end
end
local finish = os.clock() - start
print("Finish " .. tostring(finish))

If the upper-bound of the random is 5000, this takes even longer than the script exhaustion timeout period. If the upper-bound of the random is 50000, then the loop instead takes just 1.4 seconds. If you multiply the vector by 100, it takes even less time (just 0.5 seconds), presumably due to less hash collisions.

I may be off on my conclusion, but I think this is going to be a real issue for people.

3 Likes

Check the source code of the following game: WIP RoKarts [Alpha 1.8.5] - Roblox

The performance heavy code can be located in the “GeometryManager” module. It’s basically different intersection tests for geometric collisions.

The aforementioned CFrame-heavy code can be found in “KartRenderer”, I do quite a bit of CFrame operations to animate the player model with IK (specifically the UpdateAvatar function inside said module, I also tag this region of code in the microprofiler)

Also, if you do find any use at all in my place as some sort of benchmark or analysis, you should probably save a local copy of the place, since I am making daily changes that may or may not break things.

I started noticing this too interestingly!

When I tested, two Vector3 values would result in the same index in the table. For some reason, however, when I tried applying this to my terrain generator, not only did this fail a very large number of times, the performance was terrible. I saw my terrain gen go from taking between 0.9 and 1.9 seconds depending on the map size to 5.8 to 6.9 seconds. For reference, the number of index operations I’m doing is about 10k. Not sure what’s going on, but, Vectors really hate being table keys now and it seems that my previous excitement has wavered since its so much slower and doesn’t seem to work consistently.

Strangely, I got a small few attempts to work, and, the results were really really good, my generation speed got all the way down to 0.7 to 1.4 seconds, and, no issues with caching.

Edit: The caching issue was due to me rarely entering a CFrame into the first component of Vector3.new. Not sure what’s going on with that, I got no error, but, fixing this resolved the issue, though, I am still getting very poor performance. I got it back down to about 2.3 to 4.8 seconds now based on about 20 tests.

image
:flushed:

My solution to this was simple, for testing purposes, multiply your key values by a large number, say 10 million, and then when dealing with the keys, divide by 10 million. Then you will get more unique hashcodes, and the performance should be solid, more so than it was before with your old keying method.

1 Like

Thanks for the feedback, we will look closer at the hash function we use atm and make sure it works better.

1 Like

Very interesting. For now, I believe I can just use the following and calculate my own hash, which, shouldn’t be expensive since I already access the x y and z components:

local MAX_INT = 1e7 -- This is the best value I found without losing any precision from my testing
local MAX_INT_SQ = MAX_INT^2
local hash = x + y * MAX_INT + y * MAX_INT_SQ

This is pretty much what I was doing before, just with extra steps to prevent duplicate vectors, now I can just optimize things a good bit more.

2 Likes

This should be fixed next week.

2 Likes

Legacy code. If you pass something invalid to one of the arguments of Vector3.new it will interpret it as 0. Sadly, way too much code accidentally relies on this in one way or another so we can’t fix it.

3 Likes

Interesting. Maybe this could display a one-time warning in the console, it took me a minute to figure out and it seems kind of obscure. I accidentally found out that I was passing a CFrame and then I realized what was happening, but, that was probably lucky of me. I don’t really remember what the general rule of thumb is for when warnings should be displayed, but, I’m not too sure how else that could be signaled (if at all I suppose)

1 Like