Categories
Blog

Faster visit of std::variant

Whilst experimenting with std::visit on Godbolt, I was surprised how noisy the generated assembly was. For all the main compilers there seemed to be a lot of calls and vtables being generated, and it all seemed unecessary given that so much could be determined at compile time. I tried using std::variant.index(), std::get() and std::get_if() – initially in a big switch statement – and found that the compiler could be coerced into generating far more minimal code:

This post is about the first part of the challenge: getting my hands dirty and rolling it into a variadic template. My starting point on Godbolt was https://godbolt.org/z/fv8z9Yr6Y. A variant of types Foo and Bar is stored in an array. If the if (argc == 1) condition is left out then Clang can reduce this to a move operation, but other compilers add a lot of noise, and when the conditional is introduced, all compilers don’t seem to be able to reduce the visit gracefully:

struct Foo {};
struct Bar {};
using FooBar = std::variant<Foo, Bar>;

struct FooBarVisitor {
  int acc = 1;
  void operator()(const Foo&) { acc += 1; }
  void operator()(const Bar&) { acc *= 2; }
};

int main(int argc, char*[]) {
  std::array<FooBar, 4> arr = {Foo(), Bar(), Bar(), Foo()};
  if (argc == 1) {
    arr[1] = Foo();
  }

  FooBarVisitor sv;
  for (const auto& a : arr) {
     zob::visit(a, sv);
     //std::visit(sv, a);
  }

  return sv.acc;
}

The output assembly from this is shown above. The original std::visit assembly was far more noisy:

Getting the variadics to match the arguments in std::variant was the main challenge, so my visit method uses a template pattern match to get the args:

template <
  typename Visitor,
  typename... VariantArgs
>
constexpr void visit(
  const std::variant<VariantArgs...>& inst,
  Visitor& visitor
) {
  // Generate a caller typed on variant arguments
  auto caller = detail::ConstVariantCallVisitor<
      Visitor,
      VariantArgs...
    >();

  ...

The ‘caller’ is a struct that wraps up the visit calls for all possible types. It is necessary to have this concrete type because I will be passing more variadic types in the next part of the code, and you can’t have more than one parameter pack in a template definition:

    ...    

    // Instigate the first variadic call using the full list
    // of variant args to process recursively
    caller.template call_first_match<
      VariantArgs...
    >(
      inst,
      visitor
    );
  };}

The args are unrolled by the variadic method call_first_match. If in future, this were extended to that the visitor could take multiple args, then there may need to be another struct to add further indirection to the parameter pack.

I’ve put the initial draft of this visitor onto GitHub https://github.com/alexallmont/variant_visit, because if it’s useful I need to add a lot more. Initially I need to swap the arguments to zob::visit so they match std::visit. Also I need to handle non-const variants, and allows the visitor operator to accept multiple arguments.

Leave a Reply

Your email address will not be published. Required fields are marked *