Perl 是一門非常靈活的語言,然而,其易用特性會使程序員滋生出一種懶散的編程習慣。我們應該對這些壞習慣負責,同時可以采取一些快捷步驟來提高 Perl 應用程序的性能。在本文中,我們將介紹優化的一些關鍵內容,了解哪些解決方案有效、哪些無效,以及如何繼續構建並擴展設計時就考慮到優化和速度的應用程序。
拙劣的性能源自草率的編程 坦率地說,我喜歡 Perl,而且到處使用 Perl。我已經使用 Perl 開發了很多 Web 站點,編寫了很多管理腳本,並編寫了一些游戲。通常使用 Perl 是為了節省時間,並為我自動檢查一些信息:從彩票號碼到股票價格,我甚至使用 Perl 來自動編寫郵件。由於使用 Perl 讓一切都變得如此簡單,因此很容易忘記對其進行優化。許多情況下,這並不是世界末日。因此多花幾個毫秒來查詢股票價格或處理日志文件又有什麼關系呢? 然而,這些相同的懶惰習慣在小程序中可能只是多花費幾毫秒的時間,但是在大規模開發項目中,多耗費的時間就變成數倍了。這就是 Perl 的 TMTOWTDI (There's More Than One Way To Do It) 頌歌開始變壞的地方。如果您需要很快的速度,不管有多少種慢速的方法,但是可能只有一兩種方法可以達到最快的結果。最終,即使您可以得到預期的結果,但草率的編程還是會導致拙劣的性能。因此,在本文中,我們將介紹一些可以用來取消 Perl 應用程序額外執行周期的關鍵技術。
優化方法 首先,有必要隨時記住 Perl 是一門編譯語言程序。您所編寫的源代碼是轉換為執行的字節碼時進行編譯的。字節碼本身就有一個指令范圍,所有的指令都是使用高度優化的 C 語言編寫的。然而,即使在這些指令中,有些操作仍然可以進行優化,得到相同的結果,但是執行的效率更高。總體來講,這意味著您要使用邏輯序列與字節碼的組合,後者是從前者中生成的,最終會影響性能。某些相似操作之間性能的差距可能非常巨大。現在讓我們考慮清單 1 和清單 2 中的代碼。這兩段代碼都是將兩個字符串連接為一個字符串,一個是通過普通的連接方法實現,而另外一個是通過生成一個數組並使用 join 方法進行連接。 清單 1. 連接字符串,版本 1 my $string = 'abcdefghijklmnopqrstuvwxyz'; my $concat = ''; foreach my $count (1..999999) { $concat .= $string; } 清單 2. 連接字符串,版本 2 my $string = 'abcdefghijklmnopqrstuvwxyz'; my @concat; foreach my $count (1..999999) { push @concat,$string; } my $concat = join('',@concat); 執行清單 1 需要 1.765 秒,而執行清單 2 則需要 5.244 秒。這兩段代碼都生成一個字符串,那麼是什麼操作耗費了這麼多時間呢?傳統上講(包括 Perl 開發組),我們都認為連接字符串是一個非常耗時的過程,因為我們需要為變量擴展內存,然後將字符串及新添加的內容復制到新的變量中。另一方面,向一個數組中添加一個字符串應該非常簡單。我們還添加了使用 join() 復制連接字符串的問題,這會額外增加 1 秒的時間。 這種情況下的問題在於,將字符串 push() 到一個字符串中非常耗時;首先,我們要執行一次函數調用(這會涉及壓棧和出棧操作),還要添加額外的數組管理工作。相反,連接字符串操作非常簡單,只是運行一個操作碼,將一個字符串變量附加到一個現有的字符串變量中即可。即使設置數組的大小來減少其他工作的負載(使用 $#concat = 999999),也只能節省 1 秒鐘的時間。 上面這個例子是非常極端的一個例子,在使用數組時,速度可以比使用字符串快數倍;如果需要重用一個特定的序列,但要使用不同的次序或不同的空格字符,那麼這就是很好的一個例子。當然,如果想重新排列序列的內容,那麼數組也非常有用。順便說一下,在這個例子中,產生一個重復 999,999 次字符的字符串的更簡便方法是: $concat = 999999 x 'abcdefghijklmnopqrstuvwxyz'; 這裡介紹的很多技術單獨使用都不會引起多大的差異,但是當您在應用程序中組合使用這些技術時,就可以在 Perl 應用程序中節省幾百毫秒的時間,甚至是幾秒的時間。
使用引用 如果使用大型數組或 hash 表,並使用它們作為函數的參數,那麼應該使用它們的一個引用,而不應該直接使用它們。通過使用引用,可以告訴函數指向信息的指針。如果不使用引用,就需要將整個數組或 hash 表復制到該函數的調用棧中,然後在函數中再次對其進行復制。引用還可以節省內存(這可以減少足跡和管理的負載),並簡化您的編程。
字符串處理 如果在程序中使用了大量的靜態字符串,例如,在 Web 應用程序中,那麼就要記得使用單引號,而不是使用雙引號。雙引號會強制 Perl 檢查可能插入的信息,這會增加打印字符串的負載: print 'A string','another string',"\n"; 我使用逗號來分隔參數,而不是使用句號將這些字符串連接在一起。這樣可以簡化處理過程;print 只是簡單地向輸出文件發送一個參數。連接操作會首先將字符串連接在一起,然後將其作為一個參數打印。
循環 正如您已經看到的一樣,帶有參數的函數調用的開銷很高,因為要想讓函數調用正常工作,Perl 只能將這些參數壓入調用堆棧之後,再調用函數,然後從堆棧中再次接收響應。所有這些操作都需要盡避免我們不需要的負載和處理操作。由於這個原因,在一個循環中使用太多函數調用不是個好主意。同樣,這減少了比較的次數。循環 1,000 次並向函數傳遞信息會導致調用該函數 1,000 次。要解決這個問題,只需要調整一下代碼的順序即可。我們不使用 清單 3 的格式,而是使用清單 4 中的格式。 清單 3. 循環調用函數 foreach my $item (keys %{$values}) { $values->{$item}->{result} = calculate($values->{$item}); } sub calculate { my ($item) = @_; return ($item->{adda}+$item->{addb}); } 清單 4. 函數使用循環 calculate_list($values); sub calculate_list { my ($list) = @_; foreach my $item (keys %{$values}) { $values->{$item}->{result} = ($item->{adda}+$item->{addb}); } } 更好的方式是在這種簡單的計算中或者在簡單的循環中使用 map: map { $values->{$_}->{result} = $values->{$_}->{adda}+$values->{$_}->{addb} } keys %{$values}; 還要記住的是,在循環中,每次反復都是在浪費時間,因此不要多次使用相同的循環,而是要盡量在一個循環中執行所有的操作。 排序 另外一種有關循環的通用操作是排序,特別是對 hash 表中的鍵值進行排序。在這個例子中嵌入對列表元素進行排序的操作是非常誘人的,如清單 5 所示。 清單 5. 不好的排序 my @marksorted = sort {sprintf('%s%s%s', $marked_items->{$b}->{'upddate'}, $marked_items->{$b}->{'updtime'}, $marked_items->{$a}->{itemid}) sprintf('%s%s%s', $marked_items->{$a}->{'upddate'}, $marked_items->{$a}->{'updtime'}, $marked_items->{$a}->{itemid}) } keys %{$marked_items}; 這是一個典型的復雜數據排序操作,在該例中,要對日期、時間和 ID 號進行排序,這是通過將數字連接在一個數字上,然後對其進行數字排序實現的。問題是排序操作要遍歷列表元素,並根據比較操作上下移動列表。這是一種類型的排序,但是與我們已經看到的排序的例子不同,它對每次比較操作都調用 sprintf。每次循環至少執行兩次,遍歷列表需要執行的精確循環次數取決於列表最初排序的情況。例如,對於一個 10,000 個元素的列表來說,您可能會調用 sprintf 超過 240,000 次。 解決方案是創建一個包含排序信息的列表,並只生成一次排序域信息。參考清單 5 中的例子,我將這段代碼改寫為清單 6 的代碼。 清單 6. 較好的排序 map { $marked_items->{$_}->{sort} = sprintf('%s%s%s', $marked_items->{$_}->{'upddate'}, $marked_items->{$_}->{'updtime'}, $marked_items->{$_}->{itemid}) } keys %{$marked_items}; my @marksorted = sort { $marked_items->{$b}->{sort} $marked_items->{$a}->{sort} } keys %{$marked_items}; 現在不需要每次都調用 sprintf,對 hash 表中的每一項,只需要調用一次該函數,就可以在 hash 表中生成一個排序字段,然後在排序時就可以使用這個排序字段了。排序操作只能訪問排序字段的值。您可以將對包含 10,000 個元素的 hash 表的調用從 240,000 次減少到 10,000 次。這取決於最初對排序部分執行的操作,但是如果使用清單 6 中的方法,則可能節省一半的時間。 如果使用從數據庫(例如 mysql 或類似的數據庫)查詢的結果來構建 hash 表,並在查詢中使用使用排序操作,然後按照這個順序來構建 hash 表,那麼就無需再次遍歷這些信息來進行排序。
使用簡短的邏輯 與排序相關的是如何遍歷可選值列表。使用很多 if 語句耗費的時間可能會令人難以置信。例如,請參閱清單 7 中的代碼。 清單 7. 進行選擇 if ($userchoice > 0) { $realchoice = $userchoice; }