当变量备选方案是完全不同的类型时,调用std::visit
时std::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);
}
最新版本的gcc
和clang
在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
,我们有十几条设置指令,更重要的是还有一个间接分支:如果将一系列包含不同备选方案的Foobar
variant
对象作为目标,则会严重预测失误。最后,间接调用似乎是进一步优化的一个不可逾越的障碍:这里将观察一个没有任何上下文的简单调用,但在实际使用中,它可能会被优化为一个更大的函数,有很大的机会进行进一步优化-但我认为间接调用会阻止它
你可以在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
缓慢的主要原因,而且在任何情况下,Foo
和Bar
都不可能出现这种条件,因为它们的构造函数都没有抛出4。即使您想支持这样的状态,union
的结果函数仍然非常有效——只添加了一个小的检查,但调用基类的行为是相同的。
在这种调用公共基类函数的情况下,我能对variant
的有效使用做些什么吗?还是零成本抽象的承诺在这里没有实现?
我对不同的调用模式、编译器选项等持开放态度。
1特别是,检查变量是否为valueless_by_exception
,这是由于先前的赋值失败所致。
2指向基类的指针并不总是对于所有备选方案,与最派生的指针具有相同的关系,例如,当涉及多重继承时。
3井gcc
有点糟糕,因为它似乎在调用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