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!