Delphi程式碼最佳化 完結篇 (轉)

worldblog發表於2007-12-06
Delphi程式碼最佳化 完結篇 (轉)[@more@]

撰文/杜嵩 月光轉載自員雜誌第四期

區域性變數

  與C不同的是沒有類似register的指示字,無法顯式地定義一個暫存器變數,因為Delphi已將這一步智慧化了。有些區域性變數會被自動化為暫存器變數,當然到底是哪些變數,Delphi內部是有自己的標準的,一般來說,被引用的較多的變數總是能被。而全域性變數則無此好處。當然也有例外,以簡單變數為元素的陣列,作為全域性變數可節約一個暫存器,而像字串、動態陣列、這類“堆疊變數”也不一定特意將其區域性化。(之所以稱它們為“堆疊變數”,是因為作為區域性變數,它們僅在棧中存放一個指標,指向堆中分配的區,由此需要額外的入口和出口程式碼,Borland官方對此的解釋是堆比棧快。)

區域性過程

  過程內部套過程,這也是Delphi獨有的語法。然而區域性過程會帶來額外的棧操作,以便區域性過程內可以訪問其父過程的變數。因此有必要把區域性過程挪出來,然後用引數傳遞需要的變數。

過程引數

  Delphi中預設的呼叫約定是register,這種方式下EAX、ECX、EDX可被用來傳遞引數,所以過程的引數一般不要多於三個。而在物件型別的方法中,由於有了隱含的Self指標,建議引數不多於兩個。

指標變數

  指標是個極有用的東東,中棄之不用,中又被重拾。在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引入了異常的概念,應當說是 Pascal的一大進步。但異常捕捉是建立在增加額外程式碼的基礎上的,在很少的程式碼外巢狀try塊或是在迴圈內部使用異常捕捉,未免影響。另外,對於異常不做處理就簡單丟棄也不是個好習慣。

強制型別轉換

  很多人習慣用absolute來進行型別轉換,但這會阻止此變數成為暫存器變數。因而在過程中使用型別轉換是個更好的選擇。

列舉、集合

  對於集合型別,增減單個元素時用include、exclude比s:=s+[a];快,這無須多言。
  另外,可以用{$Zn}指示字來定義列舉型別的大小,將之定義為{$Z4}四位元組可能會更快。

Pentium II帶來的新問題

  PII最不一般的特性就是它“超標量、多通道、亂序”的能力。“多通道”是指內部有3個載入通道(其中兩個只能載入簡單指令)、5個執行通道(一個負責整數運算、一個負責整數和浮點運算、一個作地址運算,還有兩個負責存取資料)和三個卸出通道;“亂序執行”則允許互不影響的指令在同一個時鐘週期內、不同的通道內同時執行。這對程式碼執行的影響就是有些指令要執行一兩個時鐘週期(比如連續的浮點運算)、有些卻因為並行而無需額外的執行週期(比如計算後的跳轉)。以上只是概述,更詳細的需要參考專門的Pentium最佳化指南和的相關文件。

CPU檢視

  Delphi32的中都有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這類指令很容易遇到一個問題:資料地址或大小(尤其是後者)沒有雙字對齊怎麼辦?答案是這裡是有空子可鑽的,大多數資料在分配時總是預設雙字對齊的,比如只考慮d對齊。當然,鑑於這個做法會帶來潛在的風險甚至,還是建議謹慎採用。

介面和虛方法

  Object Pascal和java一樣,不支援多重繼承,但可以用interface實現。但在Delphi中interface意味著雙重指標。
  而呼叫一次虛方法,則需要透過物件指標得到VMT指標,再從VMT中取得方法指標,因而在必要時可以用變通的辦法來實現。

程式碼對齊

  程式碼對齊有增加程式碼大小的缺點,但它帶來的速度提升的好處使這點犧牲顯得值得,所以一般還是建議開啟它。

程式碼風格

  Pascal是一種優美的語言(相對於C++是一種簡潔的語言--我在此並沒有厚此薄彼的意思)。就我個人而言,為了最佳化而破壞這種優美實在心有不甘,好在Delphi並不會令我感到尷尬,反而是混亂的程式碼會帶來問題。因此,保持良好的程式碼風格實在必要。

相信編譯器

  Borland擁有世界上最出色的編譯器(當然也許更好的在你的腦子裡),不僅速度快,而且編譯期最佳化能力也是一流。因此在大多數情況下,自然的程式碼就能達到較高的效率,你不必為每段程式碼都絞盡腦汁,只要關鍵部分夠快就行。

程式碼計時

  在程式碼最佳化過程中,計時是一個很有效的手段,有很多這方面的可用。儘管不必像某些雜誌上講的那樣,拿個什麼xxxMark窮折騰。不過用來量化一下自己程式碼效率的實際提升倒是件挺有成就感的事。

寫在最後

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-988890/,如需轉載,請註明出處,否則將追究法律責任。

相關文章