【发布时间】:2017-11-20 00:29:39
【问题描述】:
std::variant 在调用 std::visit 时分派给不同的访问者方法的方式在变体替代品是完全不同的类型时是非常合理的。本质上,特定于访问者的vtable 是在编译时构建的,经过一些错误检查1,通过基于当前index() 的表索引来查找适当的访问者函数,该表解析为间接在大多数平台上跳跃。
但是,如果备选方案共享一个公共基类,则调用(非虚拟)成员函数或使用访问者访问基类上的状态在概念上要简单得多:您总是调用 相同 方法,通常使用 same 指针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);
}
在 x86 上为最新版本的 gcc 和 clang 生成的代码类似3(显示为 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。
与联合
缓慢并非与生俱来:这是一个非常简单的“可区分联合”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 的结果函数也是still very efficient - 只是添加了一个小检查,但调用基类的行为是相同的。
在这种调用公共基类函数的情况下,我可以对variant 的高效使用做些什么,或者零成本抽象的承诺在这里没有实现?
我愿意接受不同的调用模式、编译器选项等。
1 特别是检查变体是否是valueless_by_exception,因为之前的分配失败了。
2 指向基类的指针总是与所有替代方案的最派生指针具有相同的关系,例如,当涉及多重继承时。
3 好吧,gcc 有点糟糕,因为它似乎在调用 visit 之前以及在每个自动生成的方法中都冗余地执行了“无价值”检查通过vtable。 clang 只做前期。请记住,当我说“gcc”时,我的真正意思是“gcc with libstdc++”,而“clang”真正的意思是“clang with libc++”。一些差异,例如生成的访问者函数中的冗余 index() 检查可能是由于库差异而不是编译器优化差异。
4 如果valueless 状态有问题,还可以考虑类似strict_variant 这样的东西,它永远不会有空状态,但如果移动构造函数无法抛出,仍然使用本地存储。
【问题讨论】:
-
@Yakk - COMDAT 折叠也被脑海中掠过:但它当然不能为跳转生成更好的代码或解决大多数问题。但是,它至少可以将两个生成的访问者函数合并为一个,然后最终用相同的(折叠的)值填充 vtable,这至少可以解决分支预测问题(因为目标现在总是相同的)。当然,LTO 可以发挥神奇的作用。不过我有疑问:似乎如果编译器可以优化它,它可能已经在没有 LTO 的情况下这样做了(所需的一切都在编译单元中可见)。
-
@PeterCordes - 对,它可能会有所帮助 - 特别是对于第二次冗余检查 inside 生成了 gcc 所做的
vtable方法(因为这使得折叠变得不可能)。要更多地了解可能发生的情况,有必要看看如果您有一个variant对象,其替代方法是静态已知的。例如,参见here,其中创建了Foobar(Bar{})并调用了基本方法。clang能够将其完全优化为一个简单的mov eax, 2,因此完全消除了所有调度和任何检查。 -
@BeeOnRope 所以第一步是教编译器到optimize this,其中唯一定义的行为是什么都不做。如果他们不能将其优化到零,那么他们对变体 vtable 的东西就没有希望了。请注意,编译器可以处理没有数组位和内联函数指针。
-
@NicolBolas - 事实上,在这种情况下,我大部分时间都使用
virtual(它解决了这个“基类”问题:不要使用virtual)。在这种情况下,它是不合适的,因为虚拟多态性既使底层对象的大小增加了三倍,又阻止了容器中直接的值语义存储之类的事情(即,您在任何地方都使用指针并进行大量动态分配,或者使用每种类型的容器进行存储和一个单独的指针容器,这是您的逻辑集合等)。功能上没有问题,但这是性能问题:) -
@BeeOnRope:我发现这个问题是因为我有同样的问题:我想要一个具有公共基类的对象容器......但是这些对象很小,所以我真的希望它们直接存储在容器,而不是堆上一百万个小块。似乎在库+编译器支持方面存在差距,无法有效处理此类事情。
标签: c++ performance x86 c++17 variant