是否有希望在std::变体上高效地调用一个公共基类方法

Is there any hope to call a common base class method on a std::variant efficiently?

本文关键字:一个 基类 类方法 std 有希望 是否 高效 调用      更新时间:2023-10-16

当变量备选方案是完全不同的类型时,调用std::visitstd::variant调度到不同访问者方法的方式是非常合理的。从本质上讲,特定于访问者的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);
}

最新版本的gccclang在x86上生成的代码类似于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>&)

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

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

mov     eax, dword ptr [rsi]
ret

所以我们一团糟。为了执行单个mov,我们有十几条设置指令,更重要的是还有一个间接分支:如果将一系列包含不同备选方案的Foobarvariant对象作为目标,则会严重预测失误。最后,间接调用似乎是进一步优化的一个不可逾越的障碍:这里将观察一个没有任何上下文的简单调用,但在实际使用中,它可能会被优化为一个更大的函数,有很大的机会进行进一步优化-但我认为间接调用会阻止它

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

与联盟

缓慢并不是固有的:这里有一个非常简单的"有区别的并集"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函数在gcc和clang:上编译为单个mov指令

getBaseMemUnion(FoobarUnion&):      # @getBaseMemUnion(FoobarUnion&)
mov     eax, dword ptr [rdi + 4]
ret

诚然,被判别的并集不必检查"无值"错误条件,但这不是variant缓慢的主要原因,而且在任何情况下,FooBar都不可能出现这种条件,因为它们的构造函数都没有抛出4。即使您想支持这样的状态,union的结果函数仍然非常有效——只添加了一个小的检查,但调用基类的行为是相同的。

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

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


1特别是,检查变量是否为valueless_by_exception,这是由于先前的赋值失败所致。

2指向基类的指针并不总是对于所有备选方案,与最派生的指针具有相同的关系,例如,当涉及多重继承时。

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

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

值得一提的是,使用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

根据需要。

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

我没有足够的能力执行汇编级分析,但我已经明确地围绕std::variant编写了一个小包装器,用于处理所有备选方案都继承自公共基类的变体。

在内部,我本质上是获得一个指向变量存储内容的指针,然后将其用作基类的普通指针。因此,一旦创建了我的新变体,我希望实际的函数调用与基类指针上的常规虚拟函数调用具有大致相同的开销。

pv::polymorphic_value< Base, Foo, Bar > variant;
variant->getBaseMember();

图书馆在https://github.com/Krzmbrzl/polymorphic_variant

相关文章: