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.
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.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.