、程式的抽象層越多,各抽象層分離得越嚴格,程式效率越低。
最原始的應用於網頁的php程式模式莫過於指令碼嵌入模式,即僅僅在一個網頁中需要動態處理或顯示資料的地方通過加入標識符嵌入php指令碼。一般來說這是php程式員最早學習的模式,它只有一個抽象層,就是網頁,故本文稱其為單層模式。
隨著網站規模逐漸增大,程式員可能會發現單層模式的程式很難維護,當想對程式修改或擴充功能時,會發現代碼非常混亂,感覺無從下手。於是模板類誕生了,它使一個網頁由兩個檔案組成:一個php程式檔案,一個html模板檔案。常用的模板類有PHPLib庫帶的Template模板類,Smarty模板類等。由於加入了額外的處理常式(模板類),程式效率下降了。你若不信可自己測試一下。其實一般情況下,不用函數(最原始的編程方法)比用函數(面向過程)的效率高,而用函數的效率又比對象封裝(物件導向)高。所以就算在編譯語言中,需要高效率的地方會用C寫而不用C++,例如FreeBSD作業系統的核心;而需要極端高效的地方還要用彙編寫。
為了使程式可以適應多種資料庫系統,或者方便隨時轉換資料庫系統,常常還會用一個類把跟資料庫打交道的函數封裝起來,這樣當轉換資料庫系統時只要把封裝類換掉就行了,主程式不需要修改。這裡又用了一個類,效率又打折扣了。
上述模板類的使用,使程式分成兩個抽象層:程式層和表現層。而資料庫類的使用又把程式層分為資料介面層和資料處理層。
項目越龐大,需要分離的抽象層就越多,這樣使得分工清晰,方便管理,但是以犧牲程式執行效率為代價。
對於抽象層造成的效率下降,最佳化的方法有二:減小抽象層、最佳化抽象層之間的介面。一般地,不應該為了提高效率而盲目減小抽象層,這樣會使得代碼混亂、難於管理。但是不應該為小項目建立過多的抽象層,除非你有將來把它做得很大的計劃。關於如何恰當分割抽象層,本文不作更深入討論。
對於上文說的兩個分層例子,最佳化抽象層之間的介面分別是模板類和資料庫操作類。抽象層介面在程式中需要被頻繁調用,以在不同層之間交換資訊,所以層介面是很值得最佳化的。對於資料庫介面類,可能僅僅是封裝一些資料庫函數,最佳化餘地恐怕不大。對於模板類,很多時候是有較大最佳化餘地的。一般地,模板模型越通用,模板類功能越強大,效率就越低,例如PHPLib庫帶的Template類就有極大的最佳化餘地。而Smarty模板類比PHPLib的Template更複雜,我沒有用過,據稱有緩衝機制,不知是否可以彌補其效能損耗。下面就來看看PHPLib的Template類有多少東西可以最佳化掉。
(1) 讀入模板檔案時,file函數效率低,改用get_file_content函數。
(2) 匹配子模板時,Regex替換函數preg_replace效率低,改用str_pos函數進行定位和用str_replace函數進行替換操作。此最佳化手段後文會詳細分析。
(3) 模板模型通用性很強,能適應各種情況,但在具體細節的處理上,通用的方法效率可能很低。可以對模板模型作適當修改。我的做法是建一個相對通用的模板類,然後再派生出一個只適用於特定程式的模板類。在通用模板類的模板模型上可以作些最佳化(相對於PHPLib的Template),例如在處理二維資料表的時候用PHPLib的Template處理就比較複雜,需要多次調用類方法(本質上是函數調用),所以重寫的時候可以把處理二維資料表的功能封裝到一個高效率的方法中,直接避免方法的多次調用。
(4) 調試功能在小項目上不需要用,跟調試相關的代碼全部去掉。
我在按上面4點重寫了模板類之後,一個複雜頁面的執行時間縮小了一個數量級(除模板外沒有作其它最佳化)。
現在就最佳化你的程式的抽象層之間的介面,特別是當這些介面是使用現成的函數或類的時候。因為這些函數或類在設計時會為了適應普遍情況而犧牲一些效率,而且它們的作者也可能沒有考慮效率問題。像如此著名的PHPLib的模板類的效率也不見得就高。
2、細節代碼最佳化
(1) 上文模板類最佳化已提到的,Regex匹配比一般字串匹配慢得多,儘可能用字串匹配而不用Regex匹配。有時候雖然用Regex匹配使程式碼更簡潔,而一般字串匹配使代碼更冗繁,但很多時候字串匹配仍比Regex高效。
(2) 字串替換函數str_replace和preg_replace都是可以接受數組參數的。有時候需要對字串進行批量替換,則用數組參數比迴圈調用替換函數來得高效。例如下面的代碼:
for ($i = 0; $i < $n; $i++) {
$str = str_replace($search[$i], $replace[$i], $str);
}
應該換成:
$str = str_replace($search, $replace, $str);
注意:這裡$search和$replace都是數組
str_replace和preg_replace的數組用法可參考PHP手冊。
(3) 對用於賦值的條件陳述式,可改用?:算符
這裡僅舉三個細節代碼最佳化的方法。實際上PHP程式還有很多細節代碼最佳化方法,要掌握這些方法,需要多看PHP手冊,多瞭解些函數。在解決一些細節問題時,用不同的函數作不同的搭配,就產生不同的方法,對不同的方法應進行實際效率測試,得出最佳化方法。
3、物件導向、面向過程、類、函數、宏
儘管物件導向方法在程式設計中有很多優點(這裡就不羅列了),但一般地說,物件導向程式的執行效率往往不如面向過程好,一個顯然的理由是物件導向的程式往往要頻繁調用對象的方法從而使代碼簡潔明了,卻降低了程式執行效率。
對於中小型項目,為程式效率著想,最好在思想上,物件導向與面向過程兼有,在代碼上,類、函數、宏搭配使用。這裡提到一個可能對讀者陌生的概念——宏(macro)。在C語言中有宏,宏彙編中也有宏,但PHP中官方沒有定義“宏”的概念。然而我們可以通過require函數和include函數實現宏的功能。require和include通常用來在程式碼中包含函數庫或類庫等檔案,一般很少用來直接包含程式碼檔案,因為包含程式碼有時會降低代碼易讀性。當有一段代碼需要在很多頁面中都執行時,通常被想到的是把它打包為函數或封裝為類。但此法有缺點如下:
(1) 降低程式效率。因為增加了函數/類方法調用。
(2) 有時此段內嵌程式碼較複雜,實現功能並不單純,封進函數並不符合函數功能單純的原則
(3) 有時內嵌程式碼需要與外部大量交換資料,如果封進函數會使參數表龐大,且處理函數的傳回值也變得複雜。
(4) 由於有以上(2)(3)兩點缺點,導致當內嵌程式碼與外部代碼交換的資料有所變化時,函數或類的介面的更改會變得麻煩。
當遇上以上情況時,我建議使用宏,即把內嵌程式碼直接寫入被包含檔案中,供各頁麵包含。但此時務必要清晰注釋宏的調用方法,輸入輸出資料等,以彌補代碼易讀性的降低。
總之,物件導向、面向過程、類、函數、宏,這些都應根據具體實際情況而搭配使用,不應盲目死守某些原則。
4、SQL資料庫
本來SQL資料庫的最佳化不應歸入PHP最佳化,但實際應用中PHP常與SQL資料庫配合構建網站,常用的資料庫有MySQL、PostgreSQL等。於是SQL查詢的效率也直接影響PHP程式的效率,故本文也略為談一談。
(1) MySQL中,使用InnoDB或BDB表(支援事務)的效率比MyISAM表(不支援事務)的效率低,尤其InnoDB表效率比MyISAM低很多,而 BDB表的主要劣勢在於佔用磁碟空間比MyISAM表多很多,且不能查知每個表佔用磁碟空間的大小。而對中小型應用,資料不一致性出現的機率是很微的,所以盡可使用MyISAM表提高效率。
(2) 恰當建立索引和儲存冗餘資料。此話題太大故本文不打算作詳述。
(3) 有些SQL查詢僅需知道資料庫中是否存在合格行,故只要查得一行,搜尋即可結束。所以對此類查詢,可在SQL句末加上LIMIT 1使資料庫一旦搜到滿足條件的行即不再搜尋。
(4) 盡量少用JOIN,有些低效率的JOIN查詢可通過儲存冗餘資料來避免。
(5) 有時候需要比較現在的時間與資料庫某列的時間的差距,返回時間差距滿足一定條件的行。在MySQL 4.1之前,是沒有DATEDIFF函數的,但可以用DATE_ADD或DATE_SUB函數簡潔實現,例如要返回xtime列距今超過10天的行,有兩種寫法:
法一:
SELECT somecolumn FROM sometable WHERE DATE_ADD(xtime, INTERVAL 10 DAY) < NOW()
法二:
SELECT somecolumn FROM sometable WHERE DATE_SUB(NOW(), INTERVAL 10 DAY) > xtime
兩種寫法執行結果等效但效率不同。如果xtime列建有索引,則法一的寫法無法使用索引,而法二的寫法可以用索引,故應採用第二種寫法提高效率。
5、延遲輸出與緩衝
有時候有些頁面無論如何也無法最佳化到需要的速度,此時可以考慮使用延遲輸出與緩衝的技術。
而緩衝也是一個大話題,故本文也不打算作詳述,但作一簡介。
常用的技術,按網站軟體層次分,有網頁服務軟體的緩衝技術,例如Apache的緩衝技術;又有動態指令碼的緩衝技術,例如用PHP編程實現的緩衝技術。
此外又可分為靜態緩衝技術和動態緩衝技術。靜態緩衝技術即把動態內容產生靜態html頁面存於磁碟,用戶端幾乎完全跟靜態頁面打交道,整個網站猶如一個靜態網站。而伺服器後台在適當時候調用動態程式更新靜態內容(重建靜態內容)。動態緩衝技術則是用戶端仍與動態網頁面打交道,而動態網頁在接到客戶請求時先檢查是否有相應的緩衝網頁,如有,直接把該靜態頁輸出到用戶端,如無或緩衝頁已淘汰,則重建緩衝頁面並輸出到用戶端。