Why is vector3/4 (and other "vmath" types) a "userdata" type, and not just a table?

Well, as my test shows above there is little, if any, gain performance wise.

Because of metatables. I am strongly against using metatables in any lua game code.

1 Like

Removing the ability to directly add/sub/mul vectors, matrices and quats would certainly break things in a major way. There is nothing stopping you from extracting components and doing arithmetic manually and there are likely scenarios where you would improve performance doing that.

If there are use cases where it make sense for new functions in the api to facilitate specific use cases, that should by all means be considered.

2 Likes

Yes I made a mess of my benchmark. Sorry about that.

Well, if you use pure tables and deal with the math yourself the vmath.vector3 lib will be slower for sure. But in all honesty this is a much more fair comparison:

-- vector3 functionality represented using a table
local vectort3

-- metatable for vector3 table objects with built in math operations
local vectort3_mt = {
	__add = function(t1, t2)
		return vectort3(t1.x + t2.x, t1.y + t2.y, t1.z + t2.z)
	end
}

vectort3 = function(x, y, z)
	return setmetatable({ x = x or 0, y = y or 0, z = z or 0, }, vectort3_mt)
end

function init(self)
	local iterations = 10000000

	local time = socket.gettime()
	local v3 = vmath.vector3(100, 100, 100)
	local v3_add = vmath.vector3(0.1, 0.1, 0.1)
	for i=1,iterations do
		v3 = v3 + v3_add
	end
	print("time v3", socket.gettime() - time)

	time = socket.gettime()
	local t3 = vectort3(100, 100, 100)
	local t3_add = vectort3(0.1, 0.1, 0.1)
	for i=1,iterations do
		t3 = t3 + t3_add
	end
	print("time t3", socket.gettime() - time)
end

Results:

|DEBUG:SCRIPT: time v3|2.4817991256714|
|DEBUG:SCRIPT: time t3|2.2304310798645|
4 Likes

Is there a way to operate on Defolds vectors in native extensions?

Not yet.
We want to expose those though.

2 Likes

Well, the idea of a Lua specific math module was premature. Just like previously mentioned, LuaJIT is able to do big optimizations in cases like this.

When using a simple function like this, the performance becomes the same again.

local function add_t3(a, b)
	return {x = a.x+b.x, y = a.y+b.y, z = a.z+b.z }
end

DEBUG:SCRIPT: time v3 1.9348421096802
DEBUG:SCRIPT: time t3 1.7929799556732

Your example assume we always want a new vector (you just measure time for table creation, not time for “+” operation).
But in my experience we much often can reuse vectors. I am always as much as possible avoid new vector creation. In Defold, vector arguments passed by value, so reusing vectors is ok.
There no custom operators in lua, so no mutable addition, subtraction etc. Using functions (add, add_mutable, etc) instead of operators (and get rid of metatables, btw) may solve this.
We suffer again (a little) from design mistake made at very early stage of Defold development.

-- vector3 functionality represented using a table
local vectort3

-- metatable for vector3 table objects with built in math operations
local vectort3_mt = {
   __add = function(t1, t2)
      return vectort3(t1.x + t2.x, t1.y + t2.y, t1.z + t2.z)
   end
}

vectort3 = function(x, y, z)
   return setmetatable({ x = x or 0, y = y or 0, z = z or 0, }, vectort3_mt)
end

local function t3_add_mutable(a, b)
   a.x = a.x + b.x
   a.y = a.y + b.y
   a.z = a.z + b.z
   return a
end

function init(self)
   local iterations = 10000000

   local time = socket.gettime()
   local v3 = vmath.vector3(100, 100, 100)
   local v3_add = vmath.vector3(0.1, 0.1, 0.1)
   for _ = 1, iterations do
      -- v3 = v3 + v3_add
      v3.x = v3.x + v3_add.x
      v3.y = v3.y + v3_add.y
      v3.z = v3.z + v3_add.z
   end
   print("time v3 mutable", socket.gettime() - time)
   print(v3)

   time = socket.gettime()
   local t3 = vectort3(100, 100, 100)
   local t3_add = vectort3(0.1, 0.1, 0.1)
   for _ = 1, iterations do
      -- t3 = t3 + t3_add
      t3 = t3_add_mutable(t3, t3_add)
   end
   print("time t3 mutable", socket.gettime() - time)
   pprint(t3)
end
DEBUG:SCRIPT: time v3 mutable	5.8870670795441
DEBUG:SCRIPT: vmath.vector3(1088062, 1088062, 1088062)
DEBUG:SCRIPT: time t3 mutable	0.015144109725952
DEBUG:SCRIPT: 
{ --[[0xdf41850]]
  y = 1000099.999839,
  x = 1000099.999839,
  z = 1000099.999839
}

~300x times diff, no new userdata, no new tables.

There are, of course, large user benefits of avoiding mutating data. In the typical case the added cost is negible, since you operate on small amounts of data.

But in cases where you do need to transform large amounts of data, mutating may be the best way to get performance. You can solve many such cases today in pure Lua and hopefully native extensions operating on buffers will help solve pretty much anything else.

Can you describe what your use case is for requiring mutating vectors? If warranted, adding a function like vmath.increase(v, a, b, c) could perhaps be possible?

Functions such as:

local x, y, z = vmath.get(v3)
vmath.set(v3, x, y, z)

would be a good addition to the vmath library.

4 Likes