How does std::any compare to std::variant?
published at 08.03.2023 17:25 by Jens Weller
Save to Instapaper Pocket
Playing around with std::variant lead to me wondering how std::any would compare to it. Afterall its also a single value store.
In last weeks blog post, I've shared 3 code samples which came about when playing with std::variant, and measuring if there would be a better method in regards to performance. During this also std::any came to my mind. It could be used in a similar way. And without the requirement to name a list of types you'd like to store...
So I set out to write a similar code piece, measuring the cost of accessing various types stored in a vector of std::any instances. All examples are compiled with gcc 11. The initial code came from cppreference. Again my initial results were a bit misleading:

While this graph is quite impressive, its not only std::any slowing things down, when looking closer at the code:
template< class T, class F>
inline std::pair< const std::type_index, std::function<void(std::any const&)>>
to_any_visitor(F const &f)
{
return {
std::type_index(typeid(T)),
[g = f](std::any const &a)
{
if constexpr (std::is_void_v)
g();
else
g(std::any_cast(a));
}
};
}
static void AnyTest(benchmark::State& state) {
// Code before the loop is not measured
std::string str("long,int,views");
std::vector v;v.push_back(std::string_view(&str[4],3));
v.push_back({4});v.push_back({1.0f});v.push_back({1.0d});v.push_back(std::string("kljdfalsd"));
size_t x = 0;
int i = 0;
std::unordered_map< std::type_index, std::function<void(std::any const&)>>
any_visitor {
to_any_visitor< int>([&x](int a){ x +=a; }),
to_any_visitor< std::string_view>([&x](std::string_view a){ x += a.size(); }),
to_any_visitor< float>([&x](float a){ x +=a; }),
to_any_visitor< double>([&x](double a){ x +=a; }),
to_any_visitor< std::string>([&x](const std::string& a)
{ x += a.size(); })
};
for (auto _ : state) {
std::any& lv = v[i % v.size()];
if (const auto it = any_visitor.find(std::type_index(lv.type()));
it != any_visitor.cend()) {
it->second(lv);
}
i++;
benchmark::DoNotOptimize(i);
benchmark::DoNotOptimize(x);
}
}
BENCHMARK(AnyTest);
The lookup via find and then accessing the any is in this test an overhead. One can directly use the index operator[]. Then for small lookups an std::map can be faster, with only 5 types in our visitor this is the case:
static void AnyTest(benchmark::State& state) {
...
std::map< std::type_index, std::function<void(std::any const&)>>
...
for (auto _ : state) {
std::any& lv = v[i % size];
/*if (const auto it = any_visitor.find(std::type_index(lv.type()));
it != any_visitor.cend()) {
it->second(lv);
}//*/
//any_visitor[lv.type().hash_code()](lv);
any_visitor[type_index(lv.type())](lv);
...
}
}
BENCHMARK(AnyTest);
Then the code comes out to std::variant being 2.6-2.8 times faster. I've tried various other ways and unlike std::variant, there is no other way to access the value. So no second or third option to compare against. And as a C++ programmer the run-time only nature of std::any is limiting the available tools. I've tried to replace std::type_index with the size_t value of type().hash_code, but this did not make a difference. I could not figure out if a lambda with auto parameter and if constexpr would in any way work with std::any. One could then have the runtime visitor as a fall back, but for a list of types use a compile time method.
Compiling the code with Clang 15 makes the difference go to 4.3, but because std::variant in clangs implementation is even faster, the timing for std::any did also improve, but not by much.
So std::any is a slower alternative, as an std::variant can make use of various C++ language features, that an any does not have avaialble.
Which makes you wonder if there are any use cases for std::any? One use case I'm currently thinking about is storing unknown types for the user of a library. In my case I do have a list of types I'd like to offer conversions from std::string_view for in a variant. These will be mostly numeric types like int, float or double. There are types which I have a use for, which I'd like not to offer conversion for. One of them is QString. QString is 16 bit, while std::string is 8 bit. And supporting types like QString (or wxString, CString) is out of scope of the library. But I could offer a user defined conversion. Where the library then stores the result of a conversion callable in an std::any.
When ever you handle types you either don't know in the context or don't want to support for various reasons, std::any offers a value store for you. One could even think about creating an "open variant", where you include std::any in the types listed.
Update 9th March
There is now a faster solution for any then the above one, created by twitter user cppimprov: using an if to compare the typeid of the type held against an actual type in an if is slightly faster then the map approach. The code:
template< class T>
bool any_has_type(std::any const& a)
{
return std::type_index(typeid(T)) == std::type_index(a.type());
}
static void AnyTest2(benchmark::State& state)
{
...
for (auto _ : state) {
auto const& lv = v[i % size];
x += [] (std::any const& a)
{
if (any_has_type< int>(a))
return static_cast< size_t>(std::any_cast(a));
if (any_has_type< float>(a))
return static_cast< size_t>(std::any_cast(a));
if (any_has_type< double>(a))
return static_cast< size_t>(std::any_cast(a));
if (any_has_type< std::string>(a))
return std::any_cast(a).size();
if (any_has_type< std::string_view>(a))
return std::any_cast(a).size();
std::abort();
}(lv);
...
Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!