C++编译器到底能帮我们把代码优化到什么程度?

横云断岭
博客专家认证
2012-01-02 01:48:02
太长了,看能贴过来不。。
http://blog.csdn.net/hengyunabc/article/details/7170865


一个简单的累加求和程序:

TYPE S=0;
for(int i = 0;i < SIZE; i++) {
S += a[i];
}


很多人都觉得这个程序写得不好,编译器不能生成很好的汇编代码。于是有了以下的几种“优化”:

#include <iostream>
using namespace std;

void main(int argc,char **argv)
{
#define TYPE int
#define SIZE 10000

TYPE* a=new TYPE[SIZE];
for(int i = 0; i<SIZE; ++i){
a[i] = i;
}
//求和,通常版本
TYPE S=0;
for(int i = 0;i < SIZE; i++) {
S += a[i];
}
cout<<S<<endl;

TYPE S2 = 0;
//版本1:认为中间产生的变量i是多余的,改为用移动指针
TYPE* end = a + SIZE;
for( ; a != end; ) {
S2 += *(a++);
}
cout<<S2<<endl;

//版本1中把a移到了数组的最后,现在移回原来的位置
a = end - SIZE;

//版本2:认为循环次数太多了,可以改为减少循环次数
TYPE S3 = 0;
for(int i = 0; i < SIZE; ){ //仅当SIZE为偶数时
S3 += a[i++];
S3 += a[i++];
}
cout<<S3<<endl;

//版本3:认为版本2中会使CPU不能乱序执行,降低了效率,应该改为汇编,把中间结果放到独立的寄存器中
//谢谢 menzi11 的文章,让我认识到程序中相关的数据会让CPU不能乱序执行。
//这里用伪汇编代替
TYPE S4 = 0;

register TYPE r1 = 0;
register TYPE r2 = 0;
for(int i = 0; i < SIZE; ){ //仅当SIZE为偶数时
r1 += a[i++];
r2 += a[i++];
}

cout<<r1 + r2<<endl;
}

上面的几种版本都合理,但是这些优化都是建立在编译器不能生成高效的汇编代码的假设上的。
下面来看下编译器生成的结果(vs2010,release):

for(int i = 0;i < SIZE; i++) {
S += a[i];
013B1040 mov ebx,dword ptr [eax+4] //把a[0],a[4],a[8]...累加到ebx中
013B1043 add ecx,dword ptr [eax-8] //把a[1],a[5],a[9]...累加到ebx中
013B1046 add edx,dword ptr [eax-4] //把a[2],a[6],a[10]...累加到ebx中
013B1049 add esi,dword ptr [eax] //把a[3],a[7],a[11]...累加到ebx中
013B104B add dword ptr [ebp-4],ebx
013B104E add eax,10h
013B1051 dec dword ptr [ebp-8]
013B1054 jne main+40h (13B1040h)
}
cout<<S<<endl;
013B1056 mov eax,dword ptr [ebp-4]
013B1059 add eax,esi
013B105B add eax,edx
013B105D mov edx,dword ptr [__imp_std::endl (13B204Ch)]
013B1063 add ecx,eax //上面的3条add指令把ebx,ecx,edx,edi都加到ecx中,即ecx是累加结果


可见编译器生成的代码是最好的代码,消灭了中间变量i,减少了循环次数,消灭了会造成CPU不能乱序执行的因素。

BTW:
有人可能会有疑问:要是size不是偶数,编译器能生成类似的高效汇编代码不?
当SIZE = 9999时:

//当SIZE = 9999时,编译器把中间结果放到三个寄存器中,perfect
for(int i = 0;i < SIZE; i++) {
S += a[i];
01341030 add ecx,dword ptr [eax-8]
01341033 add edx,dword ptr [eax-4]
01341036 add esi,dword ptr [eax]
01341038 add eax,0Ch
0134103B dec ebx
0134103C jne main+30h (1341030h)
}


当SIZE = 9997 时:

//当SIZE = 9997时,有点复杂,先把a[0]到a[9995]累加到ecx和edx中
//再把a[9996]入到edi中,最后把ecx,edi都加到edx中
//edx压栈,调用operator<< 函数
for(int i = 0;i < SIZE; i++) {
00D31024 xor eax,eax
S += a[i];
00D31026 add ecx,dword ptr [esi+eax*4]
00D31029 add edx,dword ptr [esi+eax*4+4]
00D3102D add eax,2
00D31030 cmp eax,270Ch
00D31035 jl main+26h (0D31026h)
for(int i = 0;i < SIZE; i++) {
00D31037 cmp eax,270Dh
00D3103C jge main+41h (0D31041h)
S += a[i];
00D3103E mov edi,dword ptr [esi+eax*4]
}
cout<<S<<endl;
00D31041 mov eax,dword ptr [__imp_std::endl (0D3204Ch)]
00D31046 add edx,ecx
00D31048 mov ecx,dword ptr [__imp_std::cout (0D32050h)]
00D3104E push eax
00D3104F add edx,edi
00D31051 push edx
00D31052 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0D32048h)]

上面的分析都是SIZE,即数组的大小是已知情况下,那个数组大小是未知情况下,编译器又会怎样?

TYPE mySum(TYPE* a, int size){
TYPE s = 0;
for(int i = 0; i < size; ++i){
s += a[i];
}
return s;
}


生成的汇编代码:

//先累加a[0] 到 a[size-2]
TYPE s = 0;
00ED100C xor esi,esi
for(int i = 0; i < size; ++i){
00ED100E xor eax,eax
00ED1010 cmp ebx,2
00ED1013 jl mySum+27h (0ED1027h)
00ED1015 dec ebx
s += a[i];
00ED1016 add ecx,dword ptr [edi+eax*4] //a[0],a[2],a[4]...加到ecx中
00ED1019 add edx,dword ptr [edi+eax*4+4] //a[1],a[3],a[5]...加到edx中
00ED101D add eax,2
00ED1020 cmp eax,ebx
00ED1022 jl mySum+16h (0ED1016h)
00ED1024 mov ebx,dword ptr [size]
for(int i = 0; i < size; ++i){
00ED1027 cmp eax,ebx //判断最后一个元素有没有加上
00ED1029 jge mySum+2Eh (0ED102Eh)
s += a[i];
00ED102B mov esi,dword ptr [edi+eax*4] //当size是奇数是会执行,偶数时不会执行
00ED102E add edx,ecx
}


总结:C++的编译器生成的汇编代码在绝大多数情况下都和人写出的最好的汇编代码相当。
关键的一点是编译器会不断升级,适应新的cpu指令,体系等,手写的汇编代码则通常悲剧了。
...全文
926 15 打赏 收藏 转发到动态 举报
写回复
用AI写文章
15 条回复
切换为时间正序
请发表友善的回复…
发表回复
  • 打赏
  • 举报
回复
看起来icc的优化确实很NB,我有一套Intel C++ Composer XE,不过一直没安装。:)
CandPointer 2012-05-02
  • 打赏
  • 举报
回复
VC 2010 的编译器,太垃圾了。

回在这里。

VS 2010 自带的情况,
C++/sse3
3.879s/1.395s

intel 编译器,各种不同的优化组合选项:
1.851s/1.591s ,
1.843s/1.811s
1.914s/1.914s

同样的代码, 普通的循环, VC2010 要 3.879 秒, intel 优化成 1.85秒。 仅仅稍微慢于手写的sse

http://topic.csdn.net/u/20120412/19/582af9df-7ce6-4456-8571-7968073f7984.html
iamnobody 2012-04-22
  • 打赏
  • 举报
回复
[Quote=引用 10 楼 的回复:]

反正俺是从来不考虑这种问题去优化
[/Quote]

不能空引用.
  • 打赏
  • 举报
回复
9楼的测试很好。就代码来看,sum3a_Intrinsics4应该不可能比SSSE3汇编版本快,估计是clock的计时误差造成的,可以用RDTSC测试一下。
  • 打赏
  • 举报
回复
反正俺是从来不考虑这种问题去优化
zyl910 2012-04-21
  • 打赏
  • 举报
回复
测试了一下Intrinsics函数,还测试了64位版的性能。

编译器:VC++ 2010 SP1
编译选项:/O2 /arch:SSE2

源代码——
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <tchar.h>

// 所有Intrinsics函数
#include <intrin.h>

const TCHAR *strSumResult = _T("Sum of %d to %d = %d (sum1), %d (sum2)");
const TCHAR *strTimeElapsed = _T("Time elapsed of %s is: %.3fs\n");
const TCHAR *strSumCPP = _T("sum1 (c++)");
const TCHAR *strSumSSE = _T("sum2 (asm ssse3)");

const int LoopCycle = 1000000;
const int T_SIZE = 10000;
__declspec(align(16)) int T_ARRAY[T_SIZE];
int S;

// http://topic.csdn.net/u/20120102/01/fc8d7aa4-bffc-4d9a-a34a-5056c6d27b54.html
// Author: hengyunabc
int Sum_CPP(int *a, int size)
{
int s = 0;
for(int i = 0; i < size; ++i) s += a[i];

return s;
}

#if defined(_WIN64)
int Sum_SSSE3(int *a, int size)
{
return 0; // 64位环境下不能使用32位汇编代码
}
#else
// http://topic.csdn.net/u/20120412/19/582af9df-7ce6-4456-8571-7968073f7984.html
// Author: DelphiGuy
__declspec(naked) int Sum_SSSE3(int *a, int size)
{
__asm{
xor eax,eax
mov ecx,[esp+8] // size
mov edx,[esp+4] // a ptr
cmp ecx,0
jle __5
push ebx
pxor xmm0,xmm0
mov ebx,ecx
pxor xmm1,xmm1
shr ecx,4
pxor xmm2,xmm2
and ebx,15
pxor xmm3,xmm3
jecxz __2
__1:
movdqa xmm7,[edx+48] // don't
movdqa xmm6,[edx+32] // make
movdqa xmm5,[edx+16] // any
movdqa xmm4,[edx] // change
paddd xmm3,xmm7 // in
paddd xmm2,xmm6 // left
paddd xmm1,xmm5 // instruction
paddd xmm0,xmm4 // orders
add edx,64
sub ecx,1
jnz __1
__2:
mov ecx,ebx
jecxz __4
__3:
add eax,[edx]
add edx,4
sub ecx,1
jnz __3
__4:
paddd xmm0,xmm1
paddd xmm2,xmm3
paddd xmm0,xmm2
phaddd xmm0,xmm0 // SSSE3 instruction
phaddd xmm0,xmm0 // SSSE3 instruction
movd edx,xmm0
add eax,edx
pop ebx
__5:
ret
}
}
#endif

// Intrinsics函数版
// Author: zyl910
int sum3_Intrinsics(int *a, int size)
{
if (NULL==a) return 0;
if (size<0) return 0;

int s = 0; // 返回值
__m128i xidSum = _mm_setzero_si128(); // 累积。[SSE2] 赋初值0
__m128i xidLoad; // 加载
int cntBlock = size / 4; // 块数。SSE寄存器能一次处理4个DWORD
int cntRem = size & 3; // 剩余数量
__m128i* p = (__m128i*)a;
for(int i = 0; i < cntBlock; ++i)
{
xidLoad = _mm_load_si128(p); // [SSE2] 加载
xidSum = _mm_add_epi32(xidSum, xidLoad); // [SSE2] 带符号32位紧缩加法
++p;
}

// 处理剩下的
int* q = (int*)p;
for(int i = 0; i < cntRem; ++i) s += q[i];

// 将累加值合并
xidSum = _mm_hadd_epi32(xidSum, xidSum); // [SSSE3] 带符号32位水平加法
xidSum = _mm_hadd_epi32(xidSum, xidSum);
s += _mm_cvtsi128_si32(xidSum); // [SSE2] 返回低32位

return s;
}


// Intrinsics函数版(四路循环展开)
// Author: zyl910
int sum3a_Intrinsics4(int *a, int size)
{
if (NULL==a) return 0;
if (size<0) return 0;

int s = 0; // 返回值
__m128i xidSum0 = _mm_setzero_si128(); // 累积。[SSE2] 赋初值0
__m128i xidSum1 = _mm_setzero_si128();
__m128i xidSum2 = _mm_setzero_si128();
__m128i xidSum3 = _mm_setzero_si128();
__m128i xidLoad0; // 加载
__m128i xidLoad1;
__m128i xidLoad2;
__m128i xidLoad3;
int cntBlock = size / 0x10; // 块数。SSE寄存器能一次处理4个DWORD,然后四路循环展开,即每批16个。
int cntRem = size & 0xF; // 剩余数量
__m128i* p = (__m128i*)a;
for(int i = 0; i < cntBlock; ++i)
{
xidLoad3 = _mm_load_si128(p+3); // [SSE2] 加载
xidLoad2 = _mm_load_si128(p+2);
xidLoad1 = _mm_load_si128(p+1);
xidLoad0 = _mm_load_si128(p);
xidSum3 = _mm_add_epi32(xidSum3, xidLoad3); // [SSE2] 带符号32位紧缩加法
xidSum2 = _mm_add_epi32(xidSum2, xidLoad2);
xidSum1 = _mm_add_epi32(xidSum1, xidLoad1);
xidSum0 = _mm_add_epi32(xidSum0, xidLoad0);
p+=4;
}

// 处理剩下的
int* q = (int*)p;
for(int i = 0; i < cntRem; ++i) s += q[i];

// 将累加值合并
xidSum0 = _mm_add_epi32(xidSum0, xidSum1); // [SSE2] 带符号32位紧缩加法
xidSum2 = _mm_add_epi32(xidSum2, xidSum3);
xidSum0 = _mm_add_epi32(xidSum0, xidSum2);
xidSum0 = _mm_hadd_epi32(xidSum0, xidSum0); // [SSSE3] 带符号32位水平加法
xidSum0 = _mm_hadd_epi32(xidSum0, xidSum0);
s += _mm_cvtsi128_si32(xidSum0); // [SSE2] 返回低32位

return s;
}

int _tmain(int argc, _TCHAR* argv[])
{
int i, t;

for (i = 0; i < T_SIZE; i++) T_ARRAY[i] = i;

_tprintf(strSumResult,
0,
T_SIZE - 1,
Sum_CPP(T_ARRAY, T_SIZE),
Sum_SSSE3(T_ARRAY, T_SIZE));
_tprintf(_T(", %d (sum3)"), sum3_Intrinsics(T_ARRAY, T_SIZE));
_tprintf(_T(", %d (sum3a)"), sum3a_Intrinsics4(T_ARRAY, T_SIZE));
_tprintf(_T("\n\n"));

t = clock();
for (i = 0; i < LoopCycle; i++)
S = Sum_CPP(T_ARRAY, T_SIZE);
t = clock() - t;
_tprintf(strTimeElapsed, strSumCPP, t / 1000.0);

#if defined(_WIN64)
// 64位环境下不能使用32位汇编代码
#else
t = clock();
for (i = 0; i < LoopCycle; i++)
S = Sum_SSSE3(T_ARRAY, T_SIZE);
t = clock() - t;
_tprintf(strTimeElapsed, strSumSSE, t / 1000.0);
#endif

t = clock();
for (i = 0; i < LoopCycle; i++)
S = sum3_Intrinsics(T_ARRAY, T_SIZE);
t = clock() - t;
_tprintf(strTimeElapsed, _T("sum3 (Intrinsics)"), t / 1000.0);

t = clock();
for (i = 0; i < LoopCycle; i++)
S = sum3a_Intrinsics4(T_ARRAY, T_SIZE);
t = clock() - t;
_tprintf(strTimeElapsed, _T("sum3a (Intrinsics4)"), t / 1000.0);

return 0;
}



测试脚本——
@echo off&setlocal enabledelayedexpansion
echo ## 32bit ##
for /l %%i in (1,1,3) do (
echo [%%i]
Release\testsum.exe
echo.
)

echo ## 64bit ##
for /l %%i in (1,1,3) do (
echo [%%i]
x64\Release\testsum.exe
echo.
)


CPU:Intel Core i3-2310M, 2100 MHz
内存:DDR3-1066 4GB
操作系统:Windows 7 x64
测试结果——
## 32bit ##
[1]
Sum of 0 to 9999 = 49995000 (sum1), 49995000 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.586s
Time elapsed of sum2 (asm ssse3) is: 1.360s
Time elapsed of sum3 (Intrinsics) is: 1.744s
Time elapsed of sum3a (Intrinsics4) is: 1.348s

[2]
Sum of 0 to 9999 = 49995000 (sum1), 49995000 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.583s
Time elapsed of sum2 (asm ssse3) is: 1.357s
Time elapsed of sum3 (Intrinsics) is: 1.747s
Time elapsed of sum3a (Intrinsics4) is: 1.352s

[3]
Sum of 0 to 9999 = 49995000 (sum1), 49995000 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.553s
Time elapsed of sum2 (asm ssse3) is: 1.357s
Time elapsed of sum3 (Intrinsics) is: 1.746s
Time elapsed of sum3a (Intrinsics4) is: 1.353s

## 64bit ##
[1]
Sum of 0 to 9999 = 49995000 (sum1), 0 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.541s
Time elapsed of sum3 (Intrinsics) is: 1.743s
Time elapsed of sum3a (Intrinsics4) is: 1.368s

[2]
Sum of 0 to 9999 = 49995000 (sum1), 0 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.531s
Time elapsed of sum3 (Intrinsics) is: 1.746s
Time elapsed of sum3a (Intrinsics4) is: 1.365s

[3]
Sum of 0 to 9999 = 49995000 (sum1), 0 (sum2), 49995000 (sum3), 49995000 (sum3a)

Time elapsed of sum1 (c++) is: 3.536s
Time elapsed of sum3 (Intrinsics) is: 1.745s
Time elapsed of sum3a (Intrinsics4) is: 1.361s



发现——
1.就算打开“/arch:SSE2”,VC++ 2010 SP1对Sum_CPP仍生成通用指令集代码,而不是SSE2代码。
2.四路循环展开的sum3a_Intrinsics4的64位版比32位版稍慢。但观察编译器生成的汇编代码时发现两者是基本相同的,唯一的区别是eax寻址升级为了rax寻址。原因待考。

零维星空 2012-04-12
  • 打赏
  • 举报
回复
神级人物,膜拜下
  • 打赏
  • 举报
回复
也谈“C++编译器到底能帮我们把代码优化到什么程度?”
http://topic.csdn.net/u/20120412/19/582af9df-7ce6-4456-8571-7968073f7984.html

请楼主指正。
邪哥 2012-01-03
  • 打赏
  • 举报
回复
听说release为了节约pcode使用的是esp寻址,不知道是真的还是假的
qq120848369 2012-01-02
  • 打赏
  • 举报
回复
快别写程序了。。。
zhangsongcui 2012-01-02
  • 打赏
  • 举报
回复
[Quote=引用 3 楼 xunxun1982 的回复:]

lz还没考虑编译器的SIMD化呢
[/Quote]
能做到SIMD化的编译器毕竟太少了,还是手写吧
倒是好像VC11已经能开线程了,当然openmp也容易


#include <iostream>
#include <numeric>
#include <dvec.h>

int main(int argc,char* argv[])
{
int a[10000];
std::iota(a, a+10000, 0);
Is32vec4 res = _mm_setzero_si128();
for (int* p=&a[0]; p!=&a[10000]; p+=4)
res += _mm_load_si128((__m128i const*)p);
std::cout << res[0] + res[1] + res[2] + res[3] << std::endl;
}


生成的汇编码

Is32vec4 res = _mm_setzero_si128();
00231035 pxor xmm0,xmm0
for (int* p=&a[0]; p!=&a[10000]; p+=4)
00231039 lea eax,[esp+18h]
0023103D lea ecx,[ecx]
res += _mm_load_si128((__m128i const*)p);
00231040 paddd xmm0,xmmword ptr [eax]
00231044 add eax,10h
00231047 lea ecx,[esp+9C58h]
0023104E cmp eax,ecx
00231050 jne main+40h (231040h)

xunxun 2012-01-02
  • 打赏
  • 举报
回复
lz还没考虑编译器的SIMD化呢
jixingzhong 2012-01-02
  • 打赏
  • 举报
回复
v5

顺祝新年愉快~
wangjieest 2012-01-02
  • 打赏
  • 举报
回复
编译器威武...CPU威武...不知道GPU咋想的

64,655

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧