Created
February 21, 2026 20:10
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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