可以优化 std::visit 吗?
Posted
技术标签:
【中文标题】可以优化 std::visit 吗?【英文标题】:optimizing of std::visit possible? 【发布时间】:2018-03-07 07:46:01 【问题描述】:在使用std::visit
/ std::variant
时,我在分析器输出中看到std::__detail::__variant::__gen_vtable_impl
函数花费的时间最多。
我做了这样的测试:
// 3 class families, all like this
class ElementDerivedN: public ElementBase
...
std::variant<ElementDerived1*, ElementDerived2*,... > GetVariant() override return this;
std::vector<Element*> elements;
std::vector<Visitor*> visitors;
std::vector<Third*> thirds;
// prepare a hack to get dynamic function object:
template<class... Ts> struct funcs : Ts... using Ts::operator()...; ;
template<class... Ts> funcs(Ts...) -> funcs<Ts...>;
// demo functions:
struct Actions template < typename R, typename S, typename T> void operator()( R*, S*, T* ) ;
struct SpecialActionForElement1 template < typename S, typename T > void operator()( Element1*, S*, T* ) ;
for ( auto el: elements )
for ( auto vis: visitors )
for ( auto th: thirds )
std::visit( funcs Actions(), SpecialActionForElement1Derived1(), el->GetVariant(), vis->GetVariant(), th->GetVariant() );
如前所述,std::__detail::__variant::__gen_vtable_impl<...>
花费的时间最多。
问:
由于每次访问调用时生成的 n 维函数数组从调用到调用都相同,因此最好将其保留在 std::visit
的调用之间。这可能吗?
也许我走错了路,如果是,请告诉我!
编辑: 使用标准 Fedora 安装中的编译器 gcc7.3。 std-lib 在 g++ 中被用作标准(这是什么)
构建选项:
g++ --std=c++17 -fno-rtti main.cpp -O3 -g -o go
【问题讨论】:
你在 std::variant 中使用多态对象吗?也许您可以通过完全避免 std::variant 来简化数据。变体实际上是昂贵的东西,也不是最佳的。如果你使用简单的继承,你给编译器一个机会来优化你的代码,通过去虚拟化等。 @VictorGubin:是的,这是众所周知的。原因是标准的访问者模式不能用模板(多分派)实现,因为虚拟模板在 C++ 中是不可能的。因此,使用变体/访问是解决方法。正如所见,当 N=2 时,该解决方案需要多花大约 60% 的时间来调度。这对我来说是可以接受的,但也许可以优化(该问题的原因)。与经典的访问者模式实现一样,我的具体 sw 的设计更简单,更容易阅读变体/访问。示例中的简化并没有反映我真正的软件需求! 从我的(不仅仅是我的,比如Thomas Kyte)的角度来看,如果你考虑优化,你应该从设计开始,然后再转向底层。我不认为低水平 @VictorGubin:您可以向 XY 提出任何问题。但是这个问题询问了具体功能的潜在优化选项。发现的行为让 me 假设一个对象将生成一个临时对象,该对象可以在调用之间保留。那么这与我的申请有什么关系呢?我进行了测量,发现正是在这一点上消耗了时间。所以我认为要求改进是一个重点。也许这是其他人也可以用来使他们的代码更快的想法。而且这个问题不是“我的程序很慢,请帮忙”。 @PaulR:每个变体包含 3 种类型,应该给出一个 3x3 矩阵。这也是我想知道的! 【参考方案1】:我刚刚看了一个更简单的example。该表是在编译时生成的。时间可能花在std::__detail::__variant::__gen_vtable_impl<...>
中生成的 lambdas 上。出于某种原因,这些基本上调用访问者的 lambdas 不会忽略对变体实际类型的检查。
此函数允许编译器为四个不同版本的访问 lambda 内联到在 std::visit
中创建的 lambdas 中创建代码,并将指向这些 lambdas 的指针存储在静态数组中:
double test(std::variant<int, double> v1, std::variant<int, double> v2)
return std::visit([](auto a, auto b) -> double
return a + b;
, v1, v2);
这是在测试中创建的:
(...) ; load variant tags and check for bad variant
lea rax, [rcx+rax*2] ; compute index in array
mov rdx, rsi
mov rsi, rdi
lea rdi, [rsp+15]
; index into vtable with rax
call [QWORD PTR std::__detail::__variant::(... bla lambda bla ...)::S_vtable[0+rax*8]]
这是为<double, double>
访客生成的:
std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<double (*)(test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&, std::variant<int, double>&, test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&)>, std::tuple<test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&, test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&>, std::integer_sequence<unsigned long, 1ul, 1ul> >::__visit_invoke(test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1, test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&, test(std::variant<int, double>, std::variant<int, double>)::lambda(auto:1, auto:2)#1&&):
; whew, that is a long name :-)
; redundant checks are performed whether we are accessing variants of the correct type:
cmp BYTE PTR [rdx+8], 1
jne .L15
cmp BYTE PTR [rsi+8], 1
jne .L15
; the actual computation:
movsd xmm0, QWORD PTR [rsi]
addsd xmm0, QWORD PTR [rdx]
ret
如果分析器将这些类型检查的时间和内联访问者的时间都归因于 std::__detail::__variant::__gen_vtable_impl<...>
,而不是为您提供深度嵌套的 lambda 的完整 800 多个字符名称,我不会感到惊讶。
我在这里看到的唯一通用优化潜力是省略对 lambda 中的坏变体的检查。由于 lambda 仅通过函数指针调用匹配变量,因此编译器将很难静态地发现检查是多余的。
我查看了same example compiled with clang and libc++。在 libc++ 中,多余的类型检查被消除了,所以 libstdc++ 还不是很理想。
decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul, 1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<test(std::__1::variant<int, double>, std::__1::variant<int, double>)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<test(std::__1::variant<int, double>, std::__1::variant<int, double>)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&): # @"decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul, 1ul>::__dispatch<std::__1::__variant_detail::__visitation::__variant::__value_visitor<test(std::__1::variant<int, double>, std::__1::variant<int, double>)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&>(std::__1::__variant_detail::__visitation::__variant::__value_visitor<test(std::__1::variant<int, double>, std::__1::variant<int, double>)::$_0>&&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&, std::__1::__variant_detail::__base<(std::__1::__variant_detail::_Trait)0, int, double>&)"
; no redundant check here
movsd xmm0, qword ptr [rsi] # xmm0 = mem[0],zero
addsd xmm0, qword ptr [rdx]
ret
也许您可以检查您的生产软件中实际生成的代码,以防万一它与我在示例中找到的不相似。
【讨论】:
感谢您的工作!正如我在我的程序集中发现的那样,没有静态生成的表,而是生成器函数。所以我会把我的示例代码分解到编译器生成静态解决方案的地步。也许我找到了阻碍优化的关键。 这可能与 constexpr 计算的大小或时间限制有关,请参阅answer。也许你可以调整-fconstexpr-depth
选项。以上是关于可以优化 std::visit 吗?的主要内容,如果未能解决你的问题,请参考以下文章
使用偏函数应用程序或 curry 与重载和 std::visit 结合使用时理解错误
std::visit 无法推断 std::variant 的类型