-
Notifications
You must be signed in to change notification settings - Fork 19
Garbage collection and resource management
To fully use all features of PawnPlus, one must be aware of how its garbage collection works, how to control it, and how to properly use it to one's advantage.
Only strings, variants, iterators, and handles are affected by garbage collection; other objects use different methods of managing their lifetime.
If there was infinite memory available, there would be no need for garbage collection. However, to prevent the program from taking more and more memory when it actually only uses limited amount of it, some memory must be reused. Usually, the memory for an object is explicitly freed by the programmer when they are done working with it, but this is a common cause of memory leaks in these environments.
The garbage collector is a part of the program that keeps tracks of the memory requested by other code, and automatically frees the memory it deems unused.
Since Pawn doesn't offer any reliable way of tracking references to objects (since everything is a cell), PawnPlus takes a different approach to garbage collection. All objects managed by the garbage collector are collectible by default when created, but the point of collection is deterministic.
Collection happens every time the current supercontext ends. This happens when the top-level callback finishes its execution and the server continues with its code, with no other Pawn code running. All collectible objects are then destroyed (the same can be done using pp_collect
).
This way, temporary objects such as results of string operations are always collected without having to keep track of them manually. It is also safe to return them from functions or pass them to other functions, since unless they have to be stored in a more permanent location, they will not be collected earlier.
However, the garbage collector is aggressive – once the point of collection comes, the objects will be destroyed even if they are in use. The destruction can be prevented by moving the object to the global pool.
GC-objects in PawnPlus can live in one of the two pools – the local pool and the global pool. Only objects in the local pool are collected, and the others are never destroyed, even if they aren't in use.
To offer controlling the movement of objects between the two pools, PawnPlus employs reference counting. This is another technique used for automatically freeing unused memory, where every object tracks the number of times a reference to it is acquire or released, and destroys the object when the count drops to 0.
In PawnPlus, all newly created objects appear in the local pool, with the reference count initially set to 0. This makes them collectible, but they won't be collected immediately, so it is still possible to work with them. Once the programmer intends to store the object in a permanent location, they should call acquire
on the object, increasing the reference count. Once the object is removed from the location, release
should be called.
When the reference count is increased from 0 to 1, the object is moved to the global pool, and back to the local pool when it drops from 1 to 0. Equivalently, the garbage collector only deletes the objects which have their reference count set to 0.
new String:text;
main()
{
text = str_new_static("String"); // a new string is created in the local pool
assert(str_is_valid(text)); // the object still exists
} // `text` is collected after this function returns
public Callback()
{
assert(!str_is_valid(text)); // it does no longer exist
}
To store the string permanently in the global variable, str_acquire
must be called.
new String:text;
main()
{
text = str_new_static("String");
str_acquire(text); // the object enters the global pool
} // `text` is not collected
public Callback()
{
assert(str_is_valid(text)); // still exists
str_release(text);
assert(str_is_valid(text)); // `str_release` does not destroy the object
} // but it is deleted after this
Every function can call str_acquire
if it needs to store the string, and str_release
when it is no longer needed. There should never be a need to delete the string explicitly.
In asynchronous code or in code that involves dealing with errors, having to call acquire
and release
every time can get complicated. Guards can be useful in this case:
main()
{
new String:str = str_new_static("String");
pawn_guard(str); // `str_acquire` is called
wait_ms(1000); // without the guard, the string would be collected, since the supercontext must end so the wait doesn't block the application
assert(str_valid(str));
} // once the actual context ends, `str_release` is called and the string becomes collectible (and collected shortly afterwards)
A guard is bound to a context, so if the context is paused, it still exists, and if the context is destroyed (by returning or raising an error), all guards bound to it are destroyed as well.
Standard containers do not usually do anything special to GC-objects, but there are exceptions.
new List:l = list_new();
main()
{
list_add(l, str_acquire(str_new_static("String")));
}
public Callback()
{
list_delete_deep(l);
}
Before adding the object to the list, str_acquire
is called to ensure the string will not be deleted early. When list_delete_deep
is called, the whole list will be traversed and all its elements explicitly freed.
What it means to "free" an object depends on its kind. GC-objects are always released, i.e. their reference count is decreased. For standard containers (lists, linked lists and maps), the corresponding delete_deep
operation is called again. For other objects, their associated release
operation is called (whatever its implementation is).
This means list_delete_deep
and co. assume ownership of their elements, and therefore acquire
must be explicitly called on stored elements. In addition, stored lists, maps and linked lists are considered to be part of the whole structure when list_delete_deep
is called. This behaviour might not be always desirable, since some references to other containers only mean association and not composition, and some GC-objects may not need to be kept alive by the container.
For all objects, wrapping their tag inside Ref
will make all these funcions completely oblivious to their lifetime mechanics, and treat them as opaque references (hence the name), akin to simple cells (but the tag information is preserved).
If the programmer wants to only associate an object with a container, but still keep track of its lifetime, they can store it inside a handle. This basically wraps the reference in a variant-like container, but imposes garbage collection on it as well. When the handle is collected, the inner object is destroyed as well (via release
).
main()
{
new List:l = list_new();
new List:a = list_new();
new List:b = list_new();
new List:c = list_new();
list_add(l, a); // `a` is simply added
list_add(l, handle_acquire(handle_new(b))); // `b` is added as a handle
list_add(l, pawn_ref<List>(c)); // `c` is added as Ref<List>
list_delete_deep(l);
assert(!list_valid(a)); // `a` was assumed to be part of the list, and so it was destroyed
assert(list_valid(b)); // `b` was protected by the handle, which was not collected yet
assert(list_valid(c)); // `c` was not released at all
pp_collect();
assert(!list_valid(b)); // no references to the handle were registered, and so it was destroyed, taking `b` with it
assert(list_valid(c)); // `c` still exists
}