Lua vector library

I decided to create a vector library for ROBLOX. I tried to optimize it as best as I could. Thoughts?

I was assisted by AI to create this, but not the whole thing. About 85% of it is my work.

local sqrt = math.sqrt
local max = math.max
local min = math.min
local acos = math.acos
local cos = math.cos
local sin = math.sin

vector = {}

-- Create a new vector
function vector.new(...)
	return {...}
end

-- Add two vectors
function vector.add(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = {}
	for i = 1, #v1 do
		result[i] = v1[i] + v2[i]
	end
	return result
end

-- Subtract two vectors
function vector.subtract(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = {}
	for i = 1, #v1 do
		result[i] = v1[i] - v2[i]
	end
	return result
end

-- Dot product of two vectors
function vector.dot(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = 0
	for i = 1, #v1 do
		result = result + v1[i]*v2[i]
	end
	return result
end

-- Magnitude of a vector
function vector.magnitude(v)
	local result = 0
	for i = 1, #v do
		result = result + v[i]*v[i]  -- Use multiplication instead of ^2 for efficiency
	end
	return sqrt(result)
end

-- Scalar multiplication
function vector.scalarMultiply(v, scalar)
	local result = {}
	for i = 1, #v do
		result[i] = v[i] * scalar
	end
	return result
end

-- Vector normalization
function vector.normalize(v)
	local mag = vector.magnitude(v)
	if mag == 0 then return v end
	return vector.scalarMultiply(v, 1/mag)
end

-- Cross product
function vector.cross(v1, v2)
	assert(#v1 == 3 and #v2 == 3, "Cross product is only defined for 3D vectors.")
	local result = {
		v1[2]*v2[3] - v1[3]*v2[2],
		v1[3]*v2[1] - v1[1]*v2[3],
		v1[1]*v2[2] - v1[2]*v2[1]
	}
	return result
end

-- Distance between two vectors
function vector.distance(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	return vector.magnitude(vector.subtract(v1, v2))
end

-- Angle between two vectors
function vector.angle(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	return acos(vector.dot(v1, v2) / (vector.magnitude(v1) * vector.magnitude(v2)))
end

-- Projection of v1 onto v2
function vector.projection(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local v2_normalized = vector.normalize(v2)
	local projection_length = vector.dot(v1, v2_normalized)
	return vector.scalarMultiply(v2_normalized, projection_length)
end

-- Vector rotation
function vector.rotate(v, angle)
	assert(#v == 2, "Rotation is only defined for 2D vectors.")
	local x = v[1]*cos(angle) - v[2]*sin(angle)
	local y = v[1]*sin(angle) + v[2]*cos(angle)
	return {x, y}
end

return vector

5 Likes

Although no thoughts for the uses of the module but I have some recommended optimization methods.

You should replace all the loop with ipairs and not for i = 1, #v1 do because in real performance and benchmarking test it perform worse than ipairs and without using ipairs by 0.0001s when testing with 10000 elements and on the official luau performance said Iterating through array-like tables using for i=1,#t tends to be slightly slower because of extra cost incurred when reading elements from the table..
And even for more optimization consider returning the value itself instead of creating a local variable and return it, since the engine will have to allocate a memory address for the local variable itself. I think that all I know to tell

1 Like

Did some testing, made an add function about, eh, 5% faster.

function vector.add(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = {}
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end
2 Likes

I just love seeing number goes down

2 Likes

I think I should stop looking into optimization but it seem like I have gone too far. Digging through luau performance site and posts on devforum again I found some more ways to optimizes some parts of your codes. Still you should consider if it worth implementing and sacrificing readability if the program need to be as blazingly fast to handle large amounts of operations.

Here are the results I gathered do notes that the comment --unstable meant that the function can result in faster operation but also slower operation and the chance are varies.

In my testing with my current specs i5-12400f and 3050 with standard ram amounts and empty roblox baseplate, the slow operation seem to show at the 4-15th run about 0.00001 ~ 0.0000098 slower than the other run and fast operation happens at ?-? run, I think it rare to happen but it once get to ~0.0000077, better go with optimized version 5 (add5).

Testing outside of roblox environment shows a different result maybe it because I have to create a mock up of vector3, the optimized version 3 (add3) is faster than the other. Although from luau v0.605 onward have let you to run luau-compile with --vector-lib I still don’t know if I did it right and I don't know how to uses it anyways.

So anyways here the codes that I used for testing and optimization, there methods like local xnil xnil = nil explained here and other recommended methods taken from the performance site in luau likes Creating and modifying tables using table.create, some unnecessary table index cleaning and reusing, opted into using if then expression to error than assert because of assert problem, going back into numeric loop because it faster and the amounts of elements is calculated and need to reuse for more optimization. Codes are run with --!native and --!optimize 2

Roblox Studio Code:

--!native
--!optimize 2
local clock = os.clock

local am = 1000

local create = table.create

vector = {}

-- Add two vectors
function vector.add(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = {}
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add1(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = create(#v1)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add2(v1, v2)
	local v1A = #v1
	assert(v1A == #v2, "Vectors must be of the same length.")
	local result = create(v1A)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add3(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

local xnil xnil = nil

-- Add two vectors
function vector.add4(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add5(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i = 1, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

local ind = 1

-- Add two vectors
function vector.add6(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add7(v1, v2) -- unstable
	local v1A = #v1
	if v2[v1A + ind] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add8(v1, v2) -- unstable
	local v1A = #v1
	if v2[v1A + ind] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

local random = math.random

local v1 = create(am, Vector3.new(random(),random(),random()))
local v2 = create(am, Vector3.new(random(),random(),random()))

print("og")
local elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op1")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add1(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op2")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add2(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op3")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add3(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op4")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add4(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op5")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add5(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op6")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add6(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op7")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add7(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op8")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add8(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

VS-Code:

--!native
--!optimize 2
local clock = os.clock

local am = 1000

local create = table.create

vector = {}

-- Add two vectors
function vector.add(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = {}
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add1(v1, v2)
	assert(#v1 == #v2, "Vectors must be of the same length.")
	local result = create(#v1)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add2(v1, v2)
	local v1A = #v1
	assert(v1A == #v2, "Vectors must be of the same length.")
	local result = create(v1A)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add3(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

local xnil xnil = nil

-- Add two vectors
function vector.add4(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i, value in ipairs(v1) do
		result[i] = value + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add5(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i = 1, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

local ind = 1

-- Add two vectors
function vector.add6(v1, v2)
	local v1A = #v1
	if v2[v1A + 1] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add7(v1, v2) -- unstable
	local v1A = #v1
	if v2[v1A + ind] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

-- Add two vectors
function vector.add8(v1, v2) -- unstable
	local v1A = #v1
	if v2[v1A + ind] then error"" end
	local result = create(v1A, xnil)
	for i = ind, v1A do
		result[i] = v1[i] + v2[i]
	end
	return result
end

local random = math.random

local vectorMockmt = {
	__add = function(v1, v2)
		return setmetatable({ x = v1.x + v2.x, y = v1.y + v2.y, z = v1.z + v2.z }, getmetatable(v1))
	end
}

local vectorMock1 = setmetatable({ x = 1, y = 1, z = 1}, vectorMockmt)
local vectorMock2 = setmetatable({ x = 1, y = 1, z = 1}, vectorMockmt)

local v1 = create(am, vectorMock1) --Vector3.one)
local v2 = create(am, vectorMock2) --Vector3.one)

print("og")
local elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op1")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add1(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op2")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add2(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op3")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add3(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op4")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add4(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op5")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add5(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op6")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add6(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op7")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add7(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

print("op8")
elapsed = 0
for i = 1, am do
	local start = clock()
	local _ = vector.add8(v1,v2)
	local endT = clock() - start; elapsed = elapsed + endT
end
print(elapsed/ am)

Some miscellaneous stats:
compiled code using --text from luau-compile
text.txt (15.6 KB)
stats using --record-stats=total from luau-compile. In code, variable am is changed to 1
stats.json (515 Bytes)
coverage using --coverage from luau
coverage.txt (2.0 KB)
profile using --profile from luau consider turning it to svg from the tools folder using perfgraph.py
profile.txt (1.4 KB)
No microprofiler because I am just not to be able to record it or really can know where my scripts are. All ran with luau v0.606

Some more recommendation optimization consider implementing parallel luau and uses some parallel luau module am using promise based threader for some of my project.

Also I think I have found a things to do with this maybe with AI or large datasets of vectors that maybe use for procedural animation trying to remake the thing from uncharted but I am not gonna do it anytime soon, some more notes here don’t take my information only and make sure to take information from many sources or sources that you trust and then test and trials and errors with it.

4 Likes

I have almost completely redone my vector library. DM me @lordarathres2

considering this is the kind of thing you might run 10,000 times in a frame 5% is a huge deal

1 Like