Visual C ++优化选项 - 如何改进代码输出?

时间:2018-02-02 15:08:38

标签: c++ c++11 visual-c++ cl

是否有任何选项(/ O2除外)来改进Visual C ++代码输出?在这方面,MSDN文档非常糟糕。 请注意,我没有询问项目范围的设置(链接时优化等)。我只对这个特殊的例子感兴趣。

相当简单的C ++ 11代码如下所示:

#include <vector>
int main() {
    std::vector<int> v = {1, 2, 3, 4};
    int sum = 0;
    for(int i = 0; i < v.size(); i++) {
        sum += v[i];
    }
    return sum;
}

使用libc ++的Clang输出非常紧凑:

main: # @main
  mov eax, 10
  ret
另一方面,Visual C ++输出是一个多页面的混乱。 我在这里遗漏了什么,或者VS真的很糟糕吗?

编译器资源管理器链接: https://godbolt.org/g/GJYHjE

1 个答案:

答案 0 :(得分:9)

不幸的是,在这种情况下很难大大提高Visual C ++输出,即使使用更积极的优化标志也是如此。导致VS效率低下的因素有很多,包括缺乏某些编译器优化,以及Microsoft <vector>的实现结构。

检查生成的程序集,Clang在优化此代码方面做得非常出色。具体来说,与VS相比,Clang能够执行非常有效的常量传播功能内联(以及死代码消除),和新建/删除优化

常规传播

在该示例中,向量是静态初始化的:

std::vector<int> v = {1, 2, 3, 4};

通常,编译器会将常量1,2,3,4存储在数据存储器中,而在for循环中,将从存储1的低地址开始一次一个地加载一个值,并将每个值添加到总和中。

这是用于执行此操作的缩写VS代码:

movdqa   xmm0, XMMWORD PTR __xmm@00000004000000030000000200000001
...
movdqu   XMMWORD PTR $T1[rsp], xmm0 ; Store integers 1, 2, 3, 4 in memory
...
$LL4@main:
    add      ebx, DWORD PTR [rdx]   ; loop and sum the values
    lea      rdx, QWORD PTR [rdx+4]
    inc      r8d
    movsxd   rax, r8d
    cmp      rax, r9
    jb       SHORT $LL4@main
但是,Clang很聪明地意识到总和可以提前计算出来。我最好的猜测是,它将常量从内存加载到常量mov操作到寄存器(传播常量),然后将它们组合成10的结果。这具有破坏依赖性的有用副作用,并且因为地址不再加载,编译器可以自由删除其他所有内容作为死代码。

Clang在执行此操作时似乎是独一无二的 - VS或GCC都无法提前预先计算向量累积结果。

新建/删除优化

允许符合C ++ 14的编译器在某些条件下省略对new和delete的调用,特别是当分配调用的数量不是程序的可观察行为的一部分时({{ 3}}标准纸)。 这已经引起了很多关于SO的讨论:

-std=c++14 -stdlib=libc++调用的Clang确实执行了这种优化,并且消除了对new和delete的调用,这些调用确实带有副作用,但据说不会影响程序的可观察行为。使用-stdlib=libstdc++,Clang更严格并且保持对new和delete的调用 - 尽管通过查看程序集,很明显它们并不是真正需要的。

现在,在检查Optimization of raw new[]/delete[] vs std::vector生成的main代码时,我们可以找到两个函数调用(其余的向量构造和迭代代码内联到main):

call std::vector<int,std::allocator<int> >::_Range_construct_or_tidy<int const * __ptr64>

call void __cdecl operator delete(void * __ptr64)

第一个用于分配向量,第二个用于解除分配,实际上VS输出中的所有其他函数都被这些函数调用拉入。这暗示Visual C ++不会优化对分配函数的调用(对于C ++ 14一致性,我们应该添加/std:c++14标志,但结果是相同的。)

来自Visual C ++团队的这个VS(2017年5月10日)证实,确实没有实现此优化。在页面中搜索N3664表示&#34;避免/融合分配&#34;处于状态N / A,相关评论说:

  

[E]允许但不要求避免/融合分配。目前,我们选择不实施这一点。

结合新/删除优化和常量传播,很容易看到这两个优化对Clang与-stdlib=libc++的这种blog post三向比较的影响,Clang与{{ 1}}和GCC。

STL实施

VS有自己的STL实现,其结构与libc ++和stdlibc ++截然不同,这似乎对VS劣质代码生成有很大的贡献。虽然VS STL有一些非常有用的功能,例如检查迭代器和迭代器调试钩子(-stdlib=libstdc++),但它给人的印象是比stdlibc ++更重,效率更低。

为了隔离向量STL实现的影响,一个有趣的实验是使用Clang进行编译,并结合VS头文件。实际上,将 Clang 5.0.0 Visual Studio 2015 标头一起使用会导致以下代码生成 - 显然,STL实现会产生巨大影响!

_ITERATOR_DEBUG_LEVEL

更新 - Visual Studio 2017

在Visual Studio 2017中,main: # @main .Lfunc_begin0: .Lcfi0: .seh_proc main .seh_handler __CxxFrameHandler3, @unwind, @except # BB#0: # %.lr.ph pushq %rbp .Lcfi1: .seh_pushreg 5 pushq %rsi .Lcfi2: .seh_pushreg 6 pushq %rdi .Lcfi3: .seh_pushreg 7 pushq %rbx .Lcfi4: .seh_pushreg 3 subq $72, %rsp .Lcfi5: .seh_stackalloc 72 leaq 64(%rsp), %rbp .Lcfi6: .seh_setframe 5, 64 .Lcfi7: .seh_endprologue movq $-2, (%rbp) movl $16, %ecx callq "??2@YAPEAX_K@Z" movq %rax, -24(%rbp) leaq 16(%rax), %rcx movq %rcx, -8(%rbp) movups .L.ref.tmp(%rip), %xmm0 movups %xmm0, (%rax) movq %rcx, -16(%rbp) movl 4(%rax), %ebx movl 8(%rax), %esi movl 12(%rax), %edi .Ltmp0: leaq -24(%rbp), %rcx callq "?_Tidy@?$vector@HV?$allocator@H@std@@@std@@IEAAXXZ" .Ltmp1: # BB#1: # %"\01??1?$vector@HV?$allocator@H@std@@@std@@QEAA@XZ.exit" addl %ebx, %esi leal 1(%rdi,%rsi), %eax addq $72, %rsp popq %rbx popq %rdi popq %rsi popq %rbp retq .seh_handlerdata .long ($cppxdata$main)@IMGREL .text 已经进行了重大改革,正如Visual C ++团队在此Compiler Explorer上宣布的那样。具体来说,它提到了以下优化:

  
      
  • 消除了不必要的EH逻辑。例如,vector的复制赋值运算符有一个不必要的try-catch块。它只需提供基本保证,我们可以通过适当的动作排序来实现。

  •   
  • 通过避免不必要的rotate()调用来提高性能。例如,emplace(where,val)调用emplace_back(),然后调用rotate()。现在,vector仅在一个场景中调用rotate()(使用仅输入迭代器进行范围插入,如前所述)。

  •   
  • 使用有状态分配器提高性能。例如,使用不相等的分配器移动构造现在尝试激活我们的memmove()优化。 (之前,我们使用了make_move_iterator(),它具有禁止memmove()优化的副作用。)请注意,VS 2017 Update 1中将进一步改进,其中移动分配将尝试在非POCMA中重用缓冲区不平等的情况。

  •   
好奇,我回去测试了这个。在Visual Studio 2017中构建示例时,结果仍然是一个包含许多函数调用的多页面汇编列表,因此即使代码生成得到改进,也很难注意到。

但是,使用 clang 5.0.0 Visual Studio 2017 标头构建时,我们会得到以下程序集:

<vector>

请注意main: # @main .Lcfi0: .seh_proc main # BB#0: subq $40, %rsp .Lcfi1: .seh_stackalloc 40 .Lcfi2: .seh_endprologue movl $16, %ecx callq "??2@YAPEAX_K@Z" ; void * __ptr64 __cdecl operator new(unsigned __int64) movq %rax, %rcx callq "??3@YAXPEAX@Z" ; void __cdecl operator delete(void * __ptr64) movl $10, %eax addq $40, %rsp retq .seh_handlerdata .text 指令 - 也就是说,使用VS 2017&#39; movl $10, %eax,clang能够折叠所有内容,预先计算10的结果,并且只保留对new和delete的调用。

我说这太棒了!

功能内联

在这个例子中,函数内联可能是最重要的一个优化。通过将被调用函数的代码折叠到其调用站点中,编译器能够对合并代码执行进一步优化,此外,删除函数调用有助于减少调用开销和消除优化障碍。

当检查生成的VS的程序集,并比较内联(blog post)之前和之后的代码时,我们可以看到大多数向量函数确实内联,除了分配和释放函数。特别是,有<vector>的调用,这是内联某些更高级函数的结果,例如memmove

_Uninitialized_copy_al_unchecked是一个库函数,因此无法内联。但是,clang有一个聪明的方法 - 它通过调用memmove取代了对memmove的调用。 __builtin_memmove是内置/内部函数,它具有与__builtin_memmove相同的功能,但与普通函数调用相反,编译器为其生成代码并将其嵌入到调用函数中。因此,代码可以在调用函数内进一步优化,最终作为死代码删除。

<强>摘要

总之,在这个例子中,Clang显然优于VS,这要归功于高质量的优化和更高效的矢量STL实现。当为Visual C ++和clang(Visual Studio 2017标题)使用相同的头文件时,Clang击败了Visual C ++。

在写这个答案时,我不能不思考,没有Compiler Explorer我们会做什么?感谢Matt Godbolt这个神奇的工具!