code/include/swoc/MemArena.h (314 lines of code) (raw):
// SPDX-License-Identifier: Apache-2.0
// Copyright Verizon Media 2020
/** @file
Memory arena for allocations
*/
#pragma once
#include <mutex>
#include <memory>
#include <utility>
#include <new>
#if __has_include(<memory_resource>)
#include <memory_resource>
#endif
#include "swoc/MemSpan.h"
#include "swoc/Scalar.h"
#include "swoc/IntrusiveDList.h"
#include "swoc/TextView.h"
namespace swoc { inline namespace SWOC_VERSION_NS {
/** A memory arena.
The intended use is for allocating many small chunks of memory - few, large allocations are best
handled through other mechanisms. The purpose is to amortize the cost of allocation of each
chunk across larger internal allocations ("reserving memory"). In addition the allocated memory
chunks are presumed to have similar lifetimes so all of the memory in the arena can be released
when the arena is destroyed.
*/
class MemArena
#if __has_include(<memory_resource>)
: public std::pmr::memory_resource
#endif
{
using self_type = MemArena; ///< Self reference type.
public:
static constexpr size_t DEFAULT_ALIGNMENT{1}; ///< Default memory alignment.
/// Convenient alias for use with @c unique_ptr.
/// @internal Can't be @c inline because the instantiation requires a complete type.
static void (*destroyer)(self_type *);
/// Correct type for a unique pointer to an instance.
/// Initialization is
/// @code
/// MemArena::unique_ptr arena(nullptr, MemArena::destroyer);
/// @endcode
/// To create the arena on demand
/// @code
/// arena.reset(MemArena::construct_self_contained());
/// @endcode
/// If the unique pointer is to be initialized with an arena, it should probably be a direct member isntead.
using unique_ptr = std::unique_ptr<self_type, void (*)(self_type *)>;
/// Simple internal arena block of memory. Maintains the underlying memory.
struct Block {
/// A block must have at least this much free space to not be "full".
static constexpr size_t MIN_FREE_SPACE = 16;
/// Get the start of the data in this block.
char *data();
/// Get the start of the data in this block.
const char *data() const;
/// @return The first byte past allocated storage.
char *allocated_data_end();
/// @return The first byte past allocated storage.
const char *allocated_data_end() const;
/// Amount of unallocated storage.
size_t remaining() const;
/** Compute the padding needed such adding it to @a ptr is a multiple of @a align.
*
* @param ptr Base pointer.
* @param align Alignment requirement (must be a power of 2).
* @return Value to add to @a ptr to achieve @a align.
*/
static size_t align_padding(void const *ptr, size_t align);
/** Check if there is @a n bytes of space at @a align.
*
* @param n Size required.
* @param align Alignment required.
* @return @c true if there is space, @c false if not.
*/
bool satisfies(size_t n, size_t align) const;
/// Span of unallocated storage.
MemSpan<void> remnant();
/** Allocate @a n bytes from this block.
*
* @param n Number of bytes to allocate.
* @param align Alignment requirement (default, no alignment).
* @return The span of memory allocated.
*/
MemSpan<void> alloc(size_t n, size_t = DEFAULT_ALIGNMENT);
/** Discard allocations.
*
* Reset the block state to empty.
*
* @return @a this.
*/
Block &discard();
/** Check if the byte at address @a ptr is in this block.
*
* @param ptr Address of byte to check.
* @return @c true if @a ptr is in this block, @c false otherwise.
*/
bool contains(const void *ptr) const;
/// @return @c true if the block has at least @c MIN_FREE_SPACE bytes free.
bool is_full() const;
protected:
friend MemArena; ///< Container.
/** Override @c operator @c delete.
*
* This is required because the allocated memory size is larger than the class size which
* requires calling @c free directly, skipping the destructor and avoiding complaints about size
* mismatches.
*
* @param ptr Memory to be de-allocated.
*/
static void operator delete(void *ptr) noexcept;
/** Override placement (non-allocated) @c delete.
*
* @param ptr Pointer returned from @c new
* @param place Value passed to @c new.
*
* This is called only when the class constructor throws an exception during placement new.
*
* @note I think the parameters are described correctly, the documentation I can find is a bit
* vague on the source of these values. It is required even if the constructor is marked @c
* noexcept. Both are kept in order to be documented.
*
* @internal This is required by ICC, but not GCC. Annoying, but it appears this is a valid
* interpretation of the spec. In practice this is never called because the constructor does
* not throw.
*/
static void operator delete([[maybe_unused]] void *ptr, void *place) noexcept;
/** Construct to have @a n bytes of available storage.
*
* Note this is descriptive - this presumes use via placement new and the size value describes
* memory already allocated immediately after this instance.
* @param n The amount of storage.
*/
explicit Block(size_t n) noexcept;
size_t size; ///< Actual block size.
size_t allocated{0}; ///< Current allocated (in use) bytes.
struct Linkage {
/// @cond INTERNAL_DETAIL
Block *_next{nullptr};
Block *_prev{nullptr};
static Block *&next_ptr(Block *);
static Block *&prev_ptr(Block *);
/// @endcond
} _link; ///< Intrusive list support.
};
/// Intrusive list of blocks.
using BlockList = IntrusiveDList<Block::Linkage>;
/** Construct with reservation hint.
*
* No memory is initially reserved, but when memory is needed this will be done so at least
* @a n bytes of available memory is reserved.
*
* To pre-reserve call @c alloc(0), e.g.
* @code
* MemArena arena(512); // Make sure at least 512 bytes available in first block.
* arena.alloc(0); // Force allocation of first block.
* @endcode
*
* @param n Minimum number of available bytes in the first internally reserved block.
*/
explicit MemArena(size_t n = DEFAULT_BLOCK_SIZE);
/** Construct using static block.
*
* @param static_block A block of memory that is non-deletable.
*
* @a static_block is used as the first block for allocation and is never deleted. This makes
* it possible to have an instance that allocates from stack memory and only allocates from the
* heap if the static block becomes full.
*
* @note There is no default block size because the static block is the initial block. Subsequent
* allocations are based on that size.
*/
explicit MemArena(MemSpan<void> static_block);
/// no copying
MemArena(self_type const &that) = delete;
/// Allow moving the arena.
MemArena(self_type &&that) noexcept;
/// Destructor.
~MemArena();
/// No copy assignment.
self_type &operator=(self_type const &that) = delete;
/// Move assignment.
self_type &operator=(self_type &&that) noexcept;
/** Make a self-contained instance.
*
* @param n The initial memory size hint.
* @return A new, self contained instance.
*
* Create an instance of @c MemArena that is stored in its own memory pool. The size hint @a n
* is adjusted to account for the space consumed by the @c MemArena instance. This instance
* will therefore always have done its initial internal memory allocation to provide space
* for itself.
*
* This is most useful for smaller objects that need to strongly minimize their size when not
* allocating memory. In that context, this enables being able to have a memory pool as needed
* at the cost of a only single pointer in the instance.
*
* @note This requires careful attention to detail for freezing and thawing, as the @c MemArena
* itself will be in the frozen memory and must be moved to the fresh allocation.
*
* @note @c delete must not be called on the returned pointer. Instead the @c MemArena destructor
* must be explicitly called, which will clean up all of the allocated memory. See the
* documentation for further details.
*/
static self_type *construct_self_contained(size_t n = DEFAULT_BLOCK_SIZE);
/** Allocate @a n bytes of storage.
Returns a span of memory within the arena. alloc() is self expanding but DOES NOT self
coalesce. This means that no matter the arena size, the caller will always be able to alloc()
@a n bytes.
@param n number of bytes to allocate.
@param align Required alignment, defaults to 1 (no alignment). Must be a power of 2.
@return a MemSpan of the allocated memory.
*/
MemSpan<void> alloc(size_t n, size_t align = DEFAULT_ALIGNMENT);
/** ALlocate a span of memory sufficient for @a n instance of @a T.
*
* @tparam T Element type.
* @param n Number of instances.
* @return A span large enough to hold @a n instances of @a T.
*
* The instances are @b not initialized / constructed. This only allocates the memory.
* This is handy for types that don't need initialization, such as built in types like @c int.
* @code
* auto vec = arena.alloc_span<int>(20); // allocate space for 20 ints
* @endcode
*
* The memory is aligned according to @c alignof(T).
*/
template <typename T> MemSpan<T> alloc_span(size_t n);
/** Allocate and initialize a block of memory as an instance of @a T
The template type specifies the type to create and any arguments are forwarded to the
constructor. Example:
@code
struct Thing { ... };
auto thing = arena.make<Thing>(...constructor args...);
@endcode
Do @b not call @c delete an object created this way - that will attempt to free the memory and
break. A destructor may be invoked explicitly but the point of this class is that no object in
it needs to be deleted, the memory will all be reclaimed when the Arena is destroyed. In
general it is a bad idea to make objects in the Arena that own memory that is not also in the
Arena.
*/
template <typename T, typename... Args> T *make(Args &&...args);
/** Copy the contents of a string view into the arena.
*
* @param s Original string.
* @return A view of the copy of @a s.
*/
MemSpan<char> localize(MemSpan<char const> s);
/** Copy the contents of a string view into the arena.
*
* @param s Original string.
* @return A view of the copy of @a s.
*/
MemSpan<char> localize(char const * s);
/** Copy the contents of a string view into the arena as a C string.
*
* @param s Original string.
* @return A view of the copy of @a s.
*
* A terminating nul character is added to the copy which is not included in the returned view.
* This enables using the string view as a C string.
*/
MemSpan<char> localize_c(MemSpan<char const> s);
/** Copy the contents of a string view into the arena as a C string.
*
* @param s Original string.
* @return A view of the copy of @a s.
*
* A terminating nul character is added to the copy which is not included in the returned view.
* This enables using the string view as a C string.
*/
MemSpan<char> localize_c(char const * s);
/** Freeze reserved memory.
All internal memory blocks are frozen and will not be involved in future allocations.
Subsequent allocation will reserve new internal blocks. By default the first reserved block
will be large enough to contain all frozen memory. If this is not correct a different target
can be specified as @a n.
@param n Target number of available bytes in the next reserved internal block.
@return @c *this
*/
MemArena &freeze(size_t n = 0);
/** Unfreeze arena.
*
* Frozen memory is released.
*
* @return @c *this
*/
self_type &thaw();
/** Release all memory.
Empties the entire arena and deallocates all underlying memory. The hint for the next reserved
block size will be @a n if @a n is not zero, otherwise it will be the sum of all allocations
when this method was called.
@param hint Size hint for the next internal allocation.
@return @a this
@see discard
*/
MemArena &clear(size_t hint = 0);
/** Best effort allocation discard.
*
* The allocation is discard (become unallocated memory) if and only if it is at the end of
* a recent allocation block. If nothing has been allocated, this always works. Otherwise if later
* allocation exists, this method silently fails. This can work with multiple allocations in a
* stack - if all later allocations are discarded when this method is invoked it will succeed.
*
* @note This is purely a performance enhancement to enable computing with internally allocated
* but unused memory. Compare with @c remnant
*
* @note No destruction or other clean up is done, the span is simply marked as unallocated.
*
* @param hint Size hint for the next internal allocation.
* @return @a this.
*
* @see remnant
*/
MemArena &discard(MemSpan<void const> span);
/** Discard all allocations.
*
* All active internal memory blocks are reset to be empty, discarding any allocations. These blocks
* will be re-used by subsequent allocations.
*
* @param hint Size hint for the next internal allocation.
* @return @a this.
*
* @see clear
*/
MemArena &discard(size_t hint = 0);
/// @return The amount of memory allocated.
size_t size() const;
/// @return The amount of free space.
size_t remaining() const;
/** Get aligned and sized remnant.
*
* @tparam T Element type.
* @param n Number of instances of @a T
* @return A span that is in the remnant, correctly aligned with minimal padding.
*
* This is guaranteed to be the same bytes as if @c alloc<T> was called. The returned span will
* always be the specified size, the remnant will be expanded as needed.
*/
template <typename T> MemSpan<T> remnant_span(size_t n);
/// @return Contiguous free space in the current internal block.
MemSpan<void> remnant();
/** Get an aligned remnant.
*
* @param n Remnant size.
* @param align Memory alignment (default 1, must be power of 2).
* @return Space in the remnant with minimal alignment padding.
*
* @note This will always return a span of @a n bytes, the remnant will be expanded as needed.
*/
MemSpan<void> remnant(size_t n, size_t align = DEFAULT_ALIGNMENT);
/** Require @a n bytes of contiguous memory to be available for allocation.
*
* @param n Number of bytes.
* @param align Align requirement (default is 1, no alignment).
* @return @a this
*
* This forces the @c remnant to be at least @a n bytes of contiguous memory. A subsequent
* @c alloc will use this space if the allocation size is at most the remnant size.
*/
self_type &require(size_t n, size_t align = DEFAULT_ALIGNMENT);
/// @returns the total number of bytes allocated within the arena.
size_t allocated_size() const;
/** Check if a the byte at @a ptr is in memory owned by this arena.
*
* @param ptr Address of byte to check.
* @return @c true if the byte at @a ptr is in the arena, @c false if not.
*/
bool contains(const void *ptr) const;
/** Total memory footprint, including wasted space.
* @return Total memory footprint.
*/
size_t reserved_size() const;
using const_iterator = BlockList::const_iterator; ///< Constant element iteration.
using iterator = const_iterator; ///< Element iteration.
/// First active block.
const_iterator begin() const;
/// After Last active block.
const_iterator end() const;
/// First frozen block.
const_iterator frozen_begin() const;
/// After last frozen block.
const_iterator frozen_end() const;
protected:
/** Internally allocates a new block of memory of size @a n bytes.
*
* @param n Size of block to allocate.
* @return
*/
Block *make_block(size_t n);
/// Clean up the frozen list.
void destroy_frozen();
/// Clean up the active list
void destroy_active();
using Page = Scalar<4096>; ///< Size for rounding block sizes.
using QuarterPage = Scalar<Page::SCALE / 4>; ///< Quarter page - unit for sub page sizes.
using Paragraph = Scalar<16>; ///< Minimum unit of memory allocation.
static constexpr size_t ALLOC_HEADER_SIZE = 16; ///< Guess of overhead of @c malloc
/// Initial block size to allocate if not specified via API.
static constexpr size_t DEFAULT_BLOCK_SIZE = Page::SCALE - Paragraph{round_up(ALLOC_HEADER_SIZE + sizeof(Block))};
size_t _active_allocated = 0; ///< Total allocations in the active generation.
size_t _active_reserved = 0; ///< Total reserved memory (allocated from OS).
/// Total allocations in the previous generation. This is only non-zero while the arena is frozen.
size_t _frozen_allocated = 0;
size_t _frozen_reserved = 0; ///< Total frozen reserved memory.
/// Minimum free space needed in the next allocated block.
/// This is not zero iff @c reserve was called.
size_t _reserve_hint = 0;
BlockList _frozen; ///< Previous generation, frozen memory.
BlockList _active; ///< Current generation. Allocate here.
/// Static block, if any.
Block *_static_block = nullptr;
// Note on _active block list - blocks that become full are moved to the end of the list.
// This means that when searching for a block with space, the first full block encountered
// marks the last block to check. This keeps the set of blocks to check short.
private:
#if __has_include(<memory_resource>)
// PMR support methods.
/// PMR allocation.
void *do_allocate(std::size_t bytes, std::size_t align) override;
/// PMR de-allocation.
/// Does nothing.
void do_deallocate(void *, size_t, size_t) override;
/// PMR comparison of memory resources.
/// @return @c true only if @a that is the same instance as @a this.
bool do_is_equal(std::pmr::memory_resource const &that) const noexcept override;
#endif
};
/** Arena of a specific type on top of a @c MemArena.
*
* @tparam T Type in the arena.
*
* A pool of unused / free instances of @a T is kept for reuse. If none are available then a new
* instance is allocated from the arena.
*/
template <typename T> class FixedArena {
using self_type = FixedArena; ///< Self reference type.
protected:
/// Rebinding type for instances on the free list.
struct Item {
Item *_next; ///< Next item in the free list.
};
Item _list{nullptr}; ///< List of dead instances.
MemArena &_arena; ///< Memory source.
public:
/** Construct a pool.
*
* @param arena The arena for memory.
*/
explicit FixedArena(MemArena &arena);
/** Create a new instance.
*
* @tparam Args Constructor argument types.
* @param args Constructor arguments.
* @return A new instance of @a T.
*/
template <typename... Args> T *make(Args... args);
/** Destroy an instance.
*
* @param t The instance to destroy.
*
* The instance is destructed and then put on the free list for re-use.
*/
void destroy(T *t);
/// Drop all items in the free list.
void clear();
/// Access the wrapped arena directly.
MemArena &arena();
};
// --- Implementation ---
/// @cond INTERNAL_DETAIL
inline auto
MemArena::Block::Linkage::next_ptr(Block *b) -> Block *& {
return b->_link._next;
}
inline auto
MemArena::Block::Linkage::prev_ptr(Block *b) -> Block *& {
return b->_link._prev;
}
inline MemArena::Block::Block(size_t n) noexcept : size(n) {}
inline char *
MemArena::Block::data() {
return reinterpret_cast<char *>(this + 1);
}
inline const char *
MemArena::Block::data() const {
return reinterpret_cast<const char *>(this + 1);
}
inline char *
MemArena::Block::allocated_data_end() {
return this->data() + allocated;
}
inline const char *
MemArena::Block::allocated_data_end() const {
return this->data() + allocated;
}
inline bool
MemArena::Block::contains(const void *ptr) const {
const char *base = this->data();
return base <= ptr && ptr < base + size;
}
inline size_t
MemArena::Block::remaining() const {
return size - allocated;
}
inline bool
MemArena::Block::is_full() const {
return this->remaining() < MIN_FREE_SPACE;
}
inline MemSpan<void>
MemArena::Block::alloc(size_t n, size_t align) {
auto base = this->data() + allocated;
auto pad = align_padding(base, align);
if ((n + pad) > this->remaining()) {
throw(std::invalid_argument{"MemArena::Block::alloc size is more than remaining."});
}
MemSpan<void> zret = this->remnant().prefix(n + pad);
zret.remove_prefix(pad);
allocated += n + pad;
return zret;
}
inline MemSpan<void>
MemArena::Block::remnant() {
return {this->data() + allocated, this->remaining()};
}
inline MemArena::Block &
MemArena::Block::discard() {
allocated = 0;
return *this;
}
inline void
MemArena::Block::operator delete(void *ptr) noexcept {
::free(ptr);
}
inline void
MemArena::Block::operator delete([[maybe_unused]] void *ptr, void *place) noexcept {
::free(place);
}
inline size_t
MemArena::Block::align_padding(void const *ptr, size_t align) {
if (auto delta = uintptr_t(ptr) & (align - 1); delta > 0) {
return align - delta;
}
return 0;
}
inline MemArena::MemArena(size_t n) : _reserve_hint(n) {}
template <typename T>
MemSpan<T>
MemArena::alloc_span(size_t n) {
return this->alloc(sizeof(T) * n, alignof(T)).rebind<T>();
}
template <typename T, typename... Args>
T *
MemArena::make(Args &&...args) {
return new (this->alloc(sizeof(T), alignof(T)).data()) T(std::forward<Args>(args)...);
}
inline MemSpan<char> MemArena::localize(MemSpan<char const> s) {
auto span = this->alloc(s.size()).rebind<char>();
memcpy(span.data(), s.data(), span.size());
return { span.data(), span.size() };
}
inline MemSpan<char> MemArena::localize(char const * s) {
return this->localize(MemSpan<char const>(s, strlen(s)));
}
inline MemSpan<char> MemArena::localize_c(MemSpan<char const> s) {
auto span = this->alloc(s.size() + 1).rebind<char>();
memcpy(span.data(), s.data(), span.size());
span[s.size()] = '\0';
return { span.data(), span.size() };
}
inline MemSpan<char> MemArena::localize_c(char const * s) {
return this->localize_c(MemSpan<char const>(s, strlen(s)));
}
template <typename T>
MemSpan<T>
MemArena::remnant_span(size_t n) {
auto span = this->require(sizeof(T) * n, alignof(T)).remnant();
return span.remove_prefix(Block::align_padding(span.data(), alignof(T))).rebind<T>();
}
template <>
inline MemSpan<void>
MemArena::remnant_span<void>(size_t n) {
return this->require(n).remnant().prefix(n);
}
inline MemSpan<void>
MemArena::remnant(size_t n, size_t align) {
return this->require(n, align).remnant().prefix(n);
}
inline size_t
MemArena::size() const {
return _active_allocated;
}
inline size_t
MemArena::allocated_size() const {
return _frozen_allocated + _active_allocated;
}
inline size_t
MemArena::remaining() const {
return _active.empty() ? 0 : _active.head()->remaining();
}
inline MemSpan<void>
MemArena::remnant() {
return _active.empty() ? MemSpan<void>() : _active.head()->remnant();
}
inline size_t
MemArena::reserved_size() const {
return _active_reserved + _frozen_reserved;
}
inline auto
MemArena::begin() const -> const_iterator {
return _active.begin();
}
inline auto
MemArena::end() const -> const_iterator {
return _active.end();
}
inline auto
MemArena::frozen_begin() const -> const_iterator {
return _frozen.begin();
}
inline auto
MemArena::frozen_end() const -> const_iterator {
return _frozen.end();
}
template <typename T> FixedArena<T>::FixedArena(MemArena &arena) : _arena(arena) {
static_assert(sizeof(T) >= sizeof(T *));
}
template <typename T>
template <typename... Args>
T *
FixedArena<T>::make(Args... args) {
if (_list._next) {
void *t = _list._next;
_list._next = _list._next->_next;
return new (t) T(std::forward<Args>(args)...);
}
return _arena.template make<T>(std::forward<Args>(args)...);
}
template <typename T>
void
FixedArena<T>::destroy(T *t) {
if (t) {
t->~T(); // destructor.
auto item = reinterpret_cast<Item *>(t);
item->_next = _list._next;
_list._next = item;
}
}
template <typename T>
void
FixedArena<T>::clear() {
_list._next = nullptr;
}
template <typename T>
MemArena &
FixedArena<T>::arena() {
return _arena;
}
/// @endcond INTERNAL_DETAIL
}} // namespace swoc::SWOC_VERSION_NS