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

Ale*_*der 8 c++ visual-c++ c++11 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;
}
Run Code Online (Sandbox Code Playgroud)

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

main: # @main
  mov eax, 10
  ret
Run Code Online (Sandbox Code Playgroud)

另一方面,Visual C++输出是一个多页面的混乱.我在这里遗漏了什么,还是VS真的很糟糕?

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

val*_*ano 9

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

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

不断传播

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

std::vector<int> v = {1, 2, 3, 4};
Run Code Online (Sandbox Code Playgroud)

通常,编译器会将常量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
Run Code Online (Sandbox Code Playgroud)

然而,Clang非常聪明地意识到总和可以提前计算.我最好的猜测是,它将常量从内存加载到常量mov操作到寄存器(传播常量),然后将它们组合成10的结果.这具有破坏依赖性的有用副作用,并且因为地址不再加载,编译器可以自由删除其他所有内容作为死代码.

Clang似乎在这方面是独一无二的 - VS或GCC都无法提前预先计算向量累积结果.

新建/删除优化

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

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

现在,在检查VSmain生成的代码时,我们可以找到两个函数调用(其余的向量构造和内联迭代代码):main

call std::vector<int,std::allocator<int> >::_Range_construct_or_tidy<int const * __ptr64>
Run Code Online (Sandbox Code Playgroud)

call void __cdecl operator delete(void * __ptr64)
Run Code Online (Sandbox Code Playgroud)

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

这篇来自Visual C++团队的博客文章(2017年5月10日)证实,确实没有实现这种优化.在页面中搜索N3664"避免/融合分配"的节目处于状态N/A,并且链接的评论说:

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

结合新的/删除优化和常量传播,很容易看出这两个优化在这个Compiler Explorer中对Clang with -stdlib=libc++,Clang with -stdlib=libstdc++和GCC 进行3对比的影响.

STL实施

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

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

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
Run Code Online (Sandbox Code Playgroud)

更新 - Visual Studio 2017

在Visual Studio 2017中,<vector>已经看到了一项重大改革,正如Visual C++团队在此博客文章中所宣布的那样.具体来说,它提到了以下优化:

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

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

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

注意movl $10, %eax指令 - 也就是说,使用VS 2017 <vector>,clang能够折叠所有内容,预先计算10的结果,并且只保留对new和delete的调用.

我会说这太棒了!

功能内联

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

在检查生成的VS的程序集,并比较内联之前和之后的代码(Compiler Explorer)时,我们可以看到大多数向量函数确实内联,除了分配和释放函数.特别是,有一些调用memmove,这是内联一些更高级别功能的结果,例如_Uninitialized_copy_al_unchecked.

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

摘要

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

在写这个答案时,我忍不住想,如果没有编译器资源管理器,我们会做什么?感谢Matt Godbolt这个神奇的工具!