blob: 1194090044e595dd13b370b2336199c75653c28f [file] [log] [blame]
.. 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
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.rst
.. highlight:: cpp
.. default-domain:: cpp
.. |MemArena| replace:: :code:`MemArena`
.. _swoc-mem-arena:
********
MemArena
********
Synopsis
********
:code:`#include "swoc/MemArena.h"`
.. class:: template < typename T > MemArena
:libswoc:`Reference documentation <MemArena>`.
|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
* amortizing allocation costs for many small allocations.
* better memory locality for containers.
* de-allocating memory in bulk.
Note that intrusive containers such as :ref:`IntrusiveDList <swoc-intrusive-list>` and
:ref:`IntrusiveHashMap <swoc-intrusive-hashmap>` work well with a |MemArena|. If the container and
its elements are placed in the |MemArena| then no specific cleanup is needed beyond destroying the
|MemArena|.
A |MemArena| can also be `inverted <arena-inversion>`_. This means placing the |MemArena| instance
in its own memory pool so the |MemArena| and associated objects can be created with a single
base library memory allocation and cleaned up with a single :code:`delete`.
Usage
*****
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 :libswoc:`MemArena::alloc` to get chunks
of memory, or :libswoc:`MemArena::make` to get constructed class instances. :libswoc:`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 :libswoc:`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 :libswoc:`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
:libswoc:`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 :libswoc:`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. Intrusive
container classes, such as :class:`IntrusiveDList` and :class:`IntrusiveHashMap`, are more useful
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.
Generally the |MemArena| will be declared as a member variable of the the class that needs the
local memory. The class destructor will then clean up the memory when the containing class is
destructed. An archetypical example is :class:`Lexicon` which uses a |MemArena| to store its
data. :class:`IntrusiveHashMap` instances are used to track the data which is owned by the arena
which in turn is owned by the :code:`Lexicon` instance. Destroying the :code:`Lexicon` releases
all of the memory. There is no explicit cleanup code in :code:`Lexicon` precisely because the
|MemArena| destructor does that.
Temporary Allocation
====================
In some situations it is useful to allocate memory temporarily. While :code:`alloca` can be used
for this, that can be a bit risky because the stack is generally much smaller than the heap and
may be heavily used. |MemArena| provides the :libswoc:`MemArena::require` method as an alternative.
|MemArena| allocates memory in blocks and then hands out pieces of these blocks when it is asked for
memory. Therefore it is usually the case that some amount of not currently used memory is available
in the active block. Access to this memory can be obtained via :libswoc:`MemArena::remnant`. This
may be large enough for use as the needed temporary memory, in which case no allocation needs to be
done. What :libswoc:`MemArena::require` does is guarantee the remnant is at least the specified
size. If it is already large enough, nothing is done, otherwise a new block is allocated such that
it has enough free space. If this is done multiple times without allocating from the arena, the same
memory space is reused. It is also guaranteed that if the next call to :libswoc:`MemArena::alloc` or
:libswoc:`MemArena::make` does not need more than the remnant, it will be allocated from the
remnant. This makes it possible to do speculative work in the arena and "commit" it (via allocation)
after the work is successful, or abandon it if not.
Static Memory
=============
|MemArena| has :libswoc:`a constructor <MemArena::MemArena(MemSpan\<void\>)>` that enables using a
pre-defined block of memory. This memory is never released or destructed by the |MemArena|. This is
useful in local contexts where an abitrary amount of memory *may* be needed but commonly less than a
known amount of memory will actually be needed. In such a case the |MemArena| can be seeded with a
block of memory (static or on the stack) of that size so that no allocation is done in the common
case, only in the few cases where it is needed without special handling. Such a increase in
performance is essentially the only reason to use this feature.
The static block must be large enough to hold the internal block headers and still have at least
the minimum free space available. As of this writing this requires a buffer of at least 64 bytes.
If (roughly) two kilobytes would normally be enough, that could be done with ::
static constexpr size_t SIZE = 2048;
std::byte buffer[SIZE];
MemArena arena{{buffer, SIZE}};
All methods are available and functional in this case, including freezing and thawing. Any
allocations while frozen will never be in the static block, as it won't be recycled until the thaw.
Generally this is not a good technique as a situation where freeze / thaw is useful is almost
certainly not a situation where a static block is useful and vice versa.
Although such an arena could be inverted (and thereby placed in the static block) this is also very
unlikely to be useful, as the stack space for the arena would still be required during construction.
The expectation is such an instance would be scoped locally to a function or method and destroyed
upon return, being used only for temporary storage.
Examples
========
It is expected that |MemArena| will be used most commonly for string storage, where a container needs
strings and can't guarantee the lifetime of strings in arguments. A common function that is used is
a "localize" function that takes a string, allocates a copy in the arena, and returns a view of it. E.g.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 37-43
This can then be used to create a view inside the arena.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 187-188
That's about it for putting strings in the arena. Now, consider a class to be put in an arena.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 147-151,155-164, 185
A key point to note is :arg:`name` is a view, not a :code:`std::string`. This means it requires
storage elsewhere, i.e. in the arena. The string :arg:`text` has already been localized so instances
can be created safely
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 190-196
All of these are in the arena and are de-allocated when the arena is cleared or destructed. Note a
literal constant can be safely used because it has a process life time. That's a bit obscure - in
actual use it's unlikely the API will know this and will localize such strings. Also note
:libswoc:`MemArena::make` can use any of the constructors in :code:`Thing`.
Another case for string storage is formatted strings. Naturally :class:`BufferWriter` will be used
because it is just so awesome. The :libswoc:`MemArena::remnant` method will also be used to do
speculative formatted output. A problem is that at the time the formatted is provided, the final
size is not known, and so (in general) two passes are needed, one to size and one to do the actual
output. This can be optimized somewhat by using :libswoc:`MemArena::remnant`. The concept is to do
the sizing pass in to the space. If it fits, win - the string is there and no more needs to be done.
Otherwise the size needed is known and :libswoc:`MemArena::require` is used to get the contiguous
memory needed for the string. :libswoc:`MemArena::alloc` is then used to allocate the already
initialized memory.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 201-208
The code is a bit awkward and repetitious due to being example code. Production code would look
something like
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 132-143
Because this is a function there's no more repetition of the format string and arguments in use.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 210-212
:emphasize-lines: 1
.. _arena-inversion:
Inversion
---------
"Inversion" is the technique of putting the |MemArena| inside its own arena. For collections of
objects which all have the same lifetime this makes cleanup simple, as only the |MemArena| needs to
be destructed. In addition, it can minimize calls to the base allocator. If a good guess as to the
total size needed can be made, all of the objects, including the arena, can be provided for in a
single call to :code:`malloc`. This is used in :class:`Errata` for performance reasons. Here is a
simplified example.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 65-67
Things of note:
* A local stack variable is used to bootstrap the |MemArena|.
* A new |MemArena| is created in the |MemArena| and the essence of the local arena moved there.
* This works even if data has already been allocated in the arena, but it's best to do first.
* The resulting pointer is cleaned up by calling the destructor, not by :code:`delete`. This is
because, as noted earlier, it is not valid to call :code:`delete` on an object in the arena.
* The destructor is carefully implemented to work correctly even if the class instance is in
its own internal memory blocks.
In actual use, the pointer would be a class member, not another local variable, and the inversion
delayed until the memory was actually needed so that if the arena isn't needed, no allocation is done.
Although :code:`delete` cannot be used on an inverted |MemArena|, it is still possible to use
code:`std::unique_ptr` by overriding the cleanup.
Now for the actual example. This uses the :code:`localize` function from the previous example.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 79-102
:emphasize-lines: 1,10
:linenos:
In this code, :arg:`ta` is the local bootstrap temporary. The :code:`std::unique_ptr` is declared to
take as a "deleter" a function taking a :code:`MemArena *`. The actual function is the lambda in
:arg:`destroyer` declared in line 1. This could have been done directly inline, such as
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 118-121
but I think separating it is better looking code.
The checking code shows that memory can be allocated in the arena first, but is not in the arena
after the inversion. It is, however, in the inverted arena at the same address as demonstrated by
line 16. All of the allocated memory involved gets cleaned up when the :code:`std::unqiue_ptr` is
destructed.
Inversion is a bit tricky to understand but that's all there is to writing code to use it, although
setting up the :code:`std::unique_ptr` can be done in a variety of ways. Although these are
functionally identical, the memory footprint varies as noted in the comments. This can make a
difference for objects that are intended to be very small if no memory is allocated.
Two styles of lambda use have been shown, but there is one more of interest.
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 125-128
This requires the separate declaration of the lambda but cuts the :code:`std::unique_ptr` size in half. Note
every lambda is a distinct type, *even if the code is character for character identical*. This means the
pointer **must** be passed :arg:`destroyer` - there is no other object that will be valid. Despite this,
it must be passed as an argument, it cannot be omitted. But, hey, it saves 8 bytes!
Instead of a lambda, one can define a function
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 53-57
and use that
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 112-114
Finally one can define a functor
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 45-51
and use that
.. literalinclude:: ../../unit_tests/ex_MemArena.cc
:lines: 106-108
Note in this case the :code:`std::unqiue_ptr` is only 8 bytes (a single pointer) and doesn't
require an argument to the constructor.
Internals
*********
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. The list of blocks is maintained by
:ref:`IntrusiveDList <swoc-intrusive-list>` which means deallocating the memory blocks suffices to
clean up. Memory blocks are kept in an active pool so that if a new block is allocated to satisfy a
particular allocation or requirement, unused space in previous blocks is still available for use.