
常用软件类: |
|杀毒安全 | |联络聊天 | |网络软件 | |多媒体类 | |系统工具 | |图形图像 | |系统工具 | |应用软件 | |行业软件 |
开发设计类: |
|动画制作 | |图像处理 | |3D设计 | |操作系统 | |站长学院 | |网络相关 | |WEB设计 | |数据库类 | |程序开发 |
从前一节的图中我们不难看出,方案2中,如果谁的动作慢,那么他就会成为性能的瓶颈。实际上,CPU也不会像我描述的那样四平八稳地运行,指令执行的不同阶段需要的时间(时钟周期数)是不同的,因此,缩短关键步骤(即,造成瓶颈的那个步骤)是缩短执行时间的关键。
至少对于使用Intel系列的CPU来说,取数据这个步骤需要消耗比较多的时间。此外,假如数据跨越了某种边界(如4或8字节,与CPU的字长有关),则CPU需要启动两次甚至更多次数的读内存操作,这无疑对性能构成不利影响。
基于这样的原因,我们可以得到下面的设计策略:
|
|
第一条规则比较简单。例如,需要求一组数据中的最大值、最小值、平均数,那么,最好是在一次循环中做完。
“于是,这家伙又攒了一段代码”……
| 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 /= 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 |
上面的程序倒是没有什么惊人之处。唯一一个比较吓人的东西是那个jmp SHORT指令,它是否有用取决于具体的问题。C/C++编译器有时会产生这样的代码,我过去曾经错误地把所有的此类指令当作没用的代码而删掉,后来发现程序执行时间没有明显的变化。通过查阅文档才知道,这类指令实际上是“占位指令”,他们存在的意义在于占据那个地方,一来使其他语句能够正确地按CPU觉得舒服的方式对齐,二来它可以占据CPU的某些周期,使得后续的指令能够更好地并发执行,避免冲突。另一个比较常见的、实现类似功能的指令是NOP。
占位指令的去留主要是靠计时执行来判断。由于目前流行的操作系统基本上都是多任务的,因此会对计时的精确性有一定影响。如果需要进行测试的话,需要保证以下几点:
|
|
对于绝大多数程序来说,计时测试是一个非常重要的东西。我个人倾向于在进行优化后进行计时测试并比较结果。目前,我基于经验进行的优化基本上都能够提高程序的执行性能,但我还是不敢过于自信。优化确实会提高性能,但人做的和编译器做的思路不同,有时,我们的确会做一些费力不讨好的事情。