When is Vector3.Unit and Vector3.Magnitude Computed?

Premise

I have been seeing a few discussions about the expensiveness of square root functions used in Vector3.Magnitude calculations, and it seems interesting that these are treated as properties of Vector3’s and not functions, because these values take some computational power if done hundreds of times.

As far as I know, Vector3.Magnitude is computed like this:

function GetMagnitude(vector3)
    return math.sqrt(
        vector3.X*vector3.X + 
        vector3.Y*vector3.Y + 
        vector3.Z*vector3.Z
    )
end

--alternatively:

function GetMagnitude(vector3)
    local sqvec = vector3 * vector3
    return math.sqrt(
        sqvec.X + 
        sqvec.Y + 
        sqvec.Z
    )
end

and Vector3.Unit is computed like this:

function GetUnit(vector3)
    return vector3 / vector3.Magnitude
end

And that this evaluates to true:

vector3 == vector3.Unit * vector3.Magnitude

Question

  1. Because of the fact that .Magnitude and .Unit are properties, does this mean that those properties are defined when the Vector3 is created/initialized, or are these properties only defined when they are indexed?
    The latter seems more sensible, but could be overlooked.
  2. Are these values cached in the property as well when calculated, if the latter is true?
    This also seems sensible, and it probably wouldn’t be overlooked.

Reasons for asking

If you were to use an optimized form of my GetMagnitude function up there without the square root and just squaring the number to compare to make magnitude checks:

function CompareMagnitude(vector3,number)
    local magnitude = vector3.X*vector3.X +
        vector3.Y*vector3.Y +
        vector3.Z*vector3.Z
    -- -1 = ">", 0 = "==", 1 = "<"
    return math.sign(magnitude - number*number)
end

It might be extra computation for no reason if the magnitude was already calculated, and extra computation for no reason is never a good thing. I might also change a few of my solution responses and change a few people’s opinions if this turns out to be true.

Magnitude is calculated once when you access the property the first time. This is pretty easy to check:

local t
-- we alternate evens/odds so that no internal caching messes up our results

t = tick()
for i = 1, 200000, 2 do
	local v = Vector3.new(i, i, i)
	local a = v.X
end
print(tick() - t)

t = tick()
for i = 2, 200000, 2 do
	local v = Vector3.new(i, i, i)
	local a = v.Magnitude
end
print(tick() - t)

t = tick()
local v = Vector3.new(100000, 100000, 100000)
for i = 1, 100000 do
	local a = v.Magnitude
end
print(tick() - t)
0.033289194107056
0.071850538253784
0.0049929618835449

The cost for creating 100000 Vector3s and accessing a low-cost property is line 1.
The cost for creating 100000 Vector3s and accessing Magnitude on each new Vector3 is line 2.
The cost for creating 1 Vector3 and re-accessing Magnitude 100000 times is line 3.

If Magnitude were recalculated every time, we’d expect line 3 to be approximately line 2 - line 1. Instead, we see that the cost is extremely small – it must be performing the calculations once and saving them to the Vector3 object. This is likely why Magnitude and Unit are properties, so that their values can be cached after the first access.

These results actually vary if you run them multiple times or don’t alternate evens/odds. There seems to be some internal, deeper caching for Vector3s or the Magnitude/Unit properties. If you run this twice in a row really quickly, line 2 can actually be the same as or smaller than line 1!

These results do not vary significantly if you run them once alone to eliminate any effects of an internal cache.

Both the results from a stand-still and from running multiple times suggest the presence of an internal cache, so you don’t need to worry about Magnitude and Unit being recalculated on every access! The results were similar when Magnitude was replaced with Unit.

6 Likes

I would add some more to @Corecii’s great response

Vector3’s are userdata objects and unlike tables don’t have fields that can be accessed. They represent an interface with C code and every access to them is controlled by a metamethod. In the case of Vector3’s, they actually cache the magnitude and unit versions of themselves. (someone recently asked about making pseudo Vector3’s on here, reading the discussion we had on computing .magnitude and .unit may be enlightening.)

Now, Vector3’s are immutable objects. They cannot be changed. Adding them together always yields a new Vector3, not editing one of the operands and never the same as the result of a previous operation. The only reason the == equality operator works on Vector3’s is because it compares them value by value. However, if you were to compare them by identity rather than value, you will see they are different:

local t = {}
local a = Vector3.new(0, 1, 0)
local b = Vector3.new(0, 1, 0)

t[a] = true
print(t[b]) --> nil
print(a == b) --> true

t[a + b] = true
print(t[a + b]) --> nil
print((a + b) == (a + b)) --> true

This means that even though you are working with a Vector3 of the same value, the results are not cached and will need to be recalculated if indexed.

5 Likes