c++ - 有没有希望有效地调用 std::variant 上的公共(public)基类方法?

标签 c++ performance x86 c++17 variant

方式std::variantstd::visit 时分派(dispatch)到不同的访问者方法当变体替代品是完全不同的类型时,被调用是非常合理的。本质上是特定于访问者的 vtable在编译时构建,经过一些错误检查1,通过基于当前 index() 索引表来查找适当的访问者函数。在大多数平台上解析为间接跳转之类的东西。

但是,如果替代方案共享一个公共(public)基类,则调用(非虚拟)成员函数或使用访问者访问基类上的状态在概念上要简单得多:您总是调用相同的方法,并且通常使用相同的指针 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);
}

最新版本的 gcc 在 x86 上生成的代码和 clang是similar3(clang显示):
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>&)
call qword ptr [8*rcx + ...是对 vtable 指向的函数的实际间接调用(vtable 本身出现在列表的底部)。之前的代码是先检查“为空”状态,然后设置visit调用(我不确定 rdi 的奇怪之处是什么,我猜它正在设置一个指向访问者的指针作为第一个参数或其他东西)。

由 vtable 指针指向并由 call 执行的实际方法很简单,单mov读取成员。至关重要的是,两者都是相同的:
    mov     eax, dword ptr [rsi]
    ret

所以我们有一个巨大的困惑。执行那个单 mov我们有十几个设置指令,更重要的是一个间接分支:如果针对一系列 Foobar variant具有不同包含选项的对象将错误预测非常糟糕。最后,间接调用似乎是进一步优化的一个不可逾越的障碍:这里将看一个没有任何周围上下文的简单调用,但在实际使用中,这可能会被优化为一个更大的函数,有很大的进一步优化机会 - 但我认为间接调用调用将阻止它。

您可以 play with the code yourself on godbolt .

对阵 union

缓慢不是固有的:这是一个非常简单的“歧视 union ”struct将这两个类组合在一个 union 中连同 isFoo跟踪包含哪些类的鉴别器:
struct FoobarUnion {
  bool isFoo;
  union {
    Foo foo;
    Bar bar;
  };
  Base *asBase() {return isFoo ? (Base *)&foo : &bar; };
};

int getBaseMemUnion(FoobarUnion& v) {
  return v.asBase()->getBaseMember();
}

对应的getBaseMemUnion函数编译为单个 mov关于 gcc 和 clang 的说明:
getBaseMemUnion(FoobarUnion&):      # @getBaseMemUnion(FoobarUnion&)
        mov     eax, dword ptr [rdi + 4]
        ret

当然,受歧视 union 不必检查“无值(value)”错误条件,但这不是 variant 的主要原因。 Foo 在任何情况下都不可能出现这种情况。和 Bar因为他们的构造函数都没有抛出4。即使你想支持这样的状态,结果函数用 unionstill very efficient - 只添加了一个小检查,但调用基类的行为是相同的。

我可以对 variant 的这种使用做些什么吗?在调用公共(public)基类函数的情况下是有效的,还是零成本抽象的 promise 在这里没有实现?

我对不同的调用模式、编译器选项等持开放态度。

1 特别是检查变体是否为valueless_by_exception由于先前的分配失败。

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

3 井gcc有点糟糕,因为它似乎在调用 visit 之前预先执行了“无值(value)”检查。以及 vtable 指向的每个自动生成的方法中. clang 只做前期工作。请记住,当我说“gcc”时,我的意思是“gcc with libstdc++”,而“clang”的真正意思是“clang with libc++”。一些差异,比如多余的 index() checkin 生成的访问者函数可能是由于库差异而不是编译器优化差异。

4 如果valueless状态有问题,你也可以考虑像 strict_variant 这样的东西它永远不会有一个空状态,但如果移动构造函数不能抛出,它仍然使用本地存储。

最佳答案

对于它的值(value),完全手动访问 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>>{});
}

你会使用像:
int getBaseMemVariant2(Foobar& v) {
  return custom_visit([](Base& b){ return &b; }, v)->getBaseMember();
}

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]

这很好。无 VALUELESS ,这会发出:
getBaseMemVariant2(std::variant<Foo, Bar>&):
    mov     eax, DWORD PTR [rdi]
    ret

如预期的。

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

关于c++ - 有没有希望有效地调用 std::variant 上的公共(public)基类方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47383563/

相关文章:

c++ - 提升.Python : Module inside module

java - Spark 性能中的 map 操作链

解码位图时出现 Android OutOfMemoryError

c - 获取指令指针指向的给定地址的指令

c# - 如何在 C# 和 C++ 代码之间共享常量?

c++ - 数组是否被称为隐式指针

algorithm - 表示和乘以稀疏 bool 矩阵的最快方法是什么?

windows - 当 processHandle = -1 时,这个 OpenProcessToken 做了什么?

performance - SIMD/SSE 新手 : simple image filtering

c++ - using 声明是否仅导入在 using 声明之上声明的重载?