Delphi 代码优化

 Delphi 代码优化
Come from: http://www.optimalcode.com
文章编目 [显示隐藏]  
1. 字符串优化
1.1. 不重复初始化
1.2. 使用SetLength预分配长字符串(AnsiString)
1.3. 字符串与动态数组的线程安全(Thread Safety)
1.4. 避免使用短字符串
1.5. 避免使用copy函数
1.6. 总是使用长字符串,必要时转换为pchar
2. 整数代码优化
2.1. 尽量使用32位变量
2.2. 避免使用子界类型
2.3. 简化表达式
2.4. 不再畏惧乘法
2.5. 临时子界类型
2.6. 大整数运算
3. 浮点优化
3.1. 警惕 Extended
3.2. 改变FPU控制字
3.3. 多用Round
3.4. 传送实参
3.5. 自己动手,丰衣足食
3.6. 减少除法
3.7. 浮点零的检查
4. 其他优化
4.1. 局部变量
4.2. 局部过程
4.3. 过程参数
4.4. 指针变量
4.5. 数组
4.6. 流程控制
4.7. 强制类型转换
4.8. 枚举、集合
4.9. Pentium II带来的新问题
4.10. CPU视图
4.11. 循环语句
4.12. case语句
4.13. 填充和移动内存
4.14. 接口和虚方法
4.15. 代码对齐
4.16. 代码风格
4.17. 相信编译器
4.18. 代码计时
4.19. 写在最后
 




字符串优化
delphi有三种字符串类型:短字符串(string[n],n=1..255)存储区为静态分配,大小在编译时确定,这是继承于bp for dos的类型;字符数组(pchar)主要是为了兼容各类api,在bp7中已经出现,如今在delphi中更加应用广泛,其存储区可以用字符数组静态分配,也可用getmem手动分配;而长字符串(ansistring)是delphi独有的,其存储区在运行时动态分配,最灵活也最易被滥用。

不重复初始化delphi默认字符串类型AnsiString会自动初始化为空。如下代码:



delphi有三种字符串类型:短字符串(string[n],n=1..255)存储区为静态分配,大小在编译时确定,这是继承于bp for dos的类型;字符数组(pchar)主要是为了兼容各类api,在bp7中已经出现,如今在delphi中更加应用广泛,其存储区可以用字符数组静态分配,也可用getmem手动分配;而长字符串(ansistring)是delphi独有的,其存储区在运行时动态分配,最灵活也最易被滥用。

var s:string;
begin
s:="";
……
end;

s:="";就属多此一举。但是值得注意的是这对函数返回值result无效。而一般说来,用var实参传递比返回字符串值要更快一些。


使用SetLength预分配长字符串(AnsiString)动态分配内存是AnsiString的一大长项,但容易弄巧成拙,一个典型的例子如下:



s2:=" ";
for i:=2 to length(s1) do s2:=s2+s1[i];

且不说可用delete取代之,主要问题在于上例的循环中s2的内存区域被不停地重复分配,相当费时。一个简单有效的办法如下:


setlength(s2,length(s1)-1);
for i:=2 to length(s1) do s2[i-1]:=s1[i];

这样s2内存只会重新分配一次。


字符串与动态数组的线程安全(Thread Safety)在delphi 5以前动态数组与长字符串的操作这些非线程安全调用是由引用计数来处理其临界问题的,而自delphi5起就改为直接在一些临界指令前加lock指令前缀来避免这个问题。不幸的是这一修改的代价相当昂贵,因为在pentiumⅱ处理器中lock指令相当费时,大概要耗费额外的28个指令周期来完成这一操作,因而整体效率至少下降一半。解决这个问题的办法只有一个,那就是修改delphi rtl核心代码。在备份原文件后,将source/rtl/sys/system.pas中所有的lock替换为{lock},当然必须是整字替换。如此还未完全优化,下一步是将delphi4运行库中也有的xchg指令去掉,因为该指令有隐含的lock前缀,所以必须将system.pas内_lstrasg和_strlasg两个过程中的 xchg edx,[eax] 替换为如下代码:

mov ecx,[eax]
mov [eax],edx
mov edx,ecx

ok大功告成,编译一下,覆盖system.dcu即可。如此其执行效率将比delphi5提高6倍,比delphi4提高2倍。


避免使用短字符串由于很多字符串操作会先把短字符串转换为长字符串,从而减慢了执行速度,因此还是少使用短字符串为妙。


避免使用copy函数这也和滥用内存管理有关。一个典型的情形如下:

if copy(s1,23,64)=copy(s2,15,64) then ……

这样导致分配了两块临时内存,因而降低了效率。应当替换为如下代码:
i:=0;
f:=false;
repeat
 f:=s1[i+23]<>s2[i+15];
 inc(i);
until f or (i>63);
if not f then ……

同样的,如下语句就显得相当低效:
s:=copy(s,1,length(s)-10);

应改为
delete(s,length(s)-10,10);

顺便提一句,在连接字符串时,s:=s1+s2;简单而有效;但在delphi2下则s:=format([%s%s],s1,s2);可能稍快些。

总是使用长字符串,必要时转换为pchar先看看AnsiString的定义:





在delphi 5以前动态数组与长字符串的操作这些非线程安全调用是由引用计数来处理其临界问题的,而自delphi5起就改为直接在一些临界指令前加lock指令前缀来避免这个问题。不幸的是这一修改的代价相当昂贵,因为在pentiumⅱ处理器中lock指令相当费时,大概要耗费额外的28个指令周期来完成这一操作,因而整体效率至少下降一半。解决这个问题的办法只有一个,那就是修改delphi rtl核心代码。在备份原文件后,将source/rtl/sys/system.pas中所有的lock替换为{lock},当然必须是整字替换。如此还未完全优化,下一步是将delphi4运行库中也有的xchg指令去掉,因为该指令有隐含的lock前缀,所以必须将system.pas内_lstrasg和_strlasg两个过程中的 xchg edx,[eax] 替换为如下代码: 这样导致分配了两块临时内存,因而降低了效率。应当替换为如下代码: 同样的,如下语句就显得相当低效: 应改为 顺便提一句,在连接字符串时,s:=s1+s2;简单而有效;但在delphi2下则s:=format([%s%s],s1,s2);可能稍快些。
type
  AnsiString = packed record
  allocsiz: longint; //动态分配大小
  refcnt: longint; //引用计数
  Length: longint; //实际长度
  ChrArr:array[1..allocsiz-6]of char; //字节序列
  end;

其中astring[1]将返回astring.chrarr[1]的内容。很多人认为ansistring是天生低效的。其实这在很大程度上是由代码编写不良、内存管理乱用和缺乏支持的函数所致。如上所述,一旦被动态分配了一块内存,长字符串就成了一个线性的字节序列,并无所谓的效率问题。当然,若有更多有效的函数支持那就更好了。说到ansistring到pchar的转换,本质上有三个办法:

 1.p:=@s[1];这会引发uniquestring调用。
 2.p:=pchar (s);这会先检查s是否为空,若是,则返回nil,否则即返回s[1]的地址。
 3.p:=pointer(s);这不会引发任何隐含调用,因而是在确定s非空情况下的最佳选择。


整数代码优化

尽量使用32位变量在32 位代码中32 位变量是默认处理格式16位变量 word shortint widechar 的运算会令处理器临时切换为 16位处理模式因而需要双倍的处理时间相较之下8位变量byte char 只要不与其它混用却不会太慢如果实在需要多次使用一个8或16位变量可以考虑把它临时转换成32 位变量这只需要一步赋值ADWord:=Aword;


避免使用子界类型Pascal语言的一大优势便是其丰富的数据类型Delphi之 Object Pascal继承了这一传统枚举和子界类型即属此类但不幸的是他们会为优化带来麻烦因为它们的占用的字节数取决于其子界的大小比如一个元素数不超过256个的枚举类型会占用1 个字节而例如MyYear=1900..2000则会占用两个字节而如前文所述16位变量是很慢的





简化表达式过于复杂的表达式会妨碍编译器的自动优化这时可以考虑引入临时变量来简化表达式这样可以优化更重要的是提高了代码的可读性


不再畏惧乘法PII出现以前乘法运算是相当费时的以至于当时的经典优化方法便是把一类特殊的乘法转变为移位运算和加法而今在作为标准配置的PII上乘法和多数其它运算一样只需要一个指令周期即可完成当然Delphi编译器仍然会把诸如 *2 之类的运算优化为shl 1 这也不坏不是吗


临时子界类型才揭过子界类型的短又来说它的妙用像以下的语句 if ((x>=0) and (x

10)) or ((x>=20) and (x
30)) then ... 可以改写为 if x in [0..10,20..30] then ... 子界数越多优化效果越明显,不过天下可没有免费馅饼这回的代价是占用一个临时寄存器 movzx 与xor/mov 这是读入小于32位数据的两种不同方法后者在PII以前更具优势而前者在PII上因其乱序执行的特性而显得更有效率编译器对此的取舍规则似乎很复杂必要时还是自己用嵌入汇编好了

大整数运算对付大整数超过32 位的你有四种武器为什么不是七种?问Borland 别来问我--int64 comp double 和extended 其中除了64 位整数类型int64外其余都是浮点数其运算都是由FPU指令实现的这其中的comp类型存储结构同int64一模一样按照Borland的官方说法comp 类型已经过时应当被int64所取代理由很简单--整数运算总比浮点快吧然而根据一项在PII上进行的测试int64 除了在加减运算中具有无可比拟的优势外在乘除方面竟比浮点数还慢好在还有老当益壮的comp 只是稍有些繁琐首先将变量声明为int64 并声明两个辅助变量。

var
  a,b,c,d,e: int64;
  ca:comp absolute a;
  cc:comp absolute c;
//加减法不用变除法就如下处理
c:=trunc(ca/b);   //is faster than c:= a div b
乘法这么来
e:=round(ca*b+cc*d); //is faster than e:=a*b+c*d;


浮点优化

警惕 Extendedextended很大(10字节,如果代码对齐就有12字节),读写运算都很慢,是优化的大敌。且Delphi2-4对extended的代码对齐有bug。因此,若非必要,不要用extended。

同时,在混合浮点类型的运算中,编译器为了不丢失精度,临时变量以extended类型存储,所以要避免混合浮点运算。

还有,用const定义的常量,如不加指明,则也默认为extended类型。解决办法是,配合$J指示字,定义指明类型常量(typed constand)。


改变FPU控制字默认的FPU控制字令除法运算和PII/PIII上的平方根运算慢而精确,当无须得到这样的结果时,可用Set8087CW让FPU"偷懒"。

对于Single类型:Set8087CW(Default8087CW and $FCFF)

对于Double类型:Set8087CW((Default8087CW and $FCFF) or $0200)

对于extended类型:Set8087CW(Default8087CW or $0300)


多用RoundTrunc会读写FPU指令字,而Round不会,所以可以的话,尽量用Round。


传送实参对于返回浮点值的函数,入口和出口处会有附加的压栈退栈,对形如:


function func(x : SomeType): SomeFloat;

不妨改写为:
procedure func(x : SomeType; var fp : SomeFloat);

对于在过程中未修改的浮点形参,没必要用const修饰,因为那除了增加一个编译期检查外,别无用处。相应的对策是用var修饰为实参,强制传址。

自己动手,丰衣足食Delphi本身不对浮点运算作任何优化,因此很多时候,还得自己用汇编来解决。

值得注意的是,Delphi中浮点异常的触发,不是在出错之后,而是在下一条浮点指令之前。因此,通常的作法是,在一次浮点操作完毕后,加一条FWAIT指令。


减少除法除法,即多次的减法,其代价是相当昂贵的,因而有必要减少除法的次数。

另外,对于简单除法(如:a/5),编译器不一定(?!)会将其变为乘法(a*0.2),比如:


 fp:=fp*3*4/5+3*4/2;
在Delphi 4中,会被编译为:


 fp:=fp*3*4/5+6;
而只有:


 fp:=3*4/5*fp+3*4/2;
才会被编译为:

fp:=2.4*fp+6;

鉴于编译器的繁复规则,建议这一步优化自己完成。

浮点零的检查检查一个浮点数是否为零,如果简单的"Afloat=0",会把0转换为浮点零。而更好的办法是这样:

对于Single类型:

 (Dword(pointer(Asingle))shl 1) =0
对于Double类型:

type

DoubleData=record lo,hi:Dword end;

Var

ADouble:Double;

Dd:DoubleData absolute Adouble;

begin



if ((dd.hi shl 1)+dd.lo)=0 then…

end;

此法在PII上有30%-40%的效率提升。

其他优化

局部变量与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;

这样,本来不会被优化的

















30)) then ... 可以改写为 if x in [0..10,20..30] then ... 子界数越多优化效果越明显,不过天下可没有免费馅饼这回的代价是占用一个临时寄存器 movzx 与xor/mov 这是读入小于32位数据的两种不同方法后者在PII以前更具优势而前者在PII上因其乱序执行的特性而显得更有效率编译器对此的取舍规则似乎很复杂必要时还是自己用嵌入汇编好了 不妨改写为: 对于在过程中未修改的浮点形参,没必要用const修饰,因为那除了增加一个编译期检查外,别无用处。相应的对策是用var修饰为实参,强制传址。 鉴于编译器的繁复规则,建议这一步优化自己完成。 此法在PII上有30%-40%的效率提升。 与C不同的是Delphi没有类似register的指示字,无法显式地定义一个寄存器变量,因为Delphi编译器已将这一步智能化了。有些局部变量会被自动化为寄存器变量,当然到底是哪些变量,Delphi内部是有自己的标准的,一般来说,被引用的较多的变量总是能被优化。而全局变量则无此好处。当然也有例外,以简单变量为元素的数组,作为全局变量可节约一个寄存器,而像字符串、动态数组、对象这类"堆栈变量"也不一定特意将其局部化。(之所以称它们为"堆栈变量",是因为作为局部变量,它们仅在栈中存放一个指针,指向堆中分配的存储区,由此需要额外的入口和出口代码,Borland官方对此的解释是堆比栈快。) 这样,本来不会被优化的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穷折腾。不过用来量化一下自己代码效率的实际提升倒是件挺有成就感的事。


写在最后人们总是倾向于有一套美妙的规则,可以应对一切情形,可惜这对写文章无效,对代码优化同样如此。最有效的优化无过于算法的优化。因此,对编程者来予,保持一个开放的头脑,不断学习实践,才是成功的不二法门。


改写为: 增加了代码量,但减少了判断次数。减少循环条件判断也是增速的关键。

你可能感兴趣的:(Delphi 代码优化)