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:

blog/stdvisitvsany.png

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!