Skip to content

Instantly share code, notes, and snippets.

@iamtgiri
Created February 21, 2026 20:10
Show Gist options
  • Select an option

  • Save iamtgiri/b043f1728c2db960c9bdde29aa068538 to your computer and use it in GitHub Desktop.

Select an option

Save iamtgiri/b043f1728c2db960c9bdde29aa068538 to your computer and use it in GitHub Desktop.
A small C++ experiment comparing malloc/free with pool, arena, and hybrid custom allocators, focusing on performance when allocating and deallocating large numbers of small objects.
#include <benchmark/benchmark.h>
#include <cstdlib>
#include <new>
#include <cstddef>
#include <vector>
#include <stdexcept>
#include <cassert>
#include <algorithm>
/* ============================
Test object
============================ */
struct SmallObject {
int a;
float b;
double c;
};
/* ============================
Allocator implementations
============================ */
struct FreeNode {
FreeNode* next;
};
class PoolAllocator {
char* pool_begin{};
char* pool_end{};
FreeNode* free_head{};
void* memory{};
std::size_t chunk_size{};
std::size_t chunk_count{};
public:
PoolAllocator(std::size_t chunkSize, std::size_t count)
: chunk_size(chunkSize), chunk_count(count)
{
if (chunkSize < sizeof(FreeNode))
throw std::invalid_argument("chunkSize too small");
#ifdef _MSC_VER
memory = _aligned_malloc(chunk_size * chunk_count, alignof(std::max_align_t));
#else
memory = ::aligned_alloc(alignof(std::max_align_t),
chunk_size * chunk_count);
#endif
if (!memory)
throw std::bad_alloc();
pool_begin = static_cast<char*>(memory);
pool_end = pool_begin + chunk_size * chunk_count;
free_head = reinterpret_cast<FreeNode*>(pool_begin);
FreeNode* curr = free_head;
for (std::size_t i = 0; i < chunk_count - 1; ++i) {
curr->next = reinterpret_cast<FreeNode*>(
reinterpret_cast<char*>(curr) + chunk_size);
curr = curr->next;
}
curr->next = nullptr;
}
~PoolAllocator() {
#ifdef _MSC_VER
_aligned_free(memory);
#else
std::free(memory);
#endif
}
void* Allocate() {
if (!free_head)
throw std::bad_alloc();
FreeNode* node = free_head;
free_head = free_head->next;
return node;
}
void Deallocate(void* ptr) {
if (!ptr) return;
assert(ptr >= pool_begin && ptr < pool_end);
auto* node = static_cast<FreeNode*>(ptr);
node->next = free_head;
free_head = node;
}
};
class ArenaAllocator {
char* memory{};
std::size_t capacity{};
std::size_t offset{};
public:
explicit ArenaAllocator(std::size_t size)
: capacity(size)
{
#ifdef _MSC_VER
memory = static_cast<char*>(
_aligned_malloc(size, alignof(std::max_align_t)));
#else
memory = static_cast<char*>(
::aligned_alloc(alignof(std::max_align_t), size));
#endif
if (!memory)
throw std::bad_alloc();
}
~ArenaAllocator() {
#ifdef _MSC_VER
_aligned_free(memory);
#else
std::free(memory);
#endif
}
void* Allocate(std::size_t size, std::size_t alignment) {
std::size_t aligned =
(offset + alignment - 1) & ~(alignment - 1);
if (aligned + size > capacity)
throw std::bad_alloc();
void* ptr = memory + aligned;
offset = aligned + size;
return ptr;
}
void Reset() {
offset = 0;
}
};
class HybridAllocator {
static constexpr std::size_t POOL_CHUNK_SIZE = 32;
PoolAllocator pool;
ArenaAllocator arena;
public:
explicit HybridAllocator(std::size_t maxObjects)
: pool(POOL_CHUNK_SIZE, maxObjects),
arena(maxObjects* POOL_CHUNK_SIZE)
{
}
void* Allocate(std::size_t size) {
if (size <= POOL_CHUNK_SIZE)
return pool.Allocate();
return arena.Allocate(size, alignof(std::max_align_t));
}
void Deallocate(void* ptr, std::size_t size) {
if (size <= POOL_CHUNK_SIZE)
pool.Deallocate(ptr);
}
void ResetArena() {
arena.Reset();
}
};
/* ============================
Benchmarks
============================ */
static void BM_MallocFree(benchmark::State& state)
{
const std::size_t N = state.range(0);
for (auto _ : state) {
std::vector<SmallObject*> objects;
objects.reserve(N);
for (std::size_t i = 0; i < N; ++i) {
auto* p = static_cast<SmallObject*>(
std::malloc(sizeof(SmallObject)));
new (p) SmallObject{};
benchmark::DoNotOptimize(p);
objects.push_back(p);
}
for (auto* p : objects) {
p->~SmallObject();
std::free(p);
}
benchmark::ClobberMemory();
}
}
static void BM_HybridAllocator(benchmark::State& state)
{
const std::size_t N = state.range(0);
for (auto _ : state) {
HybridAllocator allocator(N);
std::vector<SmallObject*> objects;
objects.reserve(N);
allocator.ResetArena();
for (std::size_t i = 0; i < N; ++i) {
void* mem = allocator.Allocate(sizeof(SmallObject));
auto* obj = new (mem) SmallObject{};
benchmark::DoNotOptimize(obj);
objects.push_back(obj);
}
for (auto* obj : objects) {
obj->~SmallObject();
allocator.Deallocate(obj, sizeof(SmallObject));
}
benchmark::ClobberMemory();
}
}
/* ============================
Registration
============================ */
BENCHMARK(BM_MallocFree)->Arg(1'000'000);
BENCHMARK(BM_HybridAllocator)->Arg(1'000'000);
BENCHMARK_MAIN();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment