airstruck wrote:Interesting, thanks for testing it. I figured tables and functions would have different memory overhead, but wasn't sure which would come out on top. I do think it's fairly readable, but that's just a matter of opinion. I'm still curious how two vector implementations would compare instead of just dummy data, but I don't expect any improvement in the fp approach (it might get worse, there are more closed-over values than in the dummy test).
The problem is not how much memory they take up, it's how many memory allocations they perform. In total, 10,000 vectors are only using a few hundred Kb of RAM, which is pretty negligible. However, the code is performing a lot of memory allocations, all of which need to be cleaned up during garbage collection. There's also some function call overhead, because "dt*vel" calls a function, but "dt*velx" does not. Anyway, the speed is the real issue and here's some test code that shows why:
Code: Select all
local vec = require "hump_vector"
collectgarbage('collect')
collectgarbage('stop')
do
local start = os.clock()
local p = vec(0,0)
local v = vec(1,1)
local dt = 1/60
for _=1,1000 do
for _=1,1000 do
p = p + dt*v
end
collectgarbage('collect')
end
print("hump vectors took", os.clock() - start)
end
collectgarbage('collect')
do
local function extract(...) return ... end
local function Vector(x,y)
return function(operator) return (operator or extract)(x,y) end
end
local function times (x,y)
return function (c)
return Vector(c * x, c * y)
end
end
local function plus (x, y)
return function (vector)
local x2, y2 = vector(extract)
return Vector(x + x2, y + y2)
end
end
local start = os.clock()
local p = Vector(0,0)
local v = Vector(1,1)
local dt = 1/60
for _=1,1000 do
for _=1,1000 do
p = p(plus)(v(times)(dt))
end
collectgarbage('collect')
end
print("functions took", os.clock() - start)
end
collectgarbage('collect')
do
local start = os.clock()
local x,y = 0,0
local vx,vy = 1,1
local dt = 1/60
for _=1,1000 do
for _=1,1000 do
x,y = x+dt*vx, y+dt*vy
end
collectgarbage('collect')
end
print("components took", os.clock() - start)
end
Which produces this output (numbers are in seconds to perform 2 million vector operations and 1,000 garbage collections):
Code: Select all
hump vectors took 2.799155
functions took 3.679628
components took 0.079730000000001
You can see that both types of vector are considerably slower than just using components. However, you'd have to do some real profiling of a game to see how much of the game's actual performance that is, because it's only 1.4 microseconds per vector operation (amortized). I think it's worth the performance tradeoff if you're working with a few dozen or a few hundred objects (~0.01-0.1 ms per frame), but if you were working with code that did 10,000 vector operations per frame, I can see why you'd be unhappy if LÖVE forced you into using vectors.
airstruck wrote:I assume you tested this in 5.3. I think collectgarbage('collect') is actually restarting the GC in 5.1 and LuaJIT (but not 5.2 or 5.3); I got very different results (much more consistent with results from 5.2) when calling collectgarbage('stop') before each section of the test, and calling cg('collect') before cg('stop') instead of after. It looks like under LuaJIT the functional approach performs slightly better than tables (+15% or so) for this particular test.
As far as collectgarbage() goes, in
5.1,
5.2, and
5.3, "stop" is used to stop automatic garbage collection and "restart" is used to resume. "collect" runs a single pass of the garbage collection.