程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> Delphi代碼優化

Delphi代碼優化

編輯:Delphi

文章編目

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會自動初始化為空。如下代碼:

 

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 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;

浮點優化

警惕 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%的效率提升。

其他優化

局部變量
與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窮折騰。不過用來量化一下自己代碼效率的實際提升倒是件挺有成就感的事。


寫在最後
人們總是傾向於有一套美妙的規則,可以應對一切情形,可惜這對寫文章無效,對代碼優化同樣如此。最有效的優化無過於算法的優化。因此,對編程者來予,保持一個開放的頭腦,不斷學習實踐,才是成功的不二法門

摘自:踏雪無痕 blog

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved