Object recycling tips

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
cval
Citizen
Posts: 58
Joined: Sun Apr 20, 2014 2:15 pm
Location: Ukraine

Object recycling tips

Post by cval »

Okay, some of you may have heard that it is a good practice to recycle/reuse unused object instead of creating new one. That approach is easy to implement in Lua since any table can be refilled with new instance values, and in the end you are going to create no excess overhead (except for minimal utility bytes like locals, temporary references etc).

However, in Love2d not everything is tables, since host application (the engine) manipulates userdata objects in its own way - physics, images, sound data etc, so it becomes a bit far from trivial to reuse objects that use multiple userdata objects, like, if your supercool monster has its own instance of shape-body-fixture, it's probably also has a texture, a quad or a sprite id and everything that it is composed of is going to be instantiated when you create one similar to this way

Code: Select all

function Monster:new()
 local self = setmetatable({},Monster) -- if you use OOPish approach in your lua code...
 self.body = ...
 self.shape = ...
 self.fixture = ...
 self.hp = 100
 self.sprite = ...
 return self
end
Your code ofcourse can be different, however general approach is going to be similar anyway. You also can use some kind of ECS helper that does everything for you and minimizes all the hassle, however i tend to invent my own wheels for the sake of education and complete control over things that are going on in my projects.

Now, when we talk about recycling instead of destroying, when you don't need an object instance anymore, you are going to place it into a "bin", which is probably some other table named unusedObjectTable or something. When object is "dead" in a way that it is no longer useful (monster's hp is zero, bullet's timer is zero or it collided with something...), it's reference is going to be deleted from main draw/update loop and is going to be insterted into said table for future use, and when time comes, instead of creating a new instance, you just pop a table reference out of unusedObjectTable, refill its values like if they represent your freshly created monster, and put it into your main processing loops and game goes on. Bear in mind that in the case of userdata objects like physics (and unlike textures which can be stored somewhere as a single instance and can be passed as reference wherever it needed), you should not create new instance of shape-body-fixture, since you are going to waste memory, and you have to check if it exists already before creation, and make inactive in some way when your object is not needed anymore so it will not affect other objects.

And my question is: how should i write object creation function, so that it checks if i create new instance, or use already existent (and unused) ? I don't want to write separate "Object:reinstantiate()" function and i want to use all OOP capabilities that Lua offers (metatables), while refraining from any ECS/helper libs as much as possible.

My object creation code usually looks similar to this one (projectile's example)

Code: Select all

Projectile = {}
Projectile.__index = Projectile
Projectile.timer_max = 5 -- these varaibles are global and unchanged for every instance and serve as public constants
function Projectile:new(id)
	local self = setmetatable({},Projectile)
	self.id = id
	self.timer = self.timer_max
	self.body = -- vanilla body creation code
	self.shape = -- vanilla shape creation code
	self.fixture = love.physics.newFixture(self.shape,self.body)
	return self
end
I've omitted functions like update and draw since they are irrelevant. This code will just create a new instance of a projectile each time it is called and returns it wherever it was called from for future manipulations.

I ask this because i am not sure how should i pass existent instance's data. I've tried it this way

Code: Select all

function Projectile:new(id,instance)
	local self
	if instance then
		self = instance -- if existent instance is passed then we use it
	else
		self = setmetatable({},Projectile) -- otherwise we create new table
	end
	
	-- this is important since we either use existent body instance or create new one
	-- for reasons listed above
	-- shape and fixture code is similar
  	-- then, every time object is considered "dead"
  	-- i'm just going to call self.body:setActive(false)
  	-- and self.body:setActive(true) on next instantiation 
  	-- if existent instance is used
  	
	self.body = self.body or love.physics.newBody(...) 
	self.shape = self.shape or love.physics.newShape(...)
	self.fixture = self.fixture or love.physics.newFixture(...)
	
	return self
end
HOWEVER. In lua, when you call

Code: Select all

someObjectInstance:someMethod(some_argument)
-- and now you can use semi-keyword "self" to manipulate instance's data
with a colon (:) sign, it is equivalent to

Code: Select all

someObjectInstance.someMethod(someObjectInstance, some_argument)
so above example approach is kinda reduntant.

If someone still reads this - you're my hero. Every approach that comes to mind either gives table setting loops, or two different objects look like if they have variable conflict (visually they "fight" for a position, for timer values etc.), so i guess everything that i do to implement "recycling" just messes up table metaindexes and everything falls apart.

Now, sorry, i am not going to attach .love file of where i'm trying to glue this whole thing to - code is too much of a mess to torture your eyes with, but if anyone is interesed, i am more than willing to discuss missing details.

Edit. Well, actually - i'm going to be fine if answer is going to be "just write Instance:reinstantiate() function and call it on every reuse", however i'm looking for less redundant practice.

Edit2. Dude here implemented somewhat similar to what i'm trying to, however his code is, as far as i can tell, is not self sufficient and dependant on some other module (middleclass), and for my constructs its is going to be a rather painful integration.
User avatar
Beelz
Party member
Posts: 234
Joined: Thu Sep 24, 2015 1:05 pm
Location: New York, USA
Contact:

Re: Object recycling tips

Post by Beelz »

I'm not going to say it's the best way to do things, but I would use the re-initialize function. I would also assign some variable for the type of object it is(different types most likely have different methods, etc). Then when you need to create a new object run it against the table of objects for that type. If it finds a match then re-init, otherwise create a new instance.

I'm no professional, but that's the route I would try. Good luck to you figuring this out! :)

Code: Select all

if self:hasBeer() then self:drink()
else self:getBeer() end
GitHub -- Website
cval
Citizen
Posts: 58
Joined: Sun Apr 20, 2014 2:15 pm
Location: Ukraine

Re: Object recycling tips

Post by cval »

Beelz wrote: I would also assign some variable for the type of object it is(different types most likely have different methods, etc). Then when you need to create a new object run it against the table of objects for that type. If it finds a match then re-init, otherwise create a new instance.
All of my objects have "ident" field which basically a string that denotes its type, so, yeah, each time an attempt to create new object happens, i first look through unused objects table and use the one i find, if i do.

Thanks anyway, eventually, separate "reset" function does look simplest way possible.
marco.lizza
Citizen
Posts: 52
Joined: Wed Dec 23, 2015 4:03 pm

Re: Object recycling tips

Post by marco.lizza »

cval wrote:Okay, some of you may have heard that it is a good practice to recycle/reuse unused object instead of creating new one.
I'm generally really concerned on resource usage and in not wasting CPU/memory, but is it really worth in this case?

I wrote this simple code snippet, which is a rather crude simplification of the approach.

Code: Select all

local AMOUNT = 100000

local function recreate(amount)
  local t = {}
  for _ = 1, AMOUNT do
    local o = {
        a = math.random,
        b = 'Hello',
        c = true,
        d = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
      }
    table.insert(t, o)
  end
  for _ = 1, AMOUNT do
    local o = {
        a = math.random,
        b = 'Hello',
        c = true,
        d = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
      }
    table.remove(t)
    table.insert(t, o)
  end
end

local function reuse(amount)
  local t = {}
  for _ = 1, AMOUNT do
    local o = {
        a = math.random,
        b = 'Hello',
        c = true,
        d = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
      }
    table.insert(t, o)
  end
  for i = 1, AMOUNT do
    local o = t[i]
    o.a = math.random
    o.b = 'Hello'
    o.c = true
    o.d = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
  end
end

local marker = os.clock()
recreate(AMOUNT)
local delta = os.clock() - marker
print(string.format('RECREATE = %.3f', delta))

marker = os.clock()
reuse(AMOUNT)
delta = os.clock() - marker
print(string.format('RECYCLE %.3f', delta))
To me the difference in time between the two approaches is not that significant. Moreover, in the code above there isn't any object-pool logic (that will likely put an additional overhead to the "reuse").

Is there any decent reference profile data of a real-world implementation of the object reuse technique?
cval
Citizen
Posts: 58
Joined: Sun Apr 20, 2014 2:15 pm
Location: Ukraine

Re: Object recycling tips

Post by cval »

marco.lizza wrote: I'm generally really concerned on resource usage and in not wasting CPU/memory, but is it really worth in this case?
Well, ofcourse, not always. Any wild and premature optimization usually ends up doing directly opposite to what it was initially designed for. So it depends on system architecture, application and many other factors. However, the first concern that has become a reason for me to dig reusage concept is that Lua's garbage collector starts to influent system's perfomance as time goes by. Yes, it does work, and yes its step and frequency is adjustable, however, i think it is going to be way better if i lift the burden off garbage collector's shoulders as much as i can, since it allows to minimize things like fps spikes during collection cycle etc. And one more thing - in interconnected system, where every entity can have multiple references to other entities, it is not just enough to rely on garbage collector, since incorrect metatable setting (weak key/value) and un-niled references can tell garbage collector that object is still used, whereas it actually is not and thus it still sits there, wastes memory space, and this "dead" mass continues to grow... you get the idea.
marco.lizza wrote: Is there any decent reference profile data of a real-world implementation of the object reuse technique?
I'm no experienced developer to hint a real working example, however once i've stumbled upon Lua's perfomance tips, and, well it says that, considering these garbage collector's traits, it is also wise to not fully rely on it's collection cycles, create new objects only when absolutely necessary, and, if possible, reuse old objects. And as an addition, i personally think that minimizing newly created userdata (namely - physics) allocations in love2d is also a good thing. Maybe when i will release any of my projects, or once i will be able to benchmark them using different object management approaches, then there will be more arguments to force careful object creation/reusage, who knows.

Edit. Few words about your example code - yes, your two approaches may show same perfomance, however in real application, there will be lots of other moments that are going to have an impact. The one being mentioned garbage collection cycle which is going to make FPS spikes/hiccups while it goes, and the more objects are created between cycles, the longer and much stronger those spikes will be.
marco.lizza
Citizen
Posts: 52
Joined: Wed Dec 23, 2015 4:03 pm

Re: Object recycling tips

Post by marco.lizza »

cval wrote: i think it is going to be way better if i lift the burden off garbage collector's shoulders as much as i can, since it allows to minimize things like fps spikes during collection cycle etc.
I have the exact opposite opinion. :) The garbage-collector exists with the sole aim to relieve us, the programmers, of additional concerns about memory shenanigans. Why should I spend time and code in relieving it from what it does best? ;) I would rather go and revert to C++!
cval wrote: And one more thing - in interconnected system, where every entity can have multiple references to other entities, it is not just enough to rely on garbage collector, since incorrect metatable setting (weak key/value) and un-niled references can tell garbage collector that object is still used, whereas it actually is not and thus it still sits there, wastes memory space, and this "dead" mass continues to grow... you get the idea.
Yep, I got the point.

To be precise, using "improper" (weak value) tables will lead to garbage-collect objects that are still in use (for example, "unnamed" objects created on-the-fly), not the other way around.

However, the chance of ending with leaks due to cycle-referencing "unreachable" chunks of memory is not so common to happen and circular references are not so common.

Even with something like this

Code: Select all

local pool = {}
pool[#pool + 1] = { name = 'John Doe' }
pool[#pool + 1] = { name = 'Jane Doe' }
pool[#pool + 1] = { name = 'Baby Doe' }
pool = {}
you are good to go. There's no need to explicitly set to "nil" (or remove) every single value.

Being the garbage collector implemented in non-incremental way, however, spikes may occur. That's true. To reduce the impact of this, the GC should be called frequently. At the moment, I don't know how LOVE2D approaches this issue. I will check the framework source code, later today.

However, going back to the main subject, in order to properly avoid leaks the "object pool manager" should deep-release all the properties of the objects it reuses.
cval wrote: And as an addition, i personally think that minimizing newly created userdata (namely - physics) allocations in love2d is also a good thing.
I agree with this. In fact, in the above, I was considering only primitive Lua values. Userdata should be treated with caution.
cval wrote: and the more objects are created between cycles, the longer and much stronger those spikes will be.
This is true, But, also, the more objects exist, the longer any pool-management-and-reuse algorithm will run.
alloyed
Citizen
Posts: 80
Joined: Thu May 28, 2015 8:45 pm
Contact:

Re: Object recycling tips

Post by alloyed »

Notes: Lua/Luajit both have incremental GCs
http://www.lua.org/manual/5.1/manual.html#2.10
http://wiki.luajit.org/New-Garbage-Coll ... mark-sweep

afaik Love doesn't touch the garbage collector, but it's not so hard to just add collectgarbage('step') to the end of your update function. I wouldn't do this until you start having problems with GC pauses though, because it costs CPU to keep marking objects each frame.
cval
Citizen
Posts: 58
Joined: Sun Apr 20, 2014 2:15 pm
Location: Ukraine

Re: Object recycling tips

Post by cval »

Glad i've got pretty insightful opinion on the subject.
marco.lizza wrote: I have the exact opposite opinion. :) The garbage-collector exists with the sole aim to relieve us, the programmers, of additional concerns about memory shenanigans. Why should I spend time and code in relieving it from what it does best? ;) I would rather go and revert to C++!
Well, i see you're quite experienced, and that is why i think you also understand what flaws automatic garbage collection has. And there are a lot of cases where implicit memory management actually can cause much more headache than explicit one, atleast if latter has new/destroy methods (since most of today's code probably doesn't use malloc/free explicitly, it is carefully called from behind fancy curtains). However, developer who actualy is concerned about this is not going to use GC-d language for perfomance-sensitive applications.

marco.lizza wrote: To be precise, using "improper" (weak value) tables will lead to garbage-collect objects that are still in use (for example, "unnamed" objects created on-the-fly), not the other way around.
Oh, is that so? Well, now i get it, and for some reason it was unclear for me even from Lua guides. I thought that making table weak will enable it to be collected by GC only when no value references to it. So, does this apply to tables that are created locally in some function?
However it's kinda still scary to use weak tables anywere without careful control.

marco.lizza wrote: In fact, in the above, I was considering only primitive Lua values. Userdata should be treated with caution.
In my first post it was implied that i wouldn't bother with reusage concept too much, if only all of my objects were consisting of number-string-table type of data. But no, in Love2d we also deal with host-allocated resources like textures, particle emitters, physics data, and even then not all of them actually have explicit destructors (however, not that i need destructor for an image), so for large scenes with tons of objects that are created and destroyed every few hundred frames, it can be wise to manage combined data objects. Pool them, reuse them etc.
marco.lizza wrote: But, also, the more objects exist, the longer any pool-management-and-reuse algorithm will run.
Shouldn't it depend on how and where pooling is implemented? I mean, that is true if we have created, say, 200 boxes few seconds ago, and then they are all destroyed and became unused ones, and then, depending on the order that those objects are pooled-back, finding any unused object of a different type that was created before those 200 boxes is going to take a while, but then, if that happens too often, developer should probably reconsider his optimization technique.
Last edited by cval on Tue May 10, 2016 10:40 pm, edited 2 times in total.
cval
Citizen
Posts: 58
Joined: Sun Apr 20, 2014 2:15 pm
Location: Ukraine

Re: Object recycling tips

Post by cval »

alloyed wrote: I wouldn't do this until you start having problems with GC pauses though, because it costs CPU to keep marking objects each frame.
And that is another reason why i don't want to rely completely on GC, since in my application, there can be a situation with object creation spike, and while instantiation itself probably will not drop fps too much, GC cycle can and probably will. And that is why i don't want to leave too much data for GC to collect.
marco.lizza
Citizen
Posts: 52
Joined: Wed Dec 23, 2015 4:03 pm

Re: Object recycling tips

Post by marco.lizza »

cval wrote: However, developer who actualy is concerned about this is not going to use GC-d language for perfomance-sensitive applications.
That is exactly my point. If I'm going to be real concerned about garbage-collector related issues, and I'm working on a performance-critical piece of software, and I can choose the proper tool (i.e. language) to tackle it... well, I would certainly go and use C++.
cval wrote: Oh, is that so? Well, now i get it, and for some reason it was unclear for me even from Lua guides. I thought that making table weak will enable it to be collected by GC only when no value references to it. So, does this apply to tables that are created locally in some function?
However it's kinda still scary to use weak tables anywere without careful control.
The subject is quite simple, but in my opinion is a bit shady and difficult to grasp, at first. It you are accustomed with the general concept of weak-references it should be simpler to understand.

However, back to Lua, weak key/value tables simply means that the referensce they hold aren't considered during the "mark" phase of the garbage-collection. This is useful when implementing object caches (not pools), for example

Code: Select all

local function create_object()
  return {
        -- fill object values here
      }
end

-- Let's create some objects in the pool.
local pool = {}
pool[#pool + 1] = create_object()
pool[#pool + 1] = create_object()
pool[#pool + 1] = create_object()

-- And then reference them in a weak-value table.
local cache = setmetatable({}, { __mode = 'v' })
for _, v in ipairs(pool) do
  cache[#cache + 1] = v
end

-- Now we free the pool.
for k, v in ipairs(pool) do
  pool[k] = nil
end

-- Since the cache has weak value references there's no need to release it,
-- too. The garbage-collector will drop the objects.
collectgarbage()

-- Let's check if that's true.
print('POOL')
for k, v in ipairs(pool) do
  print(k)
end
print('CACHE')
for k, v in ipairs(cache) do
  print(k)
end
cval wrote: But no, in Love2d we also deal with host-allocated resources like textures, particle emitters, physics data, and even then not all of them actually have explicit destructors (however, not that i need destructor for an image), so for large scenes with tons of objects that are created and destroyed every few hundred frames, it can be wise to manage combined data objects. Pool them, reuse them etc.
That is true. But one should also try and reuse the userdata types, more than the objects themselves.
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot], YaCy [Bot] and 2 guests