可以优化 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&lt;...&gt; 花费的时间最多。

问: 由于每次访问调用时生成的 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&lt;...&gt; 中生成的 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]]

这是为&lt;double, double&gt; 访客生成的:

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&lt;...&gt;,而不是为您提供深度嵌套的 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::visit 和 std::variant 用法

std::visit 无法推断 std::variant 的类型

std::visit 和 MSVC 调试器的堆栈损坏“重载”结构

C++17 std::variant 比动态多态性慢?