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
不幸的是,在这种情况下很难大大改进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.0和Visual 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.0和Visual 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这个神奇的工具!
| 归档时间: |
|
| 查看次数: |
1744 次 |
| 最近记录: |