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';
}