Memory Pool vs Object Pool: When to Choose and How to Build One from Scratch
The article explains why high‑concurrency programs suffer from memory fragmentation and system‑call overhead, compares memory pools and object pools, outlines their distinct use‑cases, provides step‑by‑step C and C++ implementations, and highlights optimization tips and common pitfalls.
Why use a memory pool: the hidden cost of malloc
In high‑performance, high‑concurrency programs, frequent creation and destruction of small memory blocks causes repeated malloc / free calls. Each call triggers a system call, incurs user‑kernel context switches, and leads to memory fragmentation, reducing overall memory utilization and increasing latency.
Memory pool vs object pool
Both reuse pre‑allocated resources, but they operate at different abstraction layers. A memory pool works at the operating‑system memory level, optimizing raw memory allocation. An object pool works at the language‑level, reusing fully constructed objects to avoid constructor/destructor overhead and garbage‑collector pressure.
Memory pool core logic (C)
#define POOL_SIZE (1024 * 10) // pre‑allocate 10 KB
char memory_pool[POOL_SIZE];
int pool_offset = 0;
void *pool_alloc(int size) {
if (pool_offset + size > POOL_SIZE)
return NULL;
void *addr = memory_pool + pool_offset;
pool_offset += size;
return addr;
}
void pool_free() {
pool_offset = 0; // reset; memory stays in the pool
}This design eliminates system‑call overhead for each allocation, avoids external fragmentation, and keeps allocations cache‑friendly, making it suitable for custom data structures, bare‑metal code, and high‑frequency small‑block allocations.
Object pool core logic (C)
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char data[32];
int status;
} BizObj;
#define POOL_MAX_CNT 10
static BizObj obj_pool[POOL_MAX_CNT];
static int pool_used[POOL_MAX_CNT];
void object_pool_init() {
for (int i = 0; i < POOL_MAX_CNT; i++) {
obj_pool[i].status = 0;
obj_pool[i].data[0] = '\0';
pool_used[i] = 0;
}
}
BizObj* get_pool_obj() {
for (int i = 0; i < POOL_MAX_CNT; i++) {
if (pool_used[i] == 0) {
pool_used[i] = 1;
return &obj_pool[i];
}
}
return (BizObj*)malloc(sizeof(BizObj)); // fallback
}
void return_pool_obj(BizObj* obj) {
obj->status = 0;
obj->data[0] = '\0';
for (int i = 0; i < POOL_MAX_CNT; i++) {
if (obj == &obj_pool[i]) {
pool_used[i] = 0;
return;
}
}
free(obj);
}The object pool skips repeated construction/destruction, reduces GC pressure in managed languages, and is suited for high‑frequency temporary objects such as request parameters, network connections, or game entities.
Scenario selection: when to pick a memory pool
High‑frequency allocations of small blocks (e.g., 64 B–512 B) that cause fragmentation.
Latency‑sensitive code where system‑call jitter is unacceptable.
Low‑level development in C/C++/Rust where manual memory management is required.
Fixed‑size block reuse such as network packets or log buffers.
Scenario selection: when to pick an object pool
Managed‑language services (Java, Go, Python) where frequent new triggers GC pauses.
Objects with expensive construction (DB connections, sockets, large member initialization).
Short‑lived temporary objects (request DTOs, calculation entities, game effects).
Objects with fixed structure but variable size, where only the instance needs reuse.
Scenarios where pooling is not advisable
Very low request frequency – pre‑allocation wastes memory.
Resources held for a long time – low reuse rate makes the pool ineffective.
Highly variable large allocations – the pool cannot accommodate extreme size spikes.
Implementing a simple fixed‑size memory pool (C++)
Allocate a contiguous block once during initialization.
Divide the block into fixed‑size chunks and record free chunks in a linked list.
On allocation, pop a chunk from the free‑list head.
On deallocation, push the chunk back to the free‑list.
On destruction, release the whole block at once.
Fixed‑size memory pool code (C++)
#include <iostream>
#include <cstring>
class FixedMemoryPool {
private:
struct FreeBlock { FreeBlock* next; };
char* m_poolBuf;
FreeBlock* m_freeHead;
size_t m_blockSize;
size_t m_totalCount;
size_t m_usedCount;
public:
FixedMemoryPool(size_t blockSize, size_t totalCount)
: m_blockSize(blockSize >= sizeof(FreeBlock) ? blockSize : sizeof(FreeBlock)),
m_totalCount(totalCount), m_usedCount(0), m_poolBuf(nullptr), m_freeHead(nullptr) {
m_poolBuf = new char[m_blockSize * m_totalCount];
initFreeList();
}
~FixedMemoryPool() { delete[] m_poolBuf; }
void initFreeList() {
m_freeHead = reinterpret_cast<FreeBlock*>(m_poolBuf);
char* cur = m_poolBuf;
for (size_t i = 0; i < m_totalCount - 1; ++i) {
FreeBlock* curBlock = reinterpret_cast<FreeBlock*>(cur);
cur += m_blockSize;
curBlock->next = reinterpret_cast<FreeBlock*>(cur);
}
reinterpret_cast<FreeBlock*>(cur)->next = nullptr;
}
void* allocate() {
if (!m_freeHead) {
std::cout << "Memory pool exhausted" << std::endl;
return nullptr;
}
FreeBlock* blk = m_freeHead;
m_freeHead = m_freeHead->next;
++m_usedCount;
std::memset(blk, 0, m_blockSize);
return blk;
}
void deallocate(void* ptr) {
if (!ptr) return;
FreeBlock* blk = reinterpret_cast<FreeBlock*>(ptr);
blk->next = m_freeHead;
m_freeHead = blk;
--m_usedCount;
}
size_t getUsedCount() const { return m_usedCount; }
size_t getTotalCount() const { return m_totalCount; }
};
int main() {
FixedMemoryPool pool(128, 100);
void* p1 = pool.allocate();
void* p2 = pool.allocate();
std::cout << "Used blocks: " << pool.getUsedCount() << std::endl;
pool.deallocate(p1);
std::cout << "After free: " << pool.getUsedCount() << std::endl;
return 0;
}Implementing a generic object pool (C++)
Define an abstract base class with a reset() method.
Store idle objects in a queue and set a maximum capacity.
Use a factory function to create new objects when the pool is empty.
When returning an object, call reset() and push it back if the pool is not full; otherwise delete it.
Protect all operations with a mutex for thread safety.
Object pool template code (C++)
#include <iostream>
#include <queue>
#include <mutex>
#include <memory>
#include <functional>
class Reusable {
public:
virtual ~Reusable() = default;
virtual void reset() = 0;
};
template <typename T>
class ObjectPool {
static_assert(std::is_base_of_v<Reusable, T>, "Pooled object must inherit Reusable");
private:
std::queue<T*> idleQueue;
size_t maxSize;
std::mutex mtx;
using ObjectFactory = std::function<T*()>;
ObjectFactory factory;
public:
explicit ObjectPool(size_t poolMaxSize, ObjectFactory func)
: maxSize(poolMaxSize), factory(std::move(func)) {}
T* getObject() {
std::lock_guard<std::mutex> lock(mtx);
if (!idleQueue.empty()) {
T* obj = idleQueue.front();
idleQueue.pop();
return obj;
}
return factory();
}
void returnObject(T* obj) {
if (!obj) return;
std::lock_guard<std::mutex> lock(mtx);
obj->reset();
if (idleQueue.size() < maxSize) {
idleQueue.push(obj);
} else {
delete obj;
}
}
size_t getIdleCount() {
std::lock_guard<std::mutex> lock(mtx);
return idleQueue.size();
}
~ObjectPool() { destroy(); }
private:
void destroy() {
std::lock_guard<std::mutex> lock(mtx);
while (!idleQueue.empty()) {
delete idleQueue.front();
idleQueue.pop();
}
}
};
class BizObject : public Reusable {
std::string name;
int count = 0;
public:
void reset() override { name.clear(); count = 0; }
void setName(const std::string& n) { name = n; }
void setCount(int c) { count = c; }
void printInfo() const {
std::cout << "BizObject{name: " << name << ", count: " << count << "}" << std::endl;
}
};
int main() {
ObjectPool<BizObject> pool(20, [](){ return new BizObject(); });
BizObject* obj1 = pool.getObject();
obj1->setName("C++ Object Pool Demo");
obj1->setCount(666);
std::cout << "In‑use object: ";
obj1->printInfo();
pool.returnObject(obj1);
std::cout << "Idle count after return: " << pool.getIdleCount() << std::endl;
BizObject* obj2 = pool.getObject();
std::cout << "Reused object after reset: ";
obj2->printInfo();
pool.destroy();
return 0;
}Optimization tips and pitfalls
For memory pools, consider multi‑level pools for different block sizes, enforce that a block is returned only to the pool that allocated it, handle alignment to avoid CPU access penalties, and optionally add dynamic expansion when the pool is exhausted. For object pools, always enforce state reset before returning an object, use thread‑safe containers, optionally reclaim long‑idle objects, and avoid pooling very large objects. Misusing pools in low‑frequency or long‑lifetime scenarios can waste memory or degrade performance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
