Frame Allocation
This page explains coroutine frame allocation and how to optimize memory usage.
Code snippets assume using namespace boost::capy; is in effect.
|
What is a Coroutine Frame?
Every coroutine requires memory for its frame—the compiler-generated structure
holding local variables, parameters, and suspension state. By default, frames
are allocated with ::operator new.
For high-frequency coroutine creation, custom allocators can significantly reduce allocation overhead.
The frame_allocator Concept
A type satisfying frame_allocator provides:
template<class A>
concept frame_allocator =
std::copy_constructible<A> &&
requires(A& a, void* p, std::size_t n) {
{ a.allocate(n) } -> std::same_as<void*>;
{ a.deallocate(p, n) };
};
Frame allocators must be cheaply copyable handles to an underlying memory resource (e.g., a pointer to a pool).
Default Frame Allocator
The default_frame_allocator passes through to global new/delete:
struct default_frame_allocator
{
void* allocate(std::size_t n)
{
return ::operator new(n);
}
void deallocate(void* p, std::size_t)
{
::operator delete(p);
}
};
Recycling Frame Allocator
By default, async_run uses a recycling frame allocator that caches deallocated
frames for reuse. This eliminates most allocation overhead for typical coroutine
patterns where frames are created and destroyed in LIFO order.
The recycling allocator:
-
Maintains a thread-local free list
-
Reuses frames of matching size
-
Falls back to global new/delete for mismatched sizes
Custom Allocators with async_run
Pass a custom allocator as the second argument to async_run:
my_pool_allocator alloc{pool};
async_run(ex, alloc)(my_task());
The allocator is used for all coroutine frames in the launched call tree.
Implementing a Custom Allocator
class pool_frame_allocator
{
memory_pool* pool_;
public:
explicit pool_frame_allocator(memory_pool& pool)
: pool_(&pool)
{
}
void* allocate(std::size_t n)
{
return pool_->allocate(n);
}
void deallocate(void* p, std::size_t n)
{
pool_->deallocate(p, n);
}
};
static_assert(frame_allocator<pool_frame_allocator>);
Memory Layout
Coroutine frames have this layout:
First frame: [coroutine frame | tagged_ptr | allocator_wrapper]
Child frames: [coroutine frame | ptr]
The pointer at the end of each frame enables correct deallocation regardless of which allocator was active at allocation time. A tag bit distinguishes the first frame (with embedded wrapper) from child frames.
The frame_allocating_base Mixin
Derive your promise type from frame_allocating_base to enable custom frame
allocation:
struct my_promise : frame_allocating_base
{
// ... promise implementation ...
};
This mixin provides operator new and operator delete that use the
thread-local allocator when available.
Thread-Local State
The allocation mechanism uses thread-local storage:
// Set allocator for subsequent allocations
frame_allocating_base::set_frame_allocator(alloc);
// Clear allocator (revert to global new)
frame_allocating_base::clear_frame_allocator();
// Get current allocator (may be nullptr)
auto* alloc = frame_allocating_base::get_frame_allocator();
The async_run function manages this automatically—you rarely need to call
these directly.
When NOT to Use Custom Allocators
Use custom allocators when:
-
Profiling shows allocation is a bottleneck
-
You have a custom memory pool available
-
You need deterministic allocation behavior
Do NOT use custom allocators when:
-
The default recycling allocator is sufficient (it usually is)
-
Your coroutines are long-lived (allocation amortizes over time)
-
You’re unsure — measure first, optimize second
Summary
| Component | Purpose |
|---|---|
|
Concept for custom allocators |
|
Pass-through to global new/delete |
Recycling allocator |
Default: caches deallocated frames |
|
Promise mixin enabling custom allocation |
Next Steps
-
Containers — Type-erased storage
-
frame_allocator — Reference documentation