0

I am writing a cross platform shared library in C. The workflow of this library would be like,

lib *handle = lib_init();
result = lib_do_work(handle);
lib_destroy(handle);

Usually, users will init it once their application starts and closes it when the application ends. lib_do_work() usually gets called multiple times in a second. So to avoid memory allocation and deallocation for each calls, I am using a pooling mechanism. With this, I ask the pool to return an instance of the structure that I need. Pool will return an unused instance or create a new instance if nothing is free. This new instance will also be added to the pool so that it can be used next time.

Any API call to my library starts with a function call reset_pool() which makes all elements in the pool usable again. This pool is destroyed as part of lib_destroy() call. In my tests, I observed that sometime my pool is getting 100000+ instances of structure instances.

I am wondering is this a good practice in handling the memory? Any help would be great.

Navaneeth K N
  • 15,295
  • 38
  • 126
  • 184
  • Premature optimization is root of all evil. Have you measured that allocations/deallocations for each calls are slowing your application down? – nothrow Jul 10 '12 at 07:47
  • I am not sure if this is premature optimization. The only reason why I did this way is because, 1 - I don't need to do allocation each time. 2 - I don't have to manage memory everywhere. All I have to do is reset_pool at the beginning of each API method. – Navaneeth K N Jul 10 '12 at 08:17

2 Answers2

1

As has already been pointed out in the comments, only profiling will tell you if allocations and deallocations are a bottleneck in your application. Also, if your system only allocates and deallocates the same sized object all the time, then the default implementation will probably perform pretty well.

Usually, a pool provides allocation optimization by pre-allocating a block or elements. The block is carved out into individual elements to satisfy individual allocation requests. When the pool is depleted, a new block is allocated. This is an allocation optimization since it is cheaper to make fewer calls to the library allocator.

A pool allocator can also help to reduce fragmentation. If the application allocates and deallocates different sized objects with varying lifetimes, then the chance for fragmentation increases (and the coalescing code in the default allocator has to do more work). If a pool allocator is created for each different sized object, and each pool block was the same size, this would effectively eliminate fragmentation.

(As Felice points out, there is another kind of pool that pre-allocates a fixed amount of memory for the application to use, as a way to ensure the application does not use more memory than it is provisioned.)

On deallocation, individual elements can be placed onto a freelist. But. your reset_pool implementation can just walk through the blocks, free each one, and then allocate a new block.

The following is kind of simplistic. It only deals with one kind of element. POOL_SIZE would need to be tuned to be something reasonable for your application. Assume data structures like this:

typedef struct element {
    struct element *next;
    /* ... */
} element;

typedef struct pool_block {
    struct pool_block *next;
    struct element block[POOL_SIZE];
} pool_block;

typedef struct element_pool {
    struct pool_block *pools;
    struct element *freelist;
    int i;
} element_pool;

Then, the API would look something like:

void pool_init (element_pool *p) { /* ... */ }

element * pool_alloc (element_pool *p) {
    element *e = p->freelist;
    if (e) p->freelist = e->next;
    else do {
        if (p->i < POOL_SIZE) {
            e = &p->pools->block[p->i];
            p->i += 1;
        } else {
            pool_block *b = pool_block_create();
            b->next = p->pools;
            p->pools = b;
            p->i = 0;
        }
    } while (e == 0);
    return e;
}

element * pool_dealloc (element_pool *p, element *e) {
    e->next = p->freelist;
    p->freelist = e;
}

void pool_reset (element_pool *p) {
    pool_block *b;
    while ((b = p->pools)) {
        p->pools = b->next;
        pool_block_destroy(b);
    }
    pool_init(p);
}
jxh
  • 69,070
  • 8
  • 110
  • 193
  • Thanks. I have a similar implementation, but not with multiple pools. I have a single dynamically growing pool for each structure. Which means, structa will have one pool and structb will have other. I am wondering is there any advantage in doing like you have done? – Navaneeth K N Jul 10 '12 at 08:36
  • Also in my pool implementation, I am initially allocating only for one element. Then growing that 2 times for each call. – Navaneeth K N Jul 10 '12 at 08:38
  • My reset pool will just put the index back to 0. – Navaneeth K N Jul 10 '12 at 10:08
  • @Appu: I don't fully understand how your pool works, so I can't really compare your implementation to the one that I proposed. Perhaps if you could add a little code to your question about your allocate, deallocate, and reset functions, I could comment on how well they meet your objectives. Regards – jxh Jul 10 '12 at 16:20
  • Sorry for not making it clear. You can see my pool implementation here. https://github.com/navaneeth/libvarnam/blob/master/varnam-array.c#L117 – Navaneeth K N Jul 11 '12 at 05:21
  • @Appu: There are two issues I have with the pool. First is you have to perform individual allocations first before you populate your pool. So you only get amortized speed-up from your pool after your pool has reached the average peak size. Second, when growing your pool, you are likely copying a bunch of pointer during the realloc. – jxh Jul 11 '12 at 05:41
1

I don't know if it is overcomplicated for your current architecture, but usually a pool limits the number of pooled instances and queue requests when all pooled instances are busy.

Felice Pollano
  • 32,832
  • 9
  • 75
  • 115