Delphi代码优化

来源:互联网 发布:网络文学创作原理 编辑:程序博客网 时间:2024/06/04 18:53
KeyLife富翁笔记 作者 : zgl198171
标题 : Delphi代码优化
关键字:
分类 : 个人专区
密级 : 公开
(评分: , 回复: 0, 阅读: 2977) »»
忘掉extended

extended很大(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)

多用Round

Trunc会读写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%的效率提升。

Delphi代码优化(二) 整数篇



尽量使用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,这也不坏,不是吗?



临时子域类型

才揭过子域类型的短,又来说它的妙用:-p 但这也不是真正的子域类型,不过是形式上相似罢了。像以下的语句:

if ((x>=0) and (x=<10)) or ((x>=20) and (x<=30)) then …

可以改写为:

if x in [0..10,20..30] then …

子域数越多,优化效果越明显。不过除了在NOI题目里以外,天下可没有免费馅饼,这回的代价是占用一个临时寄存器。



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;



局部变量

与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穷折腾。不过用来量化一下自己代码效率的实际提升倒是件挺有成就感的事。

写在最后

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


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

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

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的定义:
type
Astring = 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非空情况下的最佳选择。