Memory pools

#include <libcork/core.h>

The functions in this section let you define memory pools, which allow you to reduce the overhead of allocating and freeing large numbers of small objects. Instead of generating a malloc call for each individual object, the memory pool allocates a large block of memory, and then subdivides this block of memory into objects of the desired size. The free objects in the memory pool are linked together in a singly-linked list, which means that allocation and deallocation is usually a (very small) constant-time operation.

Note

Memory pools are not thread safe; if you have multiple threads allocating objects of the same type, they’ll need separate memory threads.

Basic interface

struct cork_mempool

A memory pool. All of the objects created by the memory pool will be the same size; this size is provided when you initialize the memory pool.

struct cork_mempool *cork_mempool_new_size(size_t element_size)
struct cork_mempool *cork_mempool_new(TYPE type)

Allocate a new memory pool. The size of the objects allocated by the memory pool is given either as an explicit element_size, or by giving the type of the objects. The blocks allocated by the memory pool will be of a default size (currently 4Kb).

struct cork_mempool *cork_mempool_new_size_ex(size_t element_size, size_t block_size)
struct cork_mempool *cork_mempool_new_ex(TYPE type, size_t block_size)

Allocate a new memory pool. The size of the objects allocated by the memory pool is given either as an explicit element_size, or by giving the type of the objects. The blocks allocated by the memory pool will be block_size bytes large.

void cork_mempool_free(struct cork_mempool *mp)

Free a memory pool. You must have already freed all of the objects allocated by the pool; if you haven’t, then this function will cause the current process to abort.

void *cork_mempool_new_object(struct cork_mempool *mp)

Allocate a new object from the memory pool.

void cork_mempool_free_object(struct cork_mempool *mp, void *ptr)

Free an object that was allocated from the memory pool.

Initializing and finalizing objects

When you free an object that was allocated via a memory pool, the memory for that object isn’t actually freed immediately. (That’s kind of the reason that you’re using a memory pool in the first place.) This means that if your object contains any fields that are expensive to initialize and finalize, it can make sense to postpone the finalization of those fields until the memory for the object itself is actually freed.

As an example, let’s say you have the following type that you’re going to allocate via a memory pool:

struct my_data {
    struct cork_buffer  scratch_space;
    int  age;
};

Our first attempt at a constructor and destructor would then be:

static cork_mempool  *pool;
pool = cork_mempool_new(struct my_data);

struct my_data *
my_data_new(void)
{
    struct my_data  *self = cork_mempool_new_object(pool);
    if (self == NULL) {
        return NULL;
    }

    cork_buffer_init(&self->scratch_space);
    return self;
}

void
my_data_free(struct my_data *self)
{
    cork_buffer_done(&self->scratch_space);
    cork_mempool_free_object(pool, self);
}

What’s interesting about this example is that the scratch_space field, being a cork_buffer, allocates some space internally to hold whatever data we’re building up in the buffer. When we call cork_buffer_done() in our destructor, that memory is returned to the system. Later on, when we allocate a new my_data, the cork_mempool_new_object() call in our constructor might get this same physical instance back. We’ll then proceed to re-initialize the scratch_space buffer, which will then reallocate its internal buffer space as we use the type.

Since we’re using a memory pool to reuse the memory for the my_data instance, we might as well try to reuse the memory for the scratch_space field, as well. To do this, you provide initialization and finalization callbacks:

void cork_mempool_set_callbacks(struct cork_mempool *mp, void *user_data, cork_free_f free_user_data, cork_init_f init_object, cork_done_f done_object)

Provide callback functions that will be used to initialize and finalize each object created by the memory pool.

So, instead of putting the initialization logic into our constructor, we put it into the init_object function. Similarly, the finalization logic goes into done_object, and not our destructor:

static void
my_data_init(void *user_data, void *vself)
{
    struct my_data  *self = vself;
    cork_buffer_init(&self->scratch_space);
    return 0;
}

static void
my_data_done(void *user_data, void *vself)
{
    struct my_data  *self = vself;
    cork_buffer_done(&self->scratch_space);
}

static cork_mempool  *pool;
pool = cork_mempool_new(pool, struct my_data);
cork_mempool_set_callbacks(pool, NULL, NULL, my_data_init, my_data_done);

struct my_data *
my_data_new(void)
{
    return cork_mempool_new_object(pool);
}

void
my_data_free(struct my_data *self)
{
    cork_mempool_free_object(pool, self);
}

In this implementation, the scratch_space buffer is initialized when the memory for an instance is first allocated, and it’s not finalized until the memory for the instance is returned to the system. (Which basically means “when the memory pool itself is freed”.)

A caveat with this approach is that we’ve no longer guaranteed that the scratch_space buffer is empty when my_data_new returns — if we’re reusing an existing object, then the contents of the “previous” object’s buffer will still be there. We can either make sure that consumers of my_data don’t assume anything about the contents of scratch_space, or better yet, we can reset the fields in our constructor object:

struct my_data *
my_data_new(void)
{
    struct my_data  *self = cork_mempool_new_object(pool);
    cork_buffer_clear(&self->scratch_space);
    return self;
}

In this example, we can reset the buffer just by clearing it. If resetting is more involved, it can sometimes be better to leave the instance in a “messy” state, and have your clients not make assumptions. But if you do this, make sure to be clear about it in your documentation.

Table Of Contents

Previous topic

Reference-counted garbage collection

Next topic

Data structures

This Page