|
CookMem 1.4.0
A C++ memory context / allocator
|
CookMem is a general-purpose memory context and allocator written in C++11. It can be cleaned up quickly by freeing segments rather than freeing allocations individually.
Unlike projects such as nedmalloc, which is intended to replace malloc / free in multithreaded applications, CookMem focuses on single-thread performance. It is intended for use within a thread in an MPP process, avoiding synchronization costs for most allocations and deallocations. There are other use cases as well.
CookMem separates the logic for obtaining large memory segments, allowing users to easily create a version of the memory context for their specific needs.
The algorithms and some code used here are based on dlmalloc. I took the time to understand dlmalloc and rewrite its logic in C++. Some key calculations remain the same as in dlmalloc because there is no need to reinvent the wheel.
CookMem is header-only. Add the include directory to your include path and include cookmem.h.
See the tutorials for more.
cookmem::cookmem target or to build the tests, examples and documentation. CMake is not needed to use the headers directly. See Installation and Build Instructions.CookMem is faster than dlmalloc in most of the tests below: about 13 times faster for large blocks that come straight from the arena (perf_cookmem_10), about 5 times faster for interleaved realloc growth, where blocks grow in place (perf_cookmem_9), and about the same in the others. dlmalloc is faster for a sliding window of blocks of mixed sizes (perf_cookmem_11). Neither dlmalloc nor CookMem uses locks. CookMem was tested with both of its large chunk bins: the red-black tree (RB-tree), which is the default, and the AVL tree.
An interactive chart of these results is available with the documentation.
The tests were run on an AMD Ryzen 7 5800H with Ubuntu 24.04 (WSL2), built by GCC 13.3 with -O3. Times vary between runs by about 10%, so small differences are not significant. Each test was run 5 times. For each column, the best and the worst times were dropped and the remaining times were averaged. The numbers are in seconds.
| Test | What it covers | dlmalloc | CookMem (RB-tree) | CookMem (AVL-tree) |
|---|---|---|---|---|
| perf_cookmem_1 | Copy a 200-character string into a vector 1M times (small bins, plus large vector growth) | 0.179852 | 0.147207 | 0.138198 |
| perf_cookmem_2 | Same as perf_cookmem_1, then clear and fill again (reuse of freed chunks) | 0.328629 | 0.190954 | 0.177236 |
| perf_cookmem_3 | 10k strings of growing length 1 to 10000 (mostly tree bins) | 0.028434 | 0.026320 | 0.026967 |
| perf_cookmem_4 | Same as perf_cookmem_3, then clear and build again (reuse of freed tree chunks) | 0.045815 | 0.032542 | 0.031780 |
| perf_cookmem_5 | Small same-size churn (std::list<int>, small bins) | 0.108232 | 0.086955 | 0.095069 |
| perf_cookmem_6 | Mixed sizes (std::map<int, std::string>, values 0-999 chars, erase and re-insert) | 0.162361 | 0.174398 | 0.159243 |
| perf_cookmem_7 | 50k free chunks of the same 1KB size, then merging with free neighbors | 0.058387 | 0.028435 | 0.030270 |
| perf_cookmem_8 | Fragmentation and best-fit search (sizes 256 B-8 KB, refill the holes) | 0.105328 | 0.093511 | 0.097044 |
| perf_cookmem_9 | Interleaved realloc growth up to 32KB | 0.503400 | 0.088520 | 0.092882 |
| perf_cookmem_10 | Large blocks (128KB-4MB) going straight to the arena | 0.039517 | 0.003039 | 0.002812 |
| perf_cookmem_11 | Sliding window (FIFO) of 1024 live blocks, 16-2047 bytes | 0.094661 | 0.117984 | 0.122740 |
| perf_cookmem_12 | 10k vectors grown in interleaved order | 0.028886 | 0.026630 | 0.028787 |
In multithreaded applications, malloc / free or new / delete must synchronize on each call. This synchronization cost can be significant. nedmalloc exists to reduce the synchronization costs of naive implementations and thereby increase performance.
For performance-critical applications, eliminating much of this synchronization cost can be even more beneficial. With CookMem, obtaining large segments using calls such as mmap may require synchronization, but allocations within those segments do not. See Thread-local and Scoped Context STL Allocators.
There are two sub-cases.
Both cases can be addressed easily by using any of the MemContext classes. Destroying a memory context releases all of its memory at once. See Detailed Behavior for the details.
For long-running programs that take user inputs, it is possible that even when all input values are validated, edge cases in calculations can still cause the program to accidentally request a very large amount of memory and crash. CookMem provides several ways to restrict memory usage. One can set a memory footprint limit or add code to the segment request logic to restrict large memory requests. See Restricting Memory Usage.
CookMem has built-in diagnostic tools that can help gather memory statistics and detect difficult-to-catch out-of-bounds writes without significantly slowing down the program. It also detects attempts to free an allocation more than once. CookMem is not a Valgrind replacement, but in many cases it can help catch difficult bugs. See Memory Statistics and Memory Corruption Detection.