Fundamentals 20 min read

Boost C++ Performance with Fixed-Size Memory Pools: Theory and Code

This article explains why frequent dynamic memory allocation can cause slowdown and fragmentation, introduces fixed-size memory pools as an efficient solution, details their core principles, data structures, key allocation and deallocation functions, and demonstrates their performance advantage with C++ code examples and benchmarks.

Deepin Linux
Deepin Linux
Deepin Linux
Boost C++ Performance with Fixed-Size Memory Pools: Theory and Code

Part1: Memory Pool Overview

In everyday development, frequent memory allocation and deallocation can cause programs to slow down and memory usage to grow, often leading to allocation failures due to fragmentation.

Traditional allocation (new/delete in C++, malloc/free in C) works like digging and filling pits in a field, eventually leaving the field uneven (fragmented memory). This fragmentation reduces allocation efficiency and can cause failures even when total memory is sufficient.

For example, a game that creates and destroys many small objects each frame (bullets, particles) will suffer increasing fragmentation, causing stutters. A fixed-size memory pool acts like a diligent manager, organizing memory to improve performance.

Part2: Why Use a Memory Pool

2.1 Memory Fragmentation

Fragmentation reduces heap utilization. Internal fragmentation occurs when allocated blocks are larger than needed, leaving unused bytes inside the block. External fragmentation happens when free memory is split into small pieces that cannot satisfy a larger allocation request.

2.2 Allocation Efficiency

Repeatedly requesting memory from the OS is like asking parents for allowance each time; it incurs high interaction cost. A memory pool reduces this overhead by handling allocations in user space.

Part3: Fixed-Size Memory Pool Deep Dive

3.1 Core Principle

A memory pool pre‑allocates a large contiguous block from the OS and divides it into fixed‑size chunks. Allocation returns a free chunk; deallocation returns the chunk to the pool instead of the OS.

Example: a network program handling 1024‑byte packets can allocate from the pool instead of repeatedly calling the OS, greatly improving speed and reducing fragmentation.

3.2 Data Structures

Free‑list: a linked list of available chunks.

Pool struct: stores the base pointer, remaining byte count, and free‑list head.

template <class T>
class ObjectPool {
public:
    // ... other members ...
private:
    char* _memory = nullptr;      // large block from OS
    size_t _remainBytes = 0;      // remaining bytes in the block
    T* _freeList = nullptr;       // head of free list
};

3.3 Key Functions

Initialization : request a large block, split into fixed‑size chunks, and build the free‑list.

Allocation (New) : first try the free‑list; if empty, allocate from the remaining block or request a new block.

T* New() {
    T* obj = nullptr;
    if (_freeList) {
        void* next = *((void**)_freeList);
        obj = (T*)_freeList;
        _freeList = (T*)next;
        return obj;
    }
    if (_remainBytes < sizeof(T)) {
        _remainBytes = 128 * 1024; // request 128KB
        _memory = (char*)malloc(_remainBytes);
        if (!_memory) throw std::bad_alloc();
    }
    obj = (T*)_memory;
    _memory += sizeof(T);
    _remainBytes -= sizeof(T);
    new(obj) T; // placement new
    return obj;
}

Deallocation (Delete) : call the object's destructor, then insert the chunk back into the free‑list.

void Delete(T* obj) {
    obj->~T();
    *((T**)obj) = _freeList;
    _freeList = obj;
}

Part4: Code Demo and Performance Validation

4.1 Full Implementation

#include <iostream>
#include <vector>

template <class T>
class ObjectPool {
public:
    T* New() {
        T* obj = nullptr;
        if (_freeList) {
            void* next = *((void**)_freeList);
            obj = (T*)_freeList;
            _freeList = (T*)next;
        } else {
            if (_remainBytes < sizeof(T)) {
                _remainBytes = 128 * 1024;
                _memory = (char*)malloc(_remainBytes);
                if (!_memory) throw std::bad_alloc();
            }
            obj = (T*)_memory;
            _memory += sizeof(T);
            _remainBytes -= sizeof(T);
        }
        new(obj) T;
        return obj;
    }
    void Delete(T* obj) {
        obj->~T();
        *((T**)obj) = _freeList;
        _freeList = obj;
    }
private:
    char* _memory = nullptr;
    size_t _remainBytes = 0;
    T* _freeList = nullptr;
};

struct TestStruct { int data; TestStruct() : data(0) {} };

int main() {
    ObjectPool<TestStruct> pool;
    std::vector<TestStruct*> objects;
    for (size_t i = 0; i < 10; ++i) objects.push_back(pool.New());
    for (size_t i = 0; i < 10; ++i) pool.Delete(objects[i]);
    return 0;
}

4.2 Performance Test

The test runs 100,000 allocations and deallocations using both the system allocator (new/delete) and the ObjectPool. Measured times (example):

System allocation: ~1200 ms Fixed‑size pool allocation: ~350 ms

The results show that the memory pool dramatically reduces time overhead by avoiding frequent system calls and minimizing fragmentation, making it suitable for performance‑critical scenarios.

performance optimizationC#memory fragmentationcustom allocatormemory poolfixed-size allocation
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.