讀古今文學網 > 精通正則表達式(第3版) > 性能測試 >

性能測試

Benchmarking

本章主要講解速度和效率,而且會時常使用性能測試,所以我希望介紹一些測試的原則。我會用幾種語言來介紹簡單的測試方法。

基本的性能測試就是記錄程序運行的時間:先取系統時間,運行程序,再取系統時間,計算兩者的差,就是程序運行的時間。舉個例子,比較「^(a|b|c|d|e|f|g)+$」和「^[a-g]+$」。先來看Perl的表現,然後再來看其他語言。下面是簡單的Perl程序(不過,我們將會看到,這個例子有欠缺):

它看來(而且也確實是)很簡單,但是在進行性能測試時,我們需要記住幾點:

●只記錄「真正關心的(interesting)」處理時間。盡可能準確地記錄「處理」時間,盡可能避免「非處理時間」的影響。如果在開始前必須進行初始化或其他準備工作,請在它們完成之後開始計時;如果需要收尾工作,請在計時停止之後進行這些工作。

●進行「足夠多」的處理。通常,測試需要的時間是相當短暫的,而計算機時鐘的單位精度不夠,無法給出有意義的數值。

在我的機器上運行這個Perl程序,結果是:

Alternation takes 0.000 seconds.

Character class takes 0.000 seconds.

我們只能知道,這段程序所需的時間比計算機能夠測量的最短時間還要短。所以,如果程序運行的時間太短,就運行兩次、十次,甚至一千萬次,來保證「足夠多」的工作。這裡的「足夠多」取決於系統時鐘的精度,大多數系統能夠精確到1/100s,這樣,即使程序只需要0.5s,也能取得有意義的結果。

●進行「準確的」處理。進行1 000萬次快速操作需要在負責計時的代碼塊中升級1 000萬次計數器。如果可能,最好的辦法是增加真正的處理部分的比例,而不增加額外的開銷。在Perl的例子中,正則表達式應用的文本相當短:如果應用到長得多的字符串,在每次循環中所作的「真正的」處理也會多一些。

考慮到這些因素,我們可以得出下面的程序:

請注意,$TestString和$Count的初始化在計時開始之前($TestString使用了Perl提供的 x 操作符進行初始化,它表示將左邊的字符串重複右邊的次數)。在我的機器上,使用Perl5.8運行的結果是:

所以,對這個例子來說,多選結構要比字符組快22倍左右。此測試應該執行多次,選取最短的時間,以減少後台系統活動的影響。

理解測量對像

Know What You\'re Measuring

我們把初始化程序更改為下面這樣,會得到更有意思的結果:

現在,測試字符串只是上面的長度的1/1 000,而測試需要進行1 000次。每個正則表達式測試和匹配的字符總數並沒有變化,因此從理論上講,「工作量」應該沒有變化。不過,結果卻大不相同:

兩個時間都比之前的要長。原因是新增的「非處理」開銷——對$Count的檢測和更新,以及建立正則引擎的時間,現在的次數是以前的1 000倍。

對於字符組測試來說,新增的開銷花費了大約5s的時間,而多選結構則增加了將近10秒。為什麼多選結構測試的時間變化如此之大?主要是因為捕獲型括號(在每次測試之前和之後,它們都需要額外處理,這樣的操作要多1 000倍)。

無論如何,進行這點修改的要點在於說明,真正處理部分和非真正處理部分在計時中所佔的比重會強烈地影響到測試結果。

PHP測試

Benchmarking with PHP

下面是PHP的測試,使用preg引擎:

在我的機器上,結果是:

如果在測試中遇到PHP錯誤「not being safe to rely on the system\'s timezone settings」,請添加下面的代碼:

Java測試

Benchmarking with Java

因為某些原因,用Java測試很有講究。首先看個考慮不夠周到的例子,然後請思考它為什麼考慮不周到,應該如何改進:

你注意到在這個程序中正則表達式如何初始化部分編譯了嗎?我們需要測試的是匹配的速度,而不是編譯的速度。

速度取決於所使用的虛擬機(VM)。Sun的標準JRE有兩種虛擬機,client VM為快速啟動而優化,server VM為長時間、大負荷的作業而優化。

在我的機器上,使用client VM運行測試的結果如下:

使用server VM的結果如下:

這樣看來測試有點不可信了,之所以說它不夠周到,原因在於計時的結果在很大程度上取決於自動的預執行編譯器(automatic pre-execution compiler)的工作,或者說運行時編譯器(run-time compiler)與測試代碼的交互情況。某些虛擬機包含JIT(Just-In-Time compiler),JIT會根據需要,在需要執行代碼之前才進行編譯。

Java使用了我稱為BLTN(Better-Late-Than-Never)的編譯器,在執行期間計數,對反覆使用的代碼根據需要進行編譯和優化。BLTN的性質是,它只對認為「熱門」(hot,即大量使用)的代碼進行干預。如果虛擬機已經運行了一段時間,例如在服務器環境中,它已經「預熱」完畢,而我們的簡單例子確保了一台「涼」的服務器(BLTN沒有進行任何優化)。

可以把測試部分放入一個循環,來觀察「預熱」現象:

//第一輪測試計時...

如果新增的循環運行足夠長(例如,10s),BLTN就會優化熱門代碼,最後一次輸出的時間就代表了已預熱系統的情況。再次使用server VM,這些時間確實比之前有了8%和25%的提高。

另一個問題在於,負責調度GC線程的工作是不確定的。所以,進行足夠長時間的測試能夠降低這些不確定因素的影響。

VB.NET測試

Benchmarking with VB.NET

下面是VB.NET的測試程序:

在我的機器上,結果是:

在.NET Framework中使用RegexOptions.Compiled作為正則表達式構造函數的第2個參數,能夠把正則表達式編譯為效率更高的形式(☞410),其結果為:

使用Compiled功能之後,兩個測試的速度都有提高,但是多選結構的相對上升幅度更為明顯(幾乎是之前的3倍,而字符組的程序只提高到之前的1.5倍),所以多選結構從中獲益更大。

Ruby測試

Benchmarking with Ruby

下面是Ruby的測試代碼:

在我的機器上,結果如下:

Python測試

Benchmarking with Python

下面是Python的測試代碼:

因為Python的正則引擎設定的限制,我們必須減少字符串的長度,因為原來長度的字符串會導致內部錯誤(「maximum recursion limit exceeded」)。這種規定有點像減壓閥,它有助於終止無休止匹配。

作為彌補,我相應增加了測試的次數。在我的機器上,測試結果為:

Tcl測試

Benchmarking with Tcl

下面是Tcl的測試代碼:

在我的機器上,結果如下:

神奇的是,兩者速度相當。還記得嗎,我們在第145頁說過,Tcl使用的是NFA/DFA混合引擎,對DFA引擎來說,這兩個表達式是沒有區別的。本章所舉的大部分例子並不適用於Tcl,詳細信息請參考第243頁。