有没有希望在std :: variant上有效地调用公共基类方法?

Bee*_*ope 13 c++ performance x86 variant c++17

的方式std::variant分派给不同的访问者方法时std::visit被称为是非常合理的,当变异的替代品是完全不同的类型.本质上,特定vtable于访问者的特定是在编译时构建的,并且在一些错误检查1之后,通过基于当前索引表来查看适当的访问者函数,该当前index()解析为大多数平台上的间接跳转.

但是,如果备选方案共享一个公共基类,则调用(非虚拟)成员函数或使用访问者访问基类上的状态在概念上要简单得多:您总是调用相同的方法并且通常使用相同的指针2来基类.

尽管如此,实施结果同样缓慢.例如:

#include <variant>

struct Base {
  int m_base;
  int getBaseMember() { return m_base; }
};

struct Foo : public Base {
  int m_foo;
};

struct Bar : public Base {
  int m_bar;
};

using Foobar = std::variant<Foo,Bar>;

int getBaseMemVariant(Foobar& v) {
  return std::visit([](auto&& e){ return e.getBaseMember(); }, v);
}
Run Code Online (Sandbox Code Playgroud)

为最新版本的在x86生成的代码gccclang类似3(示出铛):

getBaseMemVariant(std::__1::variant<Foo, Bar>&): # @getBaseMemVariant(std::__1::variant<Foo, Bar>&)
        sub     rsp, 24
        mov     rax, rdi
        mov     ecx, dword ptr [rax + 8]
        mov     edx, 4294967295
        cmp     rcx, rdx
        je      .LBB0_2
        lea     rdx, [rsp + 8]
        mov     qword ptr [rsp + 16], rdx
        lea     rdi, [rsp + 16]
        mov     rsi, rax
        call    qword ptr [8*rcx + decltype(auto) std::__1::__variant_detail::__visitation::__base::__visit_alt<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>, std::__1::__variant_detail::__impl<Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__impl<Foo, Bar>&)::__fmatrix]
        add     rsp, 24
        ret
.LBB0_2:
        mov     edi, 8
        call    __cxa_allocate_exception
        mov     qword ptr [rax], vtable for std::bad_variant_access+16
        mov     esi, typeinfo for std::bad_variant_access
        mov     edx, std::exception::~exception()
        mov     rdi, rax
        call    __cxa_throw
decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&): # @"decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)"
        mov     eax, dword ptr [rsi]
        ret
decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&): # @"decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)"
        mov     eax, dword ptr [rsi]
        ret
decltype(auto) std::__1::__variant_detail::__visitation::__base::__visit_alt<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>, std::__1::__variant_detail::__impl<Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__impl<Foo, Bar>&)::__fmatrix:
        .quad   decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)
        .quad   decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<getBaseMemVariant(std::__1::variant<Foo, Bar>&)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, Foo, Bar>&)
Run Code Online (Sandbox Code Playgroud)

call qword ptr [8*rcx + ...是对vtable指向的函数的实际间接调用(vtable本身出现在列表的底部).之前的代码首先检查"空"状态,然后设置visit调用(我不确定rdi它的怪异是什么,我猜它是设置一个指向访问者的指针作为第一个参数或其他东西).

由vtable指向并由其执行的实际方法call非常简单,单个mov读取成员.至关重要的是,两者都是不同的:

    mov     eax, dword ptr [rsi]
    ret
Run Code Online (Sandbox Code Playgroud)

所以我们有一个很大的混乱.为了执行该单一,mov我们有十几个设置指令,更重要的是一个间接分支:如果针对一系列Foobar variant具有不同包含的替代方案的对象,将会非常错误地进行错误预测.最后,间接调用似乎是进一步优化的一个不可逾越的障碍:这里将看一个没有任何周围环境的简单调用,但在实际使用中,这可能会被优化为一个更大的函数,具有进一步优化的重要机会 - 但我认为是间接的电话会阻止它.

你可以在godbolt上自己玩代码.

与联盟对战

缓慢并不是固有的:这是一个非常简单的"区分联合" struct,它将两个类组合在一起,unionisFoo跟踪一个跟踪包含哪个类的鉴别器:

struct FoobarUnion {
  bool isFoo;
  union {
    Foo foo;
    Bar bar;
  };
  Base *asBase() {return isFoo ? (Base *)&foo : &bar; };
};

int getBaseMemUnion(FoobarUnion& v) {
  return v.asBase()->getBaseMember();
}
Run Code Online (Sandbox Code Playgroud)

相应的getBaseMemUnion函数编译为gcc和clang上的单个mov指令:

getBaseMemUnion(FoobarUnion&):      # @getBaseMemUnion(FoobarUnion&)
        mov     eax, dword ptr [rdi + 4]
        ret
Run Code Online (Sandbox Code Playgroud)

当然,受歧视的联合不必检查"无值"错误条件,但这不是variant缓慢的主要原因,并且在任何情况下这样的条件是不可能的,Foo并且Bar因为它们的构造函数都没有抛出4.即使你想支持这样的状态,用所产生的作用union仍然非常有效的 -只有一小检查添加,但调用基类的行为是一样的.

variant在这种调用公共基类函数的情况下,我有什么办法可以做到这种高效的使用,还是零成本抽象的承诺在这里没有实现?

我对不同的调用模式,编译器选项开放.


1特别是,检查变量是否是valueless_by_exception由于先前的失败分配造成的.

2指向基类的指针并不总是与所有替代的最派生指针具有相同的关系,例如,当涉及多重继承时.

3好吧gcc有点糟糕,因为它似乎在调用之前visit和在每个自动生成的方法中冗余地执行"无值"检查vtable.clang只是提前做.请记住,当我说"gcc"时,我的意思是"gcc with libstdc ++",而"clang"实际上意味着"与libc ++铿锵".一些差异,如index()生成的访问者函数中的冗余检查可能是由于库差异而不是编译器优化差异.

4如果valueless状态有问题,人们也可以考虑类似于strict_variant从不具有空状态但仍然使用本地存储的情况,如果移动构造函数不能抛出.

Bar*_*rry 9

对于它的价值,一个完全手动的访问与一个switch相当不错:

// use a code generator to write out all of these
template <typename F, typename V>
auto custom_visit(F f, V&& v, std::integral_constant<size_t, 2> )
{
    switch (v.index()) {
    case 0: return f(std::get<0>(std::forward<V>(v)));
    case 1: return f(std::get<1>(std::forward<V>(v)));
#ifdef VALUELESS
    case std::variant_npos: {
        []() [[gnu::cold, gnu::noinline]] {
            throw std::bad_variant_access();
        }();
    }
#endif
    }
    __builtin_unreachable();

}

template <typename F, typename V>
auto custom_visit(F f, V&& v) {
    return custom_visit(f, std::forward<V>(v),
        std::variant_size<std::decay_t<V>>{});
}
Run Code Online (Sandbox Code Playgroud)

您使用的是:

int getBaseMemVariant2(Foobar& v) {
  return custom_visit([](Base& b){ return &b; }, v)->getBaseMember();
}
Run Code Online (Sandbox Code Playgroud)

随着VALUELESS,这发出:

getBaseMemVariant2(std::variant<Foo, Bar>&):
    movzx   eax, BYTE PTR [rdi+8]
    cmp     al, -1
    je      .L27
    cmp     al, 1
    ja      .L28
    mov     eax, DWORD PTR [rdi]
    ret
.L27:
    sub     rsp, 8
    call    auto custom_visit<getBaseMemVariant2(std::variant<Foo, Bar>&)::{lambda(Base&)#1}, std::variant<Foo, Bar>&>(getBaseMemVariant2(std::variant<Foo, Bar>&)::{lambda(Base&)#1}, std::variant<Foo, Bar>&, std::integral_constant<unsigned long, 2ul>)::{lambda()#1}::operator()() const [clone .isra.1]
Run Code Online (Sandbox Code Playgroud)

这很不错.没有VALUELESS,这会发出:

getBaseMemVariant2(std::variant<Foo, Bar>&):
    mov     eax, DWORD PTR [rdi]
    ret
Run Code Online (Sandbox Code Playgroud)

如预期的.

我真的不知道从中得出什么结论,如果有的话.显然,有希望吗?