Object pooling and load

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By hearto
:warning: Old Version Published before Godot 3 was released.

Hello,
I read on some topics on old forums about how Godot doesn’t need an Object Pooler because instance/destroying loss of performance is negligible on Gdscript vs other languages like C#.
But what about dynamic load of resources on load like load(“path/node2d.tscn”)
My enemies or player could shoot a Barrage of bullets, that barrage is a node2d dynamically loaded for each enemy on ready with load, so on that case should I pool the loaded resources vs paths, or applies the same as instance/destroying?

If the answer is yes, my solution would be something like:

extends Node2D
var paths

func _ready():
	paths = Dictionary()
	
func load_scene(path):
	if(!paths.has(path)):
		paths[path] = load(path)
	return paths[path]

Have you seen the bullet shower demo?

duke_meister | 2016-04-11 02:33

Yes, I tried it at start of my project vs godot collision engine without masking (doesn’t exist at that time), and godot performance was leagues better. But the question here isn’t how to optimize bullets, is if repeated load calls which load the same resource are optimized on godot or I should make a pool for that.
Also I’m unsure if loads makes or not a disk read.

hearto | 2016-04-11 03:12

If you are loading the same scene over and over, you can preload it, or load it in _ready, after which you can just save it into a variable.

Bojidar Marinov | 2016-04-12 10:13

:bust_in_silhouette: Reply From: Gokudomatic2

I do have a similar concern, with an instance that needs to be dynamically configured (change size, add childs, change couple of stuffs). And I thought that pooling my instances and refilling the pool slowly over time was the best solution.
However, I don’t need to have a dictionnary because all my instances are exact copies. What I have is:

var pool=[]

func _ready():
   set_process(true)

func _process(delta):
    if pool.size()<MAX_POOL_INSTANCES:
        refill()
    # do other stuff

# refilling the pool with an amount that doesn't slow down noticeably the game
func refill():
    for i in range(POOL_REFILL_AMOUNT):
         pool.add(create_my_complex_instance())
 
func get_instance():
    if pool.size()==0: # in case pool is empty, create manually the instance
        return create_my_complex_instance()
    var instance=pool[0]
    pool.pop_front()
    return instance

You can even optimize it by calling refill() only every x frames, which I did.

Note that pop_back is likely to be faster than pop_front

Bojidar Marinov | 2016-04-11 11:00

Probably. However, to get the last item, I think you have to do something like:

my_list[my_list.size()-1]

And that’s 2 scripting instructions instead of one. The pop_front does probably more instructions too, but in C++. I don’t know which one is faster but I assume that script code is by fact slower than native code.

Gokudomatic2 | 2016-04-11 11:50

@Gokudomatnic2 ~ The problem is that pop_front is linear in length, while size + operator- + pop_back is constant… which means that for high amounts of objects, pop_back would be faster.

Bojidar Marinov | 2016-04-11 12:05

:bust_in_silhouette: Reply From: kubecz3k

EDIT: it seems I misunderstood the original question so this answer is not really in the subject…
The reason why in c# you need to use pooling is because c# was not designed for those kind of usage and have a GC which is used to freeing memory → but GC don’t do this often → it does this only when you have a lot of garbage objects. That’s why games written in unity that are not using pooling technique are losing their frames once once for couple seconds.
In GDscript there is no GC mechanism, just simple reference counting and this means objects get destroyed when you stop using them (or at least before next frame, which still means time needed to do this is negligible). Besides since there is no complicated GC on board that means more CPU time for your game.

So short answer is: you can instantiate and destroy your object in runtime whenever you need to.

I use load because lets so easily to change GRAPHICALLY the burst of bullets from the enemy ships and player, think of an editor for the user or non programmers.
That is the reason from my question, imagine enemy groups of 50 enemies, group A use barrage 1 and group 2 uses barrage 2.
This is 50 repeated loads for barrage 1 and 50 more for barrage 2, my concern is if godot make a cache like the dictionary example on my question for skip the loads from disk on repeated loads to same path.

hearto | 2016-04-11 20:19

Hey take a note on the comment from kubecz3k, this part is partially true:

instead of load since when you are using “load” the resource will be loaded each time from the hard disc which might take some time.

If you read my update as answer of this question, godot already does cache on LOAD, so if we assume the cache is never cleared, each loaded resource using load(“path”) calls will be loaded only ONCE from disk, and subsequent calls to same path will be loaded as a REFERENCE to the already loaded path.

hearto | 2016-04-12 09:57

I love to be wrong in cases such like this one :slight_smile: Thanks a lot for those informations!

kubecz3k | 2016-04-12 10:08

I deleted this part of my answer to not confuse other people that will read this in future

kubecz3k | 2016-04-12 10:12

:bust_in_silhouette: Reply From: hearto

Update:

Looking at godot source code I found the load routine and it’s seem they have a similar integrated cache as my question. Default calls to load uses a bool called p_no_cache for enable the caching of resources, on this case the tscn, this bool is default to false, so all the load(“path”) calls automatically gets pooled on godot, so the answer to my question is NO, I shouldn’t implement a pool for resource loading with load(“path”) calls, godot already take care for that.

The godot source which makes the pool on load, but requires the p_no_cache bool on false for resource pool:

RES ResourceLoader::load(const String &p_path, const String& p_type_hint, bool p_no_cache, Error *r_error) {
...

    	if (!p_no_cache && ResourceCache::has(local_path)) {
  
    		return RES( ResourceCache::get(local_path ) );
    	}
    ...
    }

and the declaration for that method:

static RES load(const String &p_path,const String& p_type_hint="",bool p_no_cache=false,Error *r_error=NULL);

Take note on the default parameter from bool p_no_cache=false

To quote the documentation: “When a resource is loaded from disk, it is always loaded once. That means, if there is a copy of that resource already loaded in memory, trying to load the resource again will just return the same copy again and again”.

Silas Gyger | 2017-01-02 16:46