当前位置: 首页 >> 程序设计 >> 高级编程之编译优化
 

高级编程之编译优化

作者:      来源:zz     发表时间:2008-04-20     浏览次数:      字号:    

5.5 存储优化

从前一节的图中我们不难看出,方案2中,如果谁的动作慢,那么他就会成为性能的瓶颈。实际上,CPU也不会像我描述的那样四平八稳地运行,指令执行的不同阶段需要的时间(时钟周期数)是不同的,因此,缩短关键步骤(即,造成瓶颈的那个步骤)是缩短执行时间的关键。

至少对于使用Intel系列的CPU来说,取数据这个步骤需要消耗比较多的时间。此外,假如数据跨越了某种边界(如4或8字节,与CPU的字长有关),则CPU需要启动两次甚至更多次数的读内存操作,这无疑对性能构成不利影响。

基于这样的原因,我们可以得到下面的设计策略:


程序设计中的内存数据访问策略

  • 尽可能减少对于内存的访问。在不违背这一原则的前提下,如果可能,将数据一次处理完。
  • 尽可能将数据按4或8字节对齐,以利于CPU存取
  • 尽可能一段时间内访问范围不大的一段内存,而不同时访问大量远距离的分散数据,以利于Cache缓存*

第一条规则比较简单。例如,需要求一组数据中的最大值、最小值、平均数,那么,最好是在一次循环中做完。

“于是,这家伙又攒了一段代码”……

int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};
int i;
int avg, max, min;

avg=max=min=a[0];

for(i=1; i<(sizeof(a)/sizeof(int)); i++){
avg+=a[i];
if(max < a[i])
max = a[i];
else if(min > a[i])
min = a[i];
}

avg /= i;

Visual C++编译器把最开始一段赋值语句翻译成了一段简直可以说是匪夷所思的代码:

; int a[]={1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0};

mov edi, 2 ; 此时edi没有意义
mov esi, 3 ; esi也是!临时变量而已。
mov DWORD PTR _a$[esp+92], edi
mov edx, 5 ; 黑名单加上edx
mov eax, 7 ; eax也别跑:)
mov DWORD PTR _a$[esp+132], edi
mov ecx, 9 ; 就差你了,ecx

; int i;
; int avg, max, min;
; avg=max=min=a[0];

mov edi, 1 ; edi摇身一变,现在它是min了。
mov DWORD PTR _a$[esp+96], esi
mov DWORD PTR _a$[esp+104], edx
mov DWORD PTR _a$[esp+112], eax
mov DWORD PTR _a$[esp+136], esi
mov DWORD PTR _a$[esp+144], edx
mov DWORD PTR _a$[esp+152], eax
mov DWORD PTR _a$[esp+88], 1 ; 编译器失误 此处edi应更好
mov DWORD PTR _a$[esp+100], 4
mov DWORD PTR _a$[esp+108], 6
mov DWORD PTR _a$[esp+116], 8
mov DWORD PTR _a$[esp+120], ecx
mov DWORD PTR _a$[esp+124], 0
mov DWORD PTR _a$[esp+128], 1
mov DWORD PTR _a$[esp+140], 4
mov DWORD PTR _a$[esp+148], 6
mov DWORD PTR _a$[esp+156], 8
mov DWORD PTR _a$[esp+160], ecx
mov DWORD PTR _a$[esp+164], 0
mov edx, edi ; edx是max。
mov eax, edi ; 期待已久的avg, 它被指定为eax

这段代码是最优的吗?我个人认为不是。因为编译器完全可以在编译过程中直接把它们作为常量数据放入内存。此外,如果预先对a[0..9]10个元素赋值,并利用串操作指令(rep movsdw),速度会更快一些。

当然,犯不上因为这些问题责怪编译器。要求编译器知道a[0..9]和[10..19]的内容一样未免过于苛刻。我们看看下面的指令段:

; for(i=1; ...

mov esi, edi
for_loop:

; avg+=a[i];

mov ecx, DWORD PTR _a$[esp+esi*4+88]
add eax, ecx

; if(max < a[i])

cmp edx, ecx
jge SHORT elseif_min

; max = a[i];

mov edx, ecx

; else if(min > a[i])

jmp SHORT elseif_min
elseif_min:
cmp edi, ecx
jle SHORT elseif_end

; min = a[i];
mov edi, ecx

elseif_end:

; [for i=1]; i<20; i++){

inc esi
cmp esi, 20
jl SHORT for_loop

; }
; avg /= i;

cdq
idiv esi


; esi: i




; ecx: 暂存变量, =a[i]
; eax: avg



; edx: max








; 有趣的代码...并不是所有的时候都有用
; 但是也别随便删除
; edi: min









; i++
; i与20比较






; avg /= i

上面的程序倒是没有什么惊人之处。唯一一个比较吓人的东西是那个jmpSHORT指令,它是否有用取决于具体的问题。C/C++编译器有时会产生这样的代码,我过去曾经错误地把所有的此类指令当作没用的代码而删掉,后来发现程序执行时间没有明显的变化。通过查阅文档才知道,这类指令实际上是“占位指令”,他们存在的意义在于占据那个地方,一来使其他语句能够正确地按CPU觉得舒服的方式对齐,二来它可以占据CPU的某些周期,使得后续的指令能够更好地并发执行,避免冲突。另一个比较常见的、实现类似功能的指令是NOP。

占位指令的去留主要是靠计时执行来判断。由于目前流行的操作系统基本上都是多任务的,因此会对计时的精确性有一定影响。如果需要进行测试的话,需要保证以下几点:


计时测试需要注意的问题

  • 测试必须在没有额外负荷的机器上完成。例如,专门用于编写和调试程序的计算机
  • 尽量终止计算机上运行的所有服务,特别是杀毒程序
  • 切断计算机的网络,这样网络的影响会消失
  • 将进程优先级调高。对于Windows系统来说,把进程(线程)设置为Time-Critical; 对于*nix系统来说,把进程设置为实时进程
  • 将测试函数运行尽可能多次运行,如10000000次,这样能够减少由于进城切换而造成的偶然误差
  • 最后,如果可能的话,把函数放到单进程的系统(例如FreeDOS)中运行。

对于绝大多数程序来说,计时测试是一个非常重要的东西。我个人倾向于在进行优化后进行计时测试并比较结果。目前,我基于经验进行的优化基本上都能够提高程序的执行性能,但我还是不敢过于自信。优化确实会提高性能,但人做的和编译器做的思路不同,有时,我们的确会做一些费力不讨好的事情。

[1] [2] [3]

责任编辑 webmaster

 
 
 
 
 
评论更多>>
 
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •