Storage customisation#
In a previous tutorial, we mentioned briefly how the wrap
class
implements a small storage optimisation, but we left the specifics out. In this tutorial,
we will see how it is possible to customise the storage options for the wrap
class.
How small is small?#
The threshold size under which the wrap
class avoids dynamic memory allocation
is represented by the configuration parameter config::static_size
. The unit of measure,
as usual, is bytes.
The default value for config::static_size
is somewhat arbitrary, but it should ensure that
on most platforms it is possible to store in static storage pointers and structs with a few members.
If config::static_size
is set to zero, then the small-object optimisation is disabled and the
wrap
class always uses dynamically-allocated memory.
One very important thing to understand about config::static_size
is that it accounts
also for the type-erasure storage overhead. That is, if you specify a config::static_size
of 24 bytes, some of those bytes will be taken up by the type-erasure machinery, and thus the
storage available for the type-erased value will be less than 24 bytes. How much less exactly
is dependent on a variety of factors and difficult to compute in advance.
For this reason, tanuki provides a holder_size
helper which can be used to compute
how much total memory is needed to statically store a value of a type T
in a wrap
.
Let us a see a simple example.
We have our usual super-basic interface and its implementation:
template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};
struct any_iface {
template <typename Base, typename Holder, typename T>
using impl = any_iface_impl<Base, Holder, T>;
};
Let us say that we want to ensure that we can store in static storage objects which are up
to the size of a pointer. We can define a custom config
instance in which we
set the config::static_size
parameter to holder_size<void *, any_iface>
:
inline constexpr auto custom_config1 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>};
This will ensure that value types whose size is up to sizeof(void *)
can be stored
in static storage.
We can then define a wrap
class using the custom config
instance and
verify that indeed static storage is employed when the wrap
contains a pointer
using the has_static_storage()
function:
using wrap1_t = tanuki::wrap<any_iface, custom_config1>;
wrap1_t w1(nullptr);
std::cout << std::boolalpha;
std::cout << "Is w1 using static storage? " << has_static_storage(w1) << '\n';
Is w1 using static storage? true
On the other hand, if we try to store 2 pointers instead of 1, we can verify that the
wrap
class switches to dynamic storage:
struct two_ptrs {
void *p1 = nullptr;
void *p2 = nullptr;
};
wrap1_t w2(two_ptrs{});
std::cout << "Is w2 using static storage? " << has_static_storage(w2) << '\n';
Is w2 using static storage? false
One limitation of the holder_size
helper is that it requires the interface
(i.e., the second parameter - any_iface
in the example) to have an implementation
for the value type (i.e., the first parameter - void *
in the example).
What about alignment?#
The alignment of the static storage within wrap
can be configured
via the config::static_align
config option. By default, the maximum
alignment supported on the platform (that is, alignof(std::max_align_t)
) is used.
This ensures that objects of any type can be stored in static storage (provided of
course that there is enough space).
In some cases, it may be convenient to specify a smaller config::static_align
value in order to reduce the memory footprint of a wrap
. Note that
config::static_align
is subject to the usual constraints
for an alignment value - specifically, the value must be a power of two.
If a config::static_align
value smaller than the default one is specified,
and the alignment required for a specific value type T
exceeds it,
then the wrap
class will automatically switch to dynamic storage.
Analogously to config::static_size
, config::static_align
accounts
for the alignment constraints imposed not only by the value type but also by the type-erasure
machinery. That is, if you specify a config::static_align
of 8, that does not necessarily
mean that it is possible to store in static storage values with an alignment of 8 or less, as the
type-erasure machinery might impose stricter alignment constraints.
Similarly to holder_size
, the holder_align
helper can be used to
compute the alignment requirement of wrap
’s static storage for a specific value type T
.
Let us see a simple example.
First, we define a new configuration instance in which we specify both a custom static size and a custom alignment:
constexpr auto custom_config2 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>,
.static_align = tanuki::holder_align<void *, any_iface>};
We can then define a new wrap type using the custom_config2
settings, and verify that it can store pointers in static storage:
using wrap2_t = tanuki::wrap<any_iface, custom_config2>;
wrap2_t w3(nullptr);
std::cout << "Is w3 using static storage? " << has_static_storage(w3) << '\n';
Is w3 using static storage? true
Finally, we can verify how specifying a smaller alignment can (at least on some platforms) reduce the
memory footprint of the wrap
class:
std::cout << "sizeof(wrap1_t) is " << sizeof(wrap1_t) << ", sizeof(wrap2_t) is " << sizeof(wrap2_t) << '\n';
sizeof(wrap1_t) is 32, sizeof(wrap2_t) is 24
Full code listing#
#include <iostream>
#include <tanuki/tanuki.hpp>
template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};
struct any_iface {
template <typename Base, typename Holder, typename T>
using impl = any_iface_impl<Base, Holder, T>;
};
inline constexpr auto custom_config1 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>};
int main()
{
using wrap1_t = tanuki::wrap<any_iface, custom_config1>;
wrap1_t w1(nullptr);
std::cout << std::boolalpha;
std::cout << "Is w1 using static storage? " << has_static_storage(w1) << '\n';
struct two_ptrs {
void *p1 = nullptr;
void *p2 = nullptr;
};
wrap1_t w2(two_ptrs{});
std::cout << "Is w2 using static storage? " << has_static_storage(w2) << '\n';
constexpr auto custom_config2 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>,
.static_align = tanuki::holder_align<void *, any_iface>};
using wrap2_t = tanuki::wrap<any_iface, custom_config2>;
wrap2_t w3(nullptr);
std::cout << "Is w3 using static storage? " << has_static_storage(w3) << '\n';
std::cout << "sizeof(wrap1_t) is " << sizeof(wrap1_t) << ", sizeof(wrap2_t) is " << sizeof(wrap2_t) << '\n';
}