網上很多文章介紹優化PHP程序,是通過安裝Zend Optimizer之類的加速軟件實現的,但這種加速是有限的。本文主要從程序代碼著手介紹一些優化手段。
1、程序的抽象層越多,各抽象層分離得越嚴格,程序效率越低。
最原始的應用於網頁的php程序模式莫過於腳本嵌入模式,即僅僅在一個網頁中需要動態處理或顯示數據的地方通過加入標識符嵌入php腳本。一般來說這是PHP程序員最早學習的模式,它只有一個抽象層,就是網頁,故本文稱其為單層模式。
隨著網站規模逐漸增大,程序員可能會發現單層模式的程序很難維護,當想對程序修改或擴充功能時,會發現代碼非常混亂,感覺無從下手。於是模板類誕生了,它使一個網頁由兩個文件組成:一個PHP程序文件,一個Html模板文件。常用的模板類有PHPLib庫帶的Template模板類,Smarty模板類等。由於加入了額外的處理程序(模板類),程序效率下降了。你若不信可自己測試一下。其實一般情況下,不用函數(最原始的編程方法)比用函數(面向過程)的效率高,而用函數的效率又比對象封裝(面向對象)高。所以就算在編譯語言中,需要高效率的地方會用C寫而不用C++,例如FreeBSD操作系統的內核;而需要極端高效的地方還要用匯編寫。
為了使程序可以適應多種數據庫系統,或者方便隨時轉換數據庫系統,常常還會用一個類把跟數據庫打交道的函數封裝起來,這樣當轉換數據庫系統時只要把封裝類換掉就行了,主程序不需要修改。這裡又用了一個類,效率又打折扣了。
上述模板類的使用,使程序分成兩個抽象層:程序層和表現層。而數據庫類的使用又把程序層分為數據接口層和數據處理層。
項目越龐大,需要分離的抽象層就越多,這樣使得分工清晰,方便管理,但是以犧牲程序執行效率為代價。
對於抽象層造成的效率下降,優化的方法有二:減小抽象層、優化抽象層之間的接口。一般地,不應該為了提高效率而盲目減小抽象層,這樣會使得代碼混亂、難於管理。但是不應該為小項目建立過多的抽象層,除非你有將來把它做得很大的計劃。關於如何恰當分割抽象層,本文不作更深入討論。
對於上文說的兩個分層例子,優化抽象層之間的接口分別是模板類和數據庫操作類。抽象層接口在程序中需要被頻繁調用,以在不同層之間交換信息,所以層接口是很值得優化的。對於數據庫接口類,可能僅僅是封裝一些數據庫函數,優化余地恐怕不大。對於模板類,很多時候是有較大優化余地的。一般地,模板模型越通用,模板類功能越強大,效率就越低,例如PHPLib庫帶的Template類就有極大的優化余地。而Smarty模板類比PHPLib的Template更復雜,我沒有用過,據稱有緩存機制,不知是否可以彌補其性能損耗。下面就來看看PHPLib的Template類有多少東西可以優化掉。
(1) 讀入模板文件時,file函數效率低,改用get_file_content函數。
(2) 匹配子模板時,正則表達式替換函數preg_replace效率低,改用str_pos函數進行定位和用str_replace函數進行替換操作。此優化手段後文會詳細分析。
(3) 模板模型通用性很強,能適應各種情況,但在具體細節的處理上,通用的方法效率可能很低。可以對模板模型作適當修改。我的做法是建一個相對通用的模板類,然後再派生出一個只適用於特定程序的模板類。在通用模板類的模板模型上可以作些優化(相對於PHPLib的Template),例如在處理二維數據表的時候用PHPLib的Template處理就比較復雜,需要多次調用類方法(本質上是函數調用),所以重寫的時候可以把處理二維數據表的功能封裝到一個高效率的方法中,直接避免方法的多次調用。
(4) 調試功能在小項目上不需要用,跟調試相關的代碼全部去掉。
我在按上面4點重寫了模板類之後,一個復雜頁面的執行時間縮小了一個數量級(除模板外沒有作其它優化)。
現在就優化你的程序的抽象層之間的接口,特別是當這些接口是使用現成的函數或類的時候。因為這些函數或類在設計時會為了適應普遍情況而犧牲一些效率,而且它們的作者也可能沒有考慮效率問題。像如此著名的PHPLib的模板類的效率也不見得就高。
2、細節代碼優化
(1) 上文模板類優化已提到的,正則表達式匹配比一般字符串匹配慢得多,盡可能用字符串匹配而不用正則表達式匹配。有時候雖然用正則表達式匹配使程序代碼更簡潔,而一般字符串匹配使代碼更冗繁,但很多時候字符串匹配仍比正則表達式高效。
(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頁面存於磁盤,客戶端幾乎完全跟靜態頁面打交道,整個網站猶如一個靜態網站。而服務器後台在適當時候調用動態程序更新靜態內容(重新生成靜態內容)。動態緩沖技術則是客戶端仍與動態頁面打交道,而動態網頁在接到客戶請求時先檢查是否有相應的緩沖網頁,如有,直接把該靜態頁輸出到客戶端,如無或緩沖頁已過時,則重新生成緩沖頁面並輸出到客戶端。