CookMem divides the job of a memory context into three parts.
- The memory arena provides the API for allocating and deallocating large memory segments.
- The memory pool manages memory chunks within memory segments. This component is typically managed and hidden by the memory context.
- The logger monitors allocation and deallocation activity. Its primary use is to gather memory statistics.
Separating the memory arena from the memory pool makes it easy to build a custom memory context for your needs. See References for the available arenas and loggers. CookMem also provides ready-to-use memory contexts, including cookmem::SimpleMemContext and cookmem::CachedMemContext. This flexibility was a primary motivation for the project.
FixedArena
Often, we want allocations to be done within a large chunk of memory, such as a stack-based or pre-allocated memory buffer. cookmem::FixedArena is for this purpose.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
char buffer[64000];
void* ptr = memCtx.allocate (100);
memCtx.deallocate (ptr);
ptr = memCtx.callocate (1, 100);
ptr = memCtx.reallocate (ptr, 1000);
memCtx.deallocate (ptr);
return 0;
}
Definition allocator.h:39
MmapArena
mmap is one way to obtain large segments of memory on Unix. cookmem::MmapArena calls mmap and munmap to obtain and release segments. On Windows, it uses VirtualAlloc and VirtualFree instead.
It should be noted that this arena does not track the memory segments since cookmem::MemPool already does the tracking.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
void* ptr = memCtx.allocate (100);
ptr = memCtx.reallocate (ptr, 1000);
memCtx.deallocate (ptr);
return 0;
}
CachedArena
cookmem::CachedArena caches released memory segments in a red-black tree and reuses them for later requests. This is useful when it is expensive to request and release memory segments.
Note that the memory pool does not release a segment to the arena as soon as the segment becomes free. It keeps free segments for later allocations, and releases them periodically, after a number of deallocations.
The arena keeps cached segments until it is destroyed or its releaseAll () is called, which returns them to the underlying arena. A context's releaseAllToSystem () releases its segments and then calls the arena's releaseAll ().
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
{
CachedMemCtx memCtx (cachedArena, logger);
void* ptr = memCtx.allocate (100);
ptr = memCtx.reallocate (ptr, 1000);
memCtx.deallocate (ptr);
}
{
CachedMemCtx memCtx (cachedArena, logger);
void* ptr = memCtx.allocate (100);
ptr = memCtx.reallocate (ptr, 1000);
memCtx.deallocate (ptr);
}
return 0;
}
Definition mmaparena.h:115
Copying Data Between Memory Pools
cookmem can determine whether a memory pointer is in the address space of a memory pool. The check iterates through all memory segments owned by the pool.
This ability is quite useful to copy needed data out from the memory pool that is about to be destroyed.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
ParentCachedMemCtx parentCtx;
int* ptrs[10];
for (int i = 0; i < 5; ++i)
{
ptrs[i] = (int*)parentCtx.allocate (sizeof(int));
*ptrs[i] = i;
}
{
ChildCachedMemCtx childCtx (parentCtx);
for (int i = 5; i < 10; ++i)
{
ptrs[i] = (int*)childCtx.allocate (sizeof(int));
*ptrs[i] = i;
}
for (int i = 0; i < 10; ++i)
{
if (childCtx.contains (ptrs[i]))
{
std::cout << "Copying index " << i << std::endl;
int* ptr = (int*)parentCtx.allocate (sizeof(int));
*ptr = *ptrs[i];
ptrs[i] = ptr;
}
}
}
for (int i = 0; i < 10; ++i)
{
if (!parentCtx.contains (ptrs[i]))
{
std::cout << "Oops: index " << i << " is problematic." << std::endl;
return 1;
}
}
return 0;
}
Querying Pointer Information
cookmem can report the allocated size of a pointer. By default, cookmem does not keep track of the exact allocated size. However, an option can be enabled to save the exact size. This option does not use extra memory, but it does require additional bookkeeping time.
Additionally, cookmem can determine whether a pointer is valid.
See the following example.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
void* ptr1 = memCtx.allocate (10);
std::cout << "Allocated size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
memCtx.setStoringExactSize (true);
void* ptr2 = memCtx.allocate (10);
std::cout << "Allocated size for ptr2: " << memCtx.getSize (ptr2) << std::endl;
std::cout << "Allocated size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
std::cout << "ptr1 is used: " << memCtx.contains (ptr1, true) << std::endl;
std::cout << "ptr2 is used: " << memCtx.contains (ptr2, true) << std::endl;
memCtx.deallocate (ptr1);
memCtx.deallocate (ptr2);
std::cout << "ptr1 is used: " << memCtx.contains (ptr1, true) << std::endl;
std::cout << "ptr2 is used: " << memCtx.contains (ptr2, true) << std::endl;
std::cout << "Size for ptr1: " << memCtx.getSize (ptr1) << std::endl;
std::cout << "Size for ptr2 " << memCtx.getSize (ptr2) << std::endl;
return 0;
}
Restricting Memory Usage
A memory context can limit the total size of the memory segments it obtains, which is its footprint. An allocation that would need memory beyond the limit fails and returns nullptr, while the context remains usable. This protects long-running programs from accidentally requesting a very large amount of memory.
setFootprintLimit () sets the limit in bytes. 0, the default, means no limit.
getFootprint () returns the current footprint, and getMaxFootprint () returns the largest footprint so far.
The footprint includes the overhead of the segments, so slightly less memory than the limit is available for allocations.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
memCtx.setFootprintLimit (1024 * 1024);
void* huge = memCtx.allocate (10 * 1024 * 1024);
std::cout << "10 MB request: " << (huge == nullptr ? "failed" : "succeeded") << std::endl;
const int maxBlocks = 100;
void* blocks[maxBlocks];
int numBlocks = 0;
while (numBlocks < maxBlocks)
{
void* ptr = memCtx.allocate (100 * 1024);
if (ptr == nullptr)
{
break;
}
blocks[numBlocks++] = ptr;
}
std::cout << "100 KB blocks allocated: " << numBlocks << std::endl;
std::cout << "Footprint: " << memCtx.getFootprint () << " bytes" << std::endl;
memCtx.deallocate (blocks[--numBlocks]);
void* ptr = memCtx.allocate (100 * 1024);
std::cout << "After freeing one block, another 100 KB request: "
<< (ptr == nullptr ? "failed" : "succeeded") << std::endl;
memCtx.deallocate (ptr);
for (int i = 0; i < numBlocks; ++i)
{
memCtx.deallocate (blocks[i]);
}
std::cout << "Maximum footprint: " << memCtx.getMaxFootprint () << " bytes" << std::endl;
return 0;
}
Memory Statistics
cookmem::StatsLogger gathers statistics on allocations and memory segments: counts, current and peak sizes, and how the allocations were satisfied. Use it as the logger of a memory context and read the statistics with getLogger ().getStats (). The default logger, cookmem::NoActionMemLogger, does nothing, so the statistics cost nothing unless you ask for them.
#include <iostream>
#include <string>
#include <vector>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
MemCtx memCtx;
std::vector<void*> ptrs;
for (int i = 0; i < 1000; ++i)
{
ptrs.push_back (memCtx.allocate (16 + i));
}
for (std::size_t i = 0; i < ptrs.size (); i += 2)
{
memCtx.deallocate (ptrs[i]);
}
std::cout <<
"Peak bytes allocated: " << stats.
getPeakAllocated () << std::endl;
for (std::size_t i = 1; i < ptrs.size (); i += 2)
{
memCtx.deallocate (ptrs[i]);
}
return 0;
}
std::size_t getCurrentSegmentSize() const
Definition logger.h:256
std::size_t getPeakAllocated() const
Definition logger.h:250
std::size_t getCurrentAllocated() const
Definition logger.h:248
std::size_t getNumDeallocations() const
Definition logger.h:232
std::size_t getCurrentSegments() const
Definition logger.h:252
std::size_t getNumAllocations() const
Definition logger.h:225
void printStats() const
Definition logger.h:287
Memory Corruption Detection
In addition to various assertions, cookmem can add padding bytes (between 1 and 8) after the end of the user-allocated memory. When a pointer is explicitly deallocated, a check determines whether the padding bytes were overwritten.
This feature can reduce allocation and deallocation performance. In some cases, it can also increase memory usage. However, it is a valuable debugging tool.
Note that once an exception is thrown, the memory context is in a bad state, so it is not a good idea to reuse it.
#include <iostream>
#include <cookmem.h>
int
main (int argc, const char* argv[])
{
try
{
std::cout << "Padding enabled: " << memCtx.isPadding () << std::endl;
void* ptr = memCtx.allocate (10);
std::cout << "Allocated size for ptr: " << memCtx.getSize (ptr) << std::endl;
memset (ptr, 0xff, 11);
memCtx.deallocate (ptr);
}
{
}
return 0;
}
Definition exception.h:39
const char * getMessage() const
Definition exception.h:73
Caller-owned STL Allocator
Standard library containers accept custom allocators, allowing their storage to come from a CookMem memory context. cookmem::Allocator<T> adapts a context to the C++ allocator interface. For example, std::vector, std::list, and std::basic_string can allocate from the same context as the rest of the application.
With the default provider, the application creates and owns the context, then passes an allocator bound to it to each container. Containers using allocators bound to the same context can share its memory pool. This makes context ownership explicit and avoids global state, at the cost of passing allocator instances when constructing containers.
#include <iostream>
#include <list>
#include <cookmem.h>
int
main ()
{
MemoryContext context;
IntCallerAllocator allocator (context);
std::list<int, IntCallerAllocator> values (allocator);
values.push_back (10);
values.push_back (20);
values.push_back (30);
for (std::list<int, IntCallerAllocator>::const_iterator it = values.begin ();
it != values.end ();
++it)
{
std::cout << *it << std::endl;
}
return 0;
}
Thread-local and Scoped Context STL Allocators
cookmem::TLSContextProvider provides one context per thread for each context type and tag type. Use a unique tag to keep allocator groups separate. By default, this context lasts for the lifetime of the thread. To limit it to a lexical scope instead, declare a cookmem::TLSContextScope before the containers. Default-constructed allocators created inside that scope use its context, which is destroyed when the scope exits.
Use TLSContextProvider<Context, Tag>::getActiveContext() to query the context currently used for allocations. When no matching scope is active, it returns the persistent thread-local context. TLSContextProvider<Context, Tag>::getThreadContext() returns the persistent thread-local context whether or not a scope is active. The provider instance's get() still returns the context to which that instance was bound.
The function-scope example uses TLSContextScope to release the context as soon as its containers are destroyed. It demonstrates both options: passing true makes its pool share the thread-local context's arena; with CachedMemContext, freed segments can be reused by either context. Passing false gives the scope its own arena. The thread-local example omits the scope, so the provider uses its per-thread context and releases it when the thread exits. In all cases, containers must be destroyed before their context ends and remain on the thread where their allocator was created. Neither approach requires passing allocator instances to containers.
To release a thread's memory before the thread exits, destroy its containers and call releaseAll () or releaseAllToSystem () on the thread-local context, which getThreadContext () returns. releaseAll () keeps the segments in the cached arena, ready for later allocations on the thread. releaseAllToSystem () also empties the cache, so the memory goes back to the system. The worker thread in the example uses releaseAllToSystem (), which matters when a thread could end without running thread-local destructors. Either way, every pointer allocated from the context becomes invalid, including those of containers that still use it.
#include <iostream>
#include <list>
#include <thread>
#include <cookmem.h>
struct MyContextTag;
void
printValues (const std::list<int, MyAllocator>& values)
{
for (std::list<int, MyAllocator>::const_iterator it = values.begin ();
it != values.end ();
++it)
{
std::cout << *it << std::endl;
}
}
void
runWithThreadLocalContext ()
{
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWithSharedArena ()
{
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWithSeparateArena ()
{
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
void
runWorkerThread ()
{
{
std::list<int, MyAllocator> values;
values.push_back (10);
values.push_back (20);
values.push_back (30);
printValues (values);
}
MyContextProvider::getThreadContext ().releaseAllToSystem ();
}
int
main ()
{
runWithThreadLocalContext ();
runWithSharedArena ();
runWithSeparateArena ();
std::thread worker (runWorkerThread);
worker.join ();
return 0;
}