This is an automated email from the ASF dual-hosted git repository.
amc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 60e778e MemArena: Add make method to construct objects in the arena.
Update documentation.
60e778e is described below
commit 60e778e177d36b6d4cb26689da2a9782821c8aa8
Author: Alan M. Carroll <[email protected]>
AuthorDate: Thu Jun 28 21:07:48 2018 -0500
MemArena: Add make method to construct objects in the arena.
Update documentation.
---
doc/conf.py | 3 +-
.../internal-libraries/MemArena.en.rst | 309 ++++++++-------------
lib/ts/MemArena.cc | 99 +++----
lib/ts/MemArena.h | 161 ++++++-----
lib/ts/unit-tests/test_MemArena.cc | 99 +++++--
5 files changed, 334 insertions(+), 337 deletions(-)
diff --git a/doc/conf.py b/doc/conf.py
index 1f09e42..54e7b10 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -167,7 +167,8 @@ pygments_style = 'sphinx'
#modindex_common_prefix = []
nitpicky = True
-nitpick_ignore = [
+nitpick_ignore = [ ('cpp:typeOrConcept', 'T')
+ , ('cpp:typeOrConcept', 'Args')
]
# Autolink issue references.
diff --git a/doc/developer-guide/internal-libraries/MemArena.en.rst
b/doc/developer-guide/internal-libraries/MemArena.en.rst
index 76ffdb4..6b2bc02 100644
--- a/doc/developer-guide/internal-libraries/MemArena.en.rst
+++ b/doc/developer-guide/internal-libraries/MemArena.en.rst
@@ -1,22 +1,17 @@
.. Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
+ or more contributor license agreements. See the NOTICE file distributed
with this work for
+ additional information regarding copyright ownership. The ASF licenses this
file to you under the
+ Apache License, Version 2.0 (the "License"); you may not use this file
except in compliance with
+ the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
-
-.. include:: ../../common.defs
+ Unless required by applicable law or agreed to in writing, software
distributed under the License
+ is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ or implied. See the License for the specific language governing permissions
and limitations under
+ the License.
+.. include:: ../../common.defs
.. highlight:: cpp
.. default-domain:: cpp
.. |MemArena| replace:: :class:`MemArena`
@@ -26,211 +21,133 @@
MemArena
*************
-|MemArena| provides a memory arena or pool for allocating memory. The intended
use is for allocating many small chunks of memory - few, large allocations are
best handled independently. The purpose is to amortize the cost of allocation
of each chunk across larger allocations in a heap style. In addition the
allocated memory is presumed to have similar lifetimes so that all of the
memory in the arena can be de-allocatred en masse. This is a memory allocation
style used by many cotainers - [...]
+|MemArena| provides a memory arena or pool for allocating memory. Internally
|MemArena| reserves
+memory in large blocks and allocates pieces of those blocks when memory is
requested. Upon
+destruction all of the reserved memory is released which also destroys all of
the allocated memory.
+This is useful when the goal is any (or all) of trying to
+
+* amortize allocation costs for many small allocations.
+* create better memory locality for containers.
+* de-allocate memory in bulk.
Description
+++++++++++
-|MemArena| manages an internal list of memory blocks, out of which it provides
allocated
-blocks of memory. When an instance is destructed all the internal blocks are
also freed. The
-expected use of this class is as an embedded memory manager for a container
class.
-
-To support coalescence and compaction of memory, the methods
:func:`MemArena::freeze` and
-:func:`MemArena::thaw` are provided. These create in effect generations of
memory allocation.
-Calling :func:`MemArena::freeze` marks a generation. After this call any
further allocations will
-be in new internal memory blocks. The corresponding call to
:func:`MemArena::thaw` cause older
-generations of internal memory to be freed. The general logic for the
container would be to freeze,
-re-allocate and copy the container elements, then thaw. This would result in
compacted memory
-allocation in a single internal block. The uses cases would be either a
process static data
-structure after initialization (coalescing for locality performence) or a
container that naturally
-re-allocates (such as a hash table during a bucket expansion). A container
could also provide its
-own API for its clients to cause a coalesence.
-
-Other than freeze / thaw, this class does not offer any mechanism to release
memory beyond its destruction. This is not an issue for either process globals
or transient arenas.
-
-Internals
-+++++++++
-
-|MemArena| opperates in *generations* of internal blocks of memory. Each
generation marks a series internal block of memory. Allocations always occur
from the most recent block within a generation, as it is always the largest and
has the most unallocated space. The most recent block (current) is also the
head of the linked list of memory blocks. Allocations are given in the form of
a :class:`MemSpan`. Once an internal block of memory has exhausted it's
avaliable space, a new, larger, int [...]
-
-.. uml::
- :align: center
-
- component [block] as b1
- component [block] as b2
- component [block] as b3
- component [block] as b4
- component [block] as b5
- component [block] as b6
-
- b1 -> b2
- b2 -> b3
- b3 -> b4
- b4 -> b5
- b5 -> b6
-
- generation -u- b3
- current -u- b1
-
-A call to :func:`MemArena::thaw` will deallocate any generation that is not
the current generation. Thus, currently it is impossible to deallocate ie. just
the third generation. Everything after the generation pointer is in previous
generations and everything before, and including, the generation pointer is in
the current generation. Since blocks are reference counted, thawing is just a
single assignment to drop everything after the generation pointer. After a
:func:`MemArena::thaw`:
-
-.. uml::
- :align: center
-
- component [block] as b3
- component [block] as b4
- component [block] as b5
- component [block] as b6
-
-
- b3 -> b4
- b4 -> b5
- b5 -> b6
-
- current -u- b3
- generation -u- b6
-
-A generation can only be updated with an explicit call to
:func:`MemArena::freeze`. The next generation is not actually allocated until a
call to :func:`MemArena::alloc` happens. On the :func:`MemArena::alloc`
following a :func:`MemArena::freeze` the next internal block of memory is the
larger of the sum of all current allocations or the number of bytes requested.
The reason for this is that the caller could :func:`MemArena::alloc` a size
larger than all current allocations at which poin [...]
-
-.. uml::
- :align: center
-
- component [block] as b3
- component [block] as b4
- component [block] as b5
- component [block] as b6
-
-
- b3 -> b4
- b4 -> b5
- b5 -> b6
-
- current -u- b3
-
-After the next :func:`MemArena::alloc`:
-
-.. uml::
- :align: center
-
- component [block\nnew generation] as b3
- component [block] as b4
- component [block] as b5
- component [block] as b6
- component [block] as b7
-
-
- b3 -> b4
- b4 -> b5
- b5 -> b6
- b6 -> b7
-
- generation -u- b3
- current -u- b3
-
-A caller can actually :func:`MemArena::alloc` **any** number of bytes.
Internally, if the arena is unable to allocate enough memory for the
allocation, it will create a new internal block of memory large enough and
allocate from that. So if the arena is allocated like:
-
-.. code-block:: cpp
-
- ts::MemArena *arena = new ts::MemArena(64);
-
-The caller can actually allocate more than 64 bytes.
-
-.. code-block:: cpp
-
- ts::MemSpan span1 = arena->alloc(16);
- ts::MemSpan span1 = arena->alloc(256);
-
-Now, span1 and span2 are in the same generation and can both be safely used.
After:
-
-.. code-block:: cpp
-
- arena->freeze();
- ts::MemSpan span3 = arena->alloc(512);
- arena->thaw();
-
-span3 can still be used but span1 and span2 have been deallocated and usage is
undefined.
-
-Internal blocks are adjusted for optimization. Each :class:`MemArena::Block`
is just a header for the underlying memory it manages. The header and memory
are allocated together for locality such that each :class:`MemArena::Block` is
immediately followed with the memory it manages. If a :class:`MemArena::Block`
is larger than a page (defaulted at 4KB), it is aligned to a power of two. The
actual memory that a :class:`MemArena::Block` can allocate out is slightly
smaller. This is because a [...]
+When a |MemArena| instance is constructed no memory is reserved. A hint can be
provided so that the
+first internal reservation of memory will have close to but at least that
amount of free space
+available to be allocated.
+
+In normal use memory is allocated from |MemArena| using
:func:`MemArena::alloc` to get chunks
+of memory, or :func:`MemArena::make` to get constructed class instances.
:func:`MemArena::make`
+takes an arbitrary set of arguments which it attempts to pass to a constructor
for the type
+:code:`T` after allocating memory (:code:`sizeof(T)` bytes) for the object. If
there isn't enough
+free reserved memory, a new internal block is reserved. The size of the new
reserved memory will be at least
+the size of the currently reserved memory, making each reservation larger than
the last.
+
+The arena can be **frozen** using :func:`MemArena::freeze` which locks down
the currently reserved
+memory and forces the internal reservation of memory for the next allocation.
By default this
+internal reservation will be the size of the frozen allocated memory. If this
isn't the best value a
+hint can be provided to the :func:`MemArena::freeze` method to specify a
different value, in the
+same manner as the hint to the constructor. When the arena is thawed
(unfrozen) using
+:func:`MemArena::thaw` the frozen memory is released, which also destroys the
frozen allocated
+memory. Doing this can be useful after a series of allocations, which can
result in the allocated
+memory being in different internal blocks, along with possibly no longer in
use memory. The result
+is to coalesce (or garbage collect) all of the in use memory in the arena into
a single bulk
+internal reserved block. This improves memory efficiency and memory locality.
This coalescence is
+done by
+
+#. Freezing the arena.
+#. Copying all objects back in to the arena.
+#. Thawing the arena.
+
+Because the default reservation hint is large enough for all of the previously
allocated memory, all
+of the copied objects will be put in the same new internal block. If this for
some reason this
+sizing isn't correct a hint can be passed to :func:`MemArena::freeze` to
specify a different value
+(if, for instance, there is a lot of unused memory of known size). Generally
this is most useful for
+data that is initialized on process start and not changed after process
startup. After the process
+start initilization, the data can be coalesced for better performance after
all modifications have
+been done. Alternatively, a container that allocates and de-allocates same
sized objects (such as a
+:code:`std::map`) can use a free list to re-use objects before going to the
|MemArena| for more
+memory and thereby avoiding collecting unused memory in the arena.
+
+Other than a freeze / thaw cycle, there is no mechanism to release memory
except for the destruction
+of the |MemArena|. In such use cases either wasted memory must be small enough
or temporary enough
+to not be an issue, or there must be a provision for some sort of garbage
collection.
+
+Generally |MemArena| is not as useful for classes that allocate their own
internal memory
+(such as :code:`std::string` or :code:`std::vector`), which includes most
container classes. One
+container class that can be easily used is :class:`IntrusiveDList` because the
links are in the
+instance and therefore also in the arena.
+
+Objects created in the arena must not have :code:`delete` called on them as
this will corrupt
+memory, usually leading to an immediate crash. The memory for the instance
will be released when the
+arena is destroyed. The destructor can be called if needed but in general if a
destructor is needed
+it is probably not a class that should be constructed in the arena. Looking at
+:class:`IntrusiveDList` again for an example, if this is used to link objects
in the arena, there is
+no need for a destructor to clean up the links - all of the objects will be
de-allocated when the
+arena is destroyed. Whether this kind of situation can be arranged with
reasonable effort is a good
+heuristic on whether |MemArena| is an appropriate choice.
+
+While |MemArena| will normally allocate memory in successive chunks from an
internal block, if the
+allocation request is large (more than a memory page) and there is not enough
space in the current
+internal block, a block just for that allocation will be created. This is
useful if the purpose of
+|MemArena| is to track blocks of memory more than reduce the number of system
level allocations.
Reference
+++++++++
.. class:: MemArena
- .. class:: Block
-
- Underlying memory allocated is owned by the :class:`Block`. A linked
list.
-
- .. member:: size_t size
- .. member:: size_t allocated
- .. member:: std::shared_ptr<Block> next
- .. function:: Block(size_t n)
- .. function:: char* data()
-
- .. function:: MemArena()
+ .. function:: MemArena(size_t n)
- Construct an empty arena.
-
- .. function:: explicit MemArena(size_t n)
-
- Construct an arena with :arg:`n` bytes.
+ Construct a memory arena. :arg:`n` is optional. Initially not memory is
reserved. If :arg:`n`
+ is provided this is a hint that the first internal memory reservation
should provide roughly
+ and at least :arg:`n` bytes of free space. Otherwise the internal
default hint is used. A call
+ to :code:`alloc(0)` will not allocate memory but will force the
reservation of internal memory
+ if this should be done immediately rather than lazily.
.. function:: MemSpan alloc(size_t n)
- Allocate an :arg:`n` byte chunk of memory in the arena.
-
- .. function:: MemArena& freeze(size_t n = 0)
+ Allocate memory of size :arg:`n` bytes in the arena. If :arg:`n` is zero
then internal memory
+ will be reserved if there is currently none, otherwise it is a no-op.
- Block all further allocation from any existing internal blocks. If
:arg:`n` is zero then on the next allocation request a block twice as large as
the current generation, otherwise the next internal block will be large enough
to hold :arg:`n` bytes.
+ .. function:: template < typename T, typename ... Args > T * make(Args&&
... args)
- .. function:: MemArena& thaw()
-
- Unallocate all internal blocks that were allocated before the current
generation.
-
- .. function:: MemArena& empty()
-
- Empties the entire arena and deallocates all underlying memory. Next
block size will be equal to the sum of all allocations before the call to empty.
-
- .. function:: size_t size() const
-
- Get the current generation size. The default size of the arena is 32KB
unless otherwise specified.
-
- .. function:: size_t remaining() const
-
- Amount of space left in the generation.
-
- .. function:: size_t allocated_size() const
-
- Total number of bytes allocated in the arena.
+ Create an instance of :arg:`T`. :code:`sizeof(T)` bytes of memory are
allocated from the arena
+ and the constructor invoked. This method takes any set of arguments,
which are passed to
+ the constructor. A pointer to the newly constructed instance of :arg:`T`
is returned. Note if
+ the instance allocates other memory that memory will not be in the
arena. Example constructing
+ a :code:`std::string_view` ::
- .. function:: size_t unallocated_size() const
+ std::string_view * sv = arena.make<std::string_view>(pointer, n);
- Total number of bytes unallocated in the arena. Can be used to see the
internal fragmentation.
+ .. function:: MemArena& freeze(size_t n)
- .. function:: bool contains (void *ptr) const
+ Stop allocating from existing internal memory blocks. These blocks are
now "frozen". Further
+ allocation calls will cause new memory to be reserved.
- Returns whether or not a pointer is in the arena.
-
- .. function:: Block* newInternalBlock(size_t n, bool custom)
+ :arg:`n` is optional. If not provided, make the hint for the next
internal memory reservation
+ to be large enough to hold all currently (now frozen) memory allocation.
If :arg:`n` is
+ provided it is used as the reservation hint.
- Create a new internal block and returns a pointer to the block.
-
- .. member:: size_t arena_size
-
- Current generation size.
-
- .. member:: size_t total_alloc
-
- Number of bytes allocated out.
-
- .. member:: size_t next_block_size
+ .. function:: MemArena& thaw()
- Size of next generation.
+ Release all frozen internal memory blocks, destroying all frozen
allocations.
- .. member:: std::shared_ptr<Block> generation
+ .. function:: MemArena& clear(size_t n)
- Pointer to the current generation.
+ Release all memory, destroying all allocations. The next memory
reservation will be the size
+ of the allocated memory (frozen and not) at the time of the call to
:func:`MemArena::clear`.
+ :arg:`n` is optional. If this is provided it is used as the hint for the
next reserved block,
+ otherwise the hint is the size of all allocated memory.
- .. member:: std::shared_ptr<Block> current
+Internals
++++++++++
- Pointer to most recent internal block of memory.
+Allocated memory is tracked by two linked lists, one for current memory and
the other for frozen
+memory. The latter is used only while the arena is frozen. Because a shared
pointer is used for the
+link, the list can be de-allocated by clearing the head pointer in |MemArena|.
This pattern is
+similar to that used by the :code:`IOBuffer` data blocks, and so those were
considered for use as
+the internal memory allcation blocks. However, that would have required some
non-trivial tweaks and,
+with the move away from internal allocation pools to memory support from
libraries like "jemalloc",
+unlikely to provide any benefit.
diff --git a/lib/ts/MemArena.cc b/lib/ts/MemArena.cc
index 9b3db24..1646282 100644
--- a/lib/ts/MemArena.cc
+++ b/lib/ts/MemArena.cc
@@ -39,9 +39,18 @@ MemArena::Block::operator delete(void *ptr)
MemArena::BlockPtr
MemArena::make_block(size_t n)
{
+ // If there's no reservation hint, use the extent. This is transient because
the hint is cleared.
+ if (_reserve_hint == 0) {
+ if (_active_reserved) {
+ _reserve_hint = _active_reserved;
+ } else if (_prev_allocated) {
+ _reserve_hint = _prev_allocated;
+ }
+ }
+
// If post-freeze or reserved, allocate at least that much.
- n = std::max<size_t>(n, next_block_size);
- next_block_size = 0; // did this, clear for next time.
+ n = std::max<size_t>(n, _reserve_hint);
+ _reserve_hint = 0; // did this, clear for next time.
// Add in overhead and round up to paragraph units.
n = Paragraph{round_up(n + ALLOC_HEADER_SIZE + sizeof(Block))};
// If a page or more, round up to page unit size and clip back to account
for alloc header.
@@ -51,50 +60,43 @@ MemArena::make_block(size_t n)
// Allocate space for the Block instance and the request memory and
construct a Block at the front.
// In theory this could use ::operator new(n) but this causes a size
mismatch during ::operator delete.
- // Easier to use malloc and not carry a memory block size value around.
- return BlockPtr(new (::malloc(n)) Block(n - sizeof(Block)));
-}
-
-MemArena::MemArena(size_t n)
-{
- next_block_size = 0; // Don't use default size.
- current = this->make_block(n);
+ // Easier to use malloc and override @c delete.
+ auto free_space = n - sizeof(Block);
+ _active_reserved += free_space;
+ return BlockPtr(new (::malloc(n)) Block(free_space));
}
MemSpan
MemArena::alloc(size_t n)
{
MemSpan zret;
- current_alloc += n;
-
- if (!current) {
- current = this->make_block(n);
- zret = current->alloc(n);
- } else if (n > current->remaining()) { // too big, need another block
- if (next_block_size < n) {
- next_block_size = 2 * current->size;
- }
+ _active_allocated += n;
+
+ if (!_active) {
+ _active = this->make_block(n);
+ zret = _active->alloc(n);
+ } else if (n > _active->remaining()) { // too big, need another block
BlockPtr block = this->make_block(n);
// For the new @a current, pick the block which will have the most free
space after taking
// the request space out of the new block.
zret = block->alloc(n);
- if (block->remaining() > current->remaining()) {
- block->next = current;
- current = block;
+ if (block->remaining() > _active->remaining()) {
+ block->next = _active;
+ _active = block;
#if defined(__clang_analyzer__)
// Defeat another clang analyzer false positive. Unit tests validate the
code is correct.
ink_assert(current.use_count() > 1);
#endif
} else {
- block->next = current->next;
- current->next = block;
+ block->next = _active->next;
+ _active->next = block;
#if defined(__clang_analyzer__)
// Defeat another clang analyzer false positive. Unit tests validate the
code is correct.
ink_assert(block.use_count() > 1);
#endif
}
} else {
- zret = current->alloc(n);
+ zret = _active->alloc(n);
}
return zret;
}
@@ -102,11 +104,15 @@ MemArena::alloc(size_t n)
MemArena &
MemArena::freeze(size_t n)
{
- prev = current;
- prev_alloc = current_alloc;
- current.reset();
- next_block_size = n ? n : current_alloc;
- current_alloc = 0;
+ _prev = _active;
+ _active.reset(); // it's in _prev now, start fresh.
+ // Update the meta data.
+ _prev_allocated = _active_allocated;
+ _active_allocated = 0;
+ _prev_reserved = _active_reserved;
+ _active_reserved = 0;
+
+ _reserve_hint = n;
return *this;
}
@@ -114,20 +120,20 @@ MemArena::freeze(size_t n)
MemArena &
MemArena::thaw()
{
- prev_alloc = 0;
- prev.reset();
+ _prev.reset();
+ _prev_reserved = _prev_allocated = 0;
return *this;
}
bool
MemArena::contains(const void *ptr) const
{
- for (Block *b = current.get(); b; b = b->next.get()) {
+ for (Block *b = _active.get(); b; b = b->next.get()) {
if (b->contains(ptr)) {
return true;
}
}
- for (Block *b = prev.get(); b; b = b->next.get()) {
+ for (Block *b = _prev.get(); b; b = b->next.get()) {
if (b->contains(ptr)) {
return true;
}
@@ -137,26 +143,13 @@ MemArena::contains(const void *ptr) const
}
MemArena &
-MemArena::clear()
+MemArena::clear(size_t n)
{
- prev.reset();
- prev_alloc = 0;
- current.reset();
- current_alloc = 0;
+ _reserve_hint = n ? n : _prev_allocated + _active_allocated;
+ _prev.reset();
+ _prev_reserved = _prev_allocated = 0;
+ _active.reset();
+ _active_reserved = _active_allocated = 0;
return *this;
}
-
-size_t
-MemArena::extent() const
-{
- size_t zret{0};
- Block *b;
- for (b = current.get(); b; b = b->next.get()) {
- zret += b->size;
- }
- for (b = prev.get(); b; b = b->next.get()) {
- zret += b->size;
- }
- return zret;
-};
diff --git a/lib/ts/MemArena.h b/lib/ts/MemArena.h
index ad10cee..fe00eaf 100644
--- a/lib/ts/MemArena.h
+++ b/lib/ts/MemArena.h
@@ -26,6 +26,7 @@
#include <new>
#include <mutex>
#include <memory>
+#include <utility>
#include <ts/MemSpan.h>
#include <ts/Scalar.h>
#include <tsconfig/IntrusivePtr.h>
@@ -33,29 +34,31 @@
/// Apache Traffic Server commons.
namespace ts
{
-/** MemArena is a memory arena for allocations.
+/** A memory arena.
- The intended use is for allocating many small chunks of memory - few,
large allocations are best handled independently.
- The purpose is to amortize the cost of allocation of each chunk across
larger allocations in a heap style. In addition the
- allocated memory is presumed to have similar lifetimes so that all of the
memory in the arena can be de-allocatred en masse.
-
- A generation is essentially a block of memory. The normal workflow is to
freeze() the current generation, alloc() a larger and
- newer generation, copy the contents of the previous generation to the new
generation, and then thaw() the previous generation.
- Note that coalescence must be done by the caller because MemSpan will only
give a reference to the underlying memory.
+ 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
{
using self_type = MemArena; ///< Self reference type.
protected:
- struct Block;
+ struct Block; // Forward declare
using BlockPtr = ts::IntrusivePtr<Block>;
friend struct IntrusivePtrPolicy<Block>;
/** Simple internal arena block of memory. Maintains the underlying memory.
+ *
+ * Intrusive pointer is used to keep all of the memory in this single block.
This struct is just
+ * the header on the full memory block allowing the raw memory and the meta
data to be obtained
+ * in a single memory allocation.
*/
struct Block : public ts::IntrusivePtrCounter {
size_t size; ///< Actual block size.
size_t allocated{0}; ///< Current allocated (in use) bytes.
- BlockPtr next; ///< Previously allocated block list.
+ BlockPtr next; ///< List of previous blocks.
/** Construct to have @a n bytes of available storage.
*
@@ -64,14 +67,19 @@ protected:
* @param n The amount of storage.
*/
Block(size_t n);
+
/// Get the start of the data in this block.
char *data();
+
/// Get the start of the data in this block.
const char *data() const;
+
/// Amount of unallocated storage.
size_t remaining() const;
+
/// Span of unallocated storage.
MemSpan remnant();
+
/** Allocate @a n bytes from this block.
*
* @param n Number of bytes to allocate.
@@ -89,7 +97,7 @@ protected:
/** Override standard delete.
*
* This is required because the allocated memory size is larger than the
class size which requires
- * passing different parameters to de-allocate the memory.
+ * calling @c free differently.
*
* @param ptr Memory to be de-allocated.
*/
@@ -97,66 +105,76 @@ protected:
};
public:
- /** Default constructor.
- * Construct with no memory.
- */
- MemArena();
- /** Construct with @a n bytes of storage.
+ /** Construct with reservation hint.
*
- * @param n Number of bytes in the initial block.
+ * 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);
+ explicit MemArena(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.
+ 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.
@return a MemSpan of the allocated memory.
*/
MemSpan alloc(size_t n);
- /** Adjust future block allocation size.
- This does not cause allocation, but instead makes a note of the size @a
n and when a new block
- is needed, it will be at least @a n bytes. This is most useful for
default constructed instances
- where the initial allocation should be delayed until use.
- @param n Minimum size of next allocated block.
- @return @a this
- */
- self_type &reserve(size_t n);
+ /** Allocate and initialize a block of memory.
+
+ The template type specifies the type to create and any arguments are
forwarded to the constructor. Example:
+ @code
+ struct Thing { ... };
+ Thing* thing = arena.make<Thing>(...constructor args...);
+ @endcode
- /** Freeze memory allocation.
+ 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);
- Will "freeze" a generation of memory. Any memory previously allocated
can still be used. This is an
- important distinction as freeze does not mean that the memory is
immutable, only that subsequent allocations
- will be in a new generation.
+ /** Freeze reserved memory.
- If @a n == 0, the first block of next generation will be large enough to
hold all existing allocations.
- This enables coalescence for locality of reference.
+ 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 Number of bytes for new generation.
+ @param n Target number of available bytes in the next reserved internal
block.
@return @c *this
*/
MemArena &freeze(size_t n = 0);
- /** Unfreeze memory allocation, discard previously frozen memory.
-
- Will "thaw" away any previously frozen generations. Any generation that
is not the current generation is considered
- frozen because there is no way to allocate in any of those memory
blocks. thaw() is the only mechanism for deallocating
- memory in the arena (other than destroying the arena itself). Thawing
away previous generations means that all spans
- of memory allocated in those generations are no longer safe to use.
-
- @return @c *this
+ /** Unfreeze arena.
+ *
+ * Frozen memory is released.
+ *
+ * @return @c *this
*/
- MemArena &thaw();
+ self_type &thaw();
/** Release all memory.
- Empties the entire arena and deallocates all underlying memory. Next
block size will be equal to the sum of all
- allocations before the call to empty.
+ Empties the entire arena and deallocates all underlying memory. The hint
for the next reservered 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.
+
+ @return @c *this
+
*/
- MemArena &clear();
+ MemArena &clear(size_t n = 0);
/// @returns the memory allocated in the generation.
size_t size() const;
@@ -180,11 +198,9 @@ public:
/** Total memory footprint, including wasted space.
* @return Total memory footprint.
*/
- size_t extent() const;
+ size_t reserved_size() const;
protected:
- /// creates a new @c Block with at least @n free space.
-
/** Internally allocates a new block of memory of size @a n bytes.
*
* @param n Size of block to allocate.
@@ -199,14 +215,19 @@ protected:
/// 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 current_alloc = 0; ///< Total allocations in the active generation.
+ size_t _active_allocated = 0; ///< Total allocations in the active
generation.
+ size_t _active_reserved = 0; ///< Total current reserved memory.
/// Total allocations in the previous generation. This is only non-zero
while the arena is frozen.
- size_t prev_alloc = 0;
+ size_t _prev_allocated = 0;
+ /// Total frozen reserved memory.
+ size_t _prev_reserved = 0;
- size_t next_block_size = DEFAULT_BLOCK_SIZE; ///< Next internal block size
+ /// Minimum free space needed in the next allocated block.
+ /// This is not zero iff @c reserve was called.
+ size_t _reserve_hint = 0;
- BlockPtr prev; ///< Previous generation.
- BlockPtr current; ///< Head of allocations list. Allocate from this.
+ BlockPtr _prev; ///< Previous generation, frozen memory.
+ BlockPtr _active; ///< Current generation. Allocate here.
};
// Implementation
@@ -247,7 +268,14 @@ MemArena::Block::alloc(size_t n)
return zret;
}
-inline MemArena::MemArena() {}
+template <typename T, typename... Args>
+T *
+MemArena::make(Args &&... args)
+{
+ return new (this->alloc(sizeof(T)).data()) T(std::forward<Args>(args)...);
+}
+
+inline MemArena::MemArena(size_t n) : _reserve_hint(n) {}
inline MemSpan
MemArena::Block::remnant()
@@ -258,32 +286,31 @@ MemArena::Block::remnant()
inline size_t
MemArena::size() const
{
- return current_alloc;
+ return _active_allocated;
}
inline size_t
MemArena::allocated_size() const
{
- return prev_alloc + current_alloc;
-}
-
-inline MemArena &
-MemArena::reserve(size_t n)
-{
- next_block_size = n;
- return *this;
+ return _prev_allocated + _active_allocated;
}
inline size_t
MemArena::remaining() const
{
- return current ? current->remaining() : 0;
+ return _active ? _active->remaining() : 0;
}
inline MemSpan
MemArena::remnant() const
{
- return current ? current->remnant() : MemSpan{};
+ return _active ? _active->remnant() : MemSpan{};
+}
+
+inline size_t
+MemArena::reserved_size() const
+{
+ return _active_reserved + _prev_reserved;
}
} // namespace ts
diff --git a/lib/ts/unit-tests/test_MemArena.cc
b/lib/ts/unit-tests/test_MemArena.cc
index 6e358b4..18d2dad 100644
--- a/lib/ts/unit-tests/test_MemArena.cc
+++ b/lib/ts/unit-tests/test_MemArena.cc
@@ -23,15 +23,20 @@
#include <catch.hpp>
+#include <string_view>
#include <ts/MemArena.h>
using ts::MemSpan;
using ts::MemArena;
+using namespace std::literals;
TEST_CASE("MemArena generic", "[libts][MemArena]")
{
ts::MemArena arena{64};
REQUIRE(arena.size() == 0);
- REQUIRE(arena.extent() >= 64);
+ REQUIRE(arena.reserved_size() == 0);
+ arena.alloc(0);
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.reserved_size() >= 64);
ts::MemSpan span1 = arena.alloc(32);
REQUIRE(span1.size() == 32);
@@ -42,9 +47,9 @@ TEST_CASE("MemArena generic", "[libts][MemArena]")
REQUIRE(span1.data() != span2.data());
REQUIRE(arena.size() == 64);
- auto extent{arena.extent()};
+ auto extent{arena.reserved_size()};
span1 = arena.alloc(128);
- REQUIRE(extent < arena.extent());
+ REQUIRE(extent < arena.reserved_size());
}
TEST_CASE("MemArena freeze and thaw", "[libts][MemArena]")
@@ -53,45 +58,75 @@ TEST_CASE("MemArena freeze and thaw", "[libts][MemArena]")
MemSpan span1{arena.alloc(1024)};
REQUIRE(span1.size() == 1024);
REQUIRE(arena.size() == 1024);
+ REQUIRE(arena.reserved_size() >= 1024);
arena.freeze();
REQUIRE(arena.size() == 0);
REQUIRE(arena.allocated_size() == 1024);
- REQUIRE(arena.extent() >= 1024);
+ REQUIRE(arena.reserved_size() >= 1024);
arena.thaw();
REQUIRE(arena.size() == 0);
- REQUIRE(arena.extent() == 0);
+ REQUIRE(arena.allocated_size() == 0);
+ REQUIRE(arena.reserved_size() == 0);
- arena.reserve(2000);
+ span1 = arena.alloc(1024);
+ arena.freeze();
+ auto extent{arena.reserved_size()};
arena.alloc(512);
- arena.alloc(1024);
- REQUIRE(arena.extent() >= 1536);
- REQUIRE(arena.extent() < 3000);
- auto extent = arena.extent();
+ REQUIRE(arena.reserved_size() > extent); // new extent should be bigger.
+ arena.thaw();
+ REQUIRE(arena.size() == 512);
+ REQUIRE(arena.reserved_size() >= 1024);
+
+ arena.clear();
+ REQUIRE(arena.size() == 0);
+ REQUIRE(arena.reserved_size() == 0);
+ span1 = arena.alloc(262144);
arena.freeze();
+ extent = arena.reserved_size();
arena.alloc(512);
- REQUIRE(arena.extent() > extent); // new extent should be bigger.
+ REQUIRE(arena.reserved_size() > extent); // new extent should be bigger.
arena.thaw();
REQUIRE(arena.size() == 512);
- REQUIRE(arena.extent() > 1536);
+ REQUIRE(arena.reserved_size() >= 262144);
arena.clear();
- REQUIRE(arena.size() == 0);
- REQUIRE(arena.extent() == 0);
+
+ span1 = arena.alloc(262144);
+ extent = arena.reserved_size();
+ arena.freeze();
+ for (int i = 0; i < 262144 / 512; ++i)
+ arena.alloc(512);
+ REQUIRE(arena.reserved_size() > extent); // Bigger while frozen memory is
still around.
+ arena.thaw();
+ REQUIRE(arena.size() == 262144);
+ REQUIRE(arena.reserved_size() == extent); // should be identical to before
freeze.
arena.alloc(512);
arena.alloc(768);
arena.freeze(32000);
arena.thaw();
- arena.alloc(1);
- REQUIRE(arena.extent() >= 32000);
+ arena.alloc(0);
+ REQUIRE(arena.reserved_size() >= 32000);
+ REQUIRE(arena.reserved_size() < 2 * 32000);
}
TEST_CASE("MemArena helper", "[libts][MemArena]")
{
+ struct Thing {
+ int ten{10};
+ std::string name{"name"};
+
+ Thing() {}
+ Thing(int x) : ten(x) {}
+ Thing(std::string const &s) : name(s) {}
+ Thing(int x, std::string_view s) : ten(x), name(s) {}
+ Thing(std::string const &s, int x) : ten(x), name(s) {}
+ };
+
ts::MemArena arena{256};
REQUIRE(arena.size() == 0);
ts::MemSpan s = arena.alloc(56);
@@ -115,6 +150,31 @@ TEST_CASE("MemArena helper", "[libts][MemArena]")
arena.thaw();
REQUIRE(!arena.contains((char *)ptr));
REQUIRE(arena.contains((char *)ptr2));
+
+ Thing *thing_one{arena.make<Thing>()};
+
+ REQUIRE(thing_one->ten == 10);
+ REQUIRE(thing_one->name == "name");
+
+ thing_one = arena.make<Thing>(17, "bob"sv);
+
+ REQUIRE(thing_one->name == "bob");
+ REQUIRE(thing_one->ten == 17);
+
+ thing_one = arena.make<Thing>("Dave", 137);
+
+ REQUIRE(thing_one->name == "Dave");
+ REQUIRE(thing_one->ten == 137);
+
+ thing_one = arena.make<Thing>(9999);
+
+ REQUIRE(thing_one->ten == 9999);
+ REQUIRE(thing_one->name == "name");
+
+ thing_one = arena.make<Thing>("Persia");
+
+ REQUIRE(thing_one->ten == 10);
+ REQUIRE(thing_one->name == "Persia");
}
TEST_CASE("MemArena large alloc", "[libts][MemArena]")
@@ -170,17 +230,16 @@ TEST_CASE("MemArena block allocation",
"[libts][MemArena]")
TEST_CASE("MemArena full blocks", "[libts][MemArena]")
{
// couple of large allocations - should be exactly sized in the generation.
- ts::MemArena arena;
size_t init_size = 32000;
+ ts::MemArena arena(init_size);
- arena.reserve(init_size);
MemSpan m1{arena.alloc(init_size - 64)};
MemSpan m2{arena.alloc(32000)};
MemSpan m3{arena.alloc(64000)};
REQUIRE(arena.remaining() >= 64);
- REQUIRE(arena.extent() > 32000 + 64000 + init_size);
- REQUIRE(arena.extent() < 2 * (32000 + 64000 + init_size));
+ REQUIRE(arena.reserved_size() > 32000 + 64000 + init_size);
+ REQUIRE(arena.reserved_size() < 2 * (32000 + 64000 + init_size));
// Let's see if that memory is really there.
memset(m1.data(), 0xa5, m1.size());