撰文/杜嵩 月光转载自程序员杂志第四期
局部变量
与C不同的是Delphi没有类似register的指示字,无法显式地定义一个寄存器变量,因为Delphi编译器已将这一步智能化了。有些局部变量会被自动化为寄存器变量,当然到底是哪些变量,Delphi内部是有自己的标准的,一般来说,被引用的较多的变量总是能被优化。而全局变量则无此好处。当然也有例外,以简单变量为元素的数组,作为全局变量可节约一个寄存器,而像字符串、动态数组、对象这类“堆栈变量”也不一定特意将其局部化。(之所以称它们为“堆栈变量”,是因为作为局部变量,它们仅在栈中存放一个指针,指向堆中分配的存储区,由此需要额外的入口和出口代码,Borland官方对此的解释是堆比栈快。)
局部过程
过程内部套过程,这也是Delphi独有的语法。然而调用局部过程会带来额外的栈操作,以便局部过程内可以访问其父过程的变量。因此有必要把局部过程挪出来,然后用参数传递需要的变量。
过程参数
Delphi中默认的调用约定是register,这种方式下EAX、ECX、EDX可被用来传递参数,所以过程的参数一般不要多于三个。而在对象类型的方法中,由于有了隐含的Self指针,建议参数不多于两个。
指针变量
指针是个极有用的东东,Java中弃之不用,C#中又被重拾。在Delphi中,指针为4字节大小,也可被寄存器化。有时候我们可以“暗示”编译器那么做,方法是使用with子句,比如:
with SomeStructure.SomeVar[i] do ///有些变量是类或者结构
begin
…
end;
这样,本来不会被优化的SomeStructure.SomeVar[i]就被寄存器化了。
数组
自从有了动态数组和乘法能力大幅提升的PII,链表除了在教科书里出现外,已经很少在实际编程中被使用了,事实也是如此,数组的确比传统链表快得多。
在Delphi中,数组类型有静态数组(var a:array[0..9] of byte)、动态数组(var a:array of byte)、指针数组(即指向静态数组的指针)和开放数组(仅用于参数传递)。静态数组、指针数组有速度快的好处,动态数组有大小可变的优势,权衡之下就有了折衷的办法,那就是定义的动态数组在必要时转换为指针。
值得注意的是,不加const或var修饰的动态数组会被作为形参传递,而动态数组用const修饰并不意味着你不能修改数组里的元素(不信你在上例中加上a[1]:=0;编译器不会报错)。上例中之所以没有使用High(a)而用了Length(a)是因为High调用了Length。
流程控制
对于结构化程序而言,break、continue、exit是不大被提倡的,但它们产生的代码是最简洁的,所以在编程中仍然占有一席之地。
Delphi引入了异常的概念,应当说是Object Pascal的一大进步。但异常捕捉是建立在增加额外代码的基础上的,在很少的代码外嵌套try块或是在循环内部使用异常捕捉,未免影响效率。另外,对于异常不做处理就简单丢弃也不是个好习惯。
强制类型转换
很多人习惯用absolute来进行类型转换,但这会阻止此变量成为寄存器变量。因而在过程中使用类型转换是个更好的选择。
枚举、集合
对于集合类型,增减单个元素时用include、exclude比s:=s+[a];快,这无须多言。
另外,可以用{$Zn}指示字来定义枚举类型的大小,将之定义为{$Z4}四字节可能会更快。
Pentium II带来的新问题
PII最不一般的特性就是它“超标量、多通道、乱序执行”的能力。“多通道”是指CPU内部有3个载入通道(其中两个只能载入简单指令)、5个执行通道(一个负责整数运算、一个负责整数和浮点运算、一个作地址运算,还有两个负责存取数据)和三个卸出通道;“乱序执行”则允许互不影响的指令在同一个时钟周期内、不同的通道内同时执行。这对代码执行的影响就是有些指令要执行一两个时钟周期(比如连续的浮点运算)、有些却因为并行而无需额外的执行周期(比如计算后的跳转)。以上只是概述,更详细的需要参考专门的Pentium优化指南和Intel的相关文档。
CPU视图
Delphi32的IDE中都有CPU视图(Delphi2、3中可通过修改注册表项来打开),调试时看看相应的汇编源码,以了解代码的优化情况,甚至精确计算所需的时钟周期(如果你水平足够的话),还是相当有效的。
循环语句
Delphi在编译循环语句时有自己独特而有效的方式,而且在大多数情况下工作得很好,但有时也需要自己弄些别的花样来,比如在较小的循环中使用更接近“汇编本质”的while结构。另外,对于较紧凑的循环将它们打开成非循环的代码,似乎更能适应PII下分支预测的倾向。
一个优化循环的例子:
for i:=1 to 40 do
begin
if i=20 then a[i]:=a[i]+20 else a[i]:=a[i]+10;
end
改写为:
for i:=1 to 19 do a[i]:=a[i]+10;
a[20]:=a[20]+20;
for i:=21 to 40 do a[i]:=a[i]+10;
增加了代码量,但减少了判断次数。减少循环条件判断也是增速的关键。
case语句
当case语句子界很多,不妨把它们分成几个部分,再套一层case。
当case语句的子界中有一两项常常用到,不妨把它们放在case前面用if判断。
填充和移动内存
在填充和移动大量内存时,最好自己写汇编,用32位指令实现。但使用movsd、stosd这类指令很容易遇到一个问题:数据地址或大小(尤其是后者)没有双字对齐怎么办?答案是这里是有空子可钻的,大多数数据在分配时总是默认双字对齐的,比如只考虑dword对齐。当然,鉴于这个做法会带来潜在的风险甚至bug,还是建议谨慎采用。
接口和虚方法
Object Pascal和java一样,不支持多重继承,但可以用interface实现。但在Delphi中interface意味着双重指针。
而调用一次虚方法,则需要通过对象指针得到VMT指针,再从VMT中取得方法指针,因而在必要时可以用变通的办法来实现。
代码对齐
代码对齐有增加代码大小的缺点,但它带来的速度提升的好处使这点牺牲显得值得,所以一般还是建议打开它。
代码风格
Pascal是一种优美的语言(相对于C++是一种简洁的语言--我在此并没有厚此薄彼的意思)。就我个人而言,为了优化而破坏这种优美实在心有不甘,好在Delphi并不会令我感到尴尬,反而是混乱的代码会带来问题。因此,保持良好的代码风格实在必要。
相信编译器
Borland拥有世界上最出色的编译器(当然也许更好的在你的脑子里),不仅速度快,而且编译期优化能力也是一流。因此在大多数情况下,自然的代码就能达到较高的效率,你不必为每段代码都绞尽脑汁,只要关键部分够快就行。
代码计时
在代码优化过程中,计时是一个很有效的手段,有很多这方面的软件可用。尽管不必像某些杂志上讲的那样,拿个什么xxxMark穷折腾。不过用来量化一下自己代码效率的实际提升倒是件挺有成就感的事。
写在最后
人们总是倾向于有一套美妙的规则,可以应对一切情形,可惜这对写文章无效,对代码优化同样如此。最有效的优化无过于算法的优化。因此,对编程者来予,保持一个开放的头脑,不断学习实践,才是成功的不二法门。