Visual C++ 优化选项 - 如何提高代码输出?

Visual C++ optimization options - how to improve the code output?

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

相当简单的 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;
}

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

main: # @main
  mov eax, 10
  ret

另一方面,Visual C++ 输出是多页的混乱。 我是不是遗漏了什么或者 VS 真的有这么糟糕吗?

编译器浏览器link: https://godbolt.org/g/GJYHjE

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

检查生成的程序集,Clang 在优化此代码方面做得非常出色。具体来说,与 VS 相比,Clang 能够执行非常有效的 Constant propagationFunction Inlining(因此,Dead代码消除),以及New/delete优化.

恒定传播

示例中,向量被静态初始化:

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 都无法提前预先计算向量累加结果。

New/Delete优化

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

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

现在,当检查 VS 生成的 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++ 团队的 blog post(2017 年 5 月 10 日)确认确实未实现此优化。在页面中搜索 N3664 显示“Avoiding/fusing 分配”的状态为 N/A,链接的评论说:

[E] Avoiding/fusing allocations is permitted but not required. For the time being, we’ve chosen not to implement this.

结合 new/delete 优化和常量传播,很容易看出这两个优化的影响 Compiler Explorer Clang 与 -stdlib=libc++ 的 3 向比较,Clang 与 -stdlib=libstdc++,和海湾合作委员会。

STL 实现

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

为了隔离vector STL实现的影响,一个有趣的实验是使用Clang进行编译,结合VS header文件。事实上,使用 Clang 5.0.0Visual Studio 2015 headers,会导致以下代码生成 - 显然, 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    , %rsp
.Lcfi5:
    .seh_stackalloc 72
    leaq    64(%rsp), %rbp
.Lcfi6:
    .seh_setframe 5, 64
.Lcfi7:
    .seh_endprologue
    movq    $-2, (%rbp)
    movl    , %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:                                 # %"??1?$vector@HV?$allocator@H@std@@@std@@QEAA@XZ.exit"
    addl    %ebx, %esi
    leal    1(%rdi,%rsi), %eax
    addq    , %rsp
    popq    %rbx
    popq    %rdi
    popq    %rsi
    popq    %rbp
    retq
    .seh_handlerdata
    .long   ($cppxdata$main)@IMGREL
    .text

更新 - Visual Studio 2017

在 Visual Studio 2017 年,<vector> 进行了重大改革,正如 Visual C++ 团队在此 blog post 上宣布的那样。具体来说,它提到了以下优化:

  • Eliminated unnecessary EH logic. For example, vector’s copy assignment operator had an unnecessary try-catch block. It just has to provide the basic guarantee, which we can achieve through proper action sequencing.

  • Improved performance by avoiding unnecessary rotate() calls. For example, emplace(where, val) was calling emplace_back() followed by rotate(). Now, vector calls rotate() in only one scenario (range insertion with input-only iterators, as previously described).

  • Improved performance with stateful allocators. For example, move construction with non-equal allocators now attempts to activate our memmove() optimization. (Previously, we used make_move_iterator(), which had the side effect of inhibiting the memmove() optimization.) Note that a further improvement is coming in VS 2017 Update 1, where move assignment will attempt to reuse the buffer in the non-POCMA non-equal case.

好奇,我回去测试了一下。 Visual Studio 2017年构建示例时,结果还是多页的汇编列表,函数调用很多,所以即使代码生成有所改进,也很难注意到。

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

main:                                   # @main
.Lcfi0:
.seh_proc main
# BB#0:
    subq    , %rsp
.Lcfi1:
    .seh_stackalloc 40
.Lcfi2:
    .seh_endprologue
    movl    , %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    , %eax
    addq    , %rsp
    retq
    .seh_handlerdata
    .text

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

我会说这太棒了!

函数内联

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

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

memmove 是库函数,因此不能内联。然而,clang 有一个聪明的方法来解决这个问题——它将对 memmove 的调用替换为对 __builtin_memmove 的调用。 __builtin_memmove 是一个 builtin/intrinsic 函数,它具有与 memmove 相同的功能,但与普通函数调用相反,编译器会为其生成代码并将其嵌入到调用函数中。因此,代码可以在调用函数内进一步优化,并最终作为死代码删除。

总结

总而言之,在这个例子中,Clang 明显优于 VS,这要归功于高质量的优化和更高效的向量 STL 实现。当为 Visual C++ 和 clang(Visual Studio 2017 headers)使用相同的 header 文件时,Clang 轻而易举地击败了 Visual C++。

在写这个答案的时候,我不由得想,如果没有Compiler Explorer我们怎么办?感谢 Matt Godbolt 提供这个神奇的工具!