PHP高級編程-迴歸原生態-數組排序陷阱

4.2.3 排序陷阱

即便記得住這些排序函數,並不代表就能正確使用。因爲,在實際項目開發中,會發現有很多開發工程師會不加思索就使用他們一直熟悉的某個排序函數,而不考慮具體的業務場景以及所運行的上下文環境。包括我在內,曾經也犯了這樣的錯。尤其在大型企業級系統中,任何一個小污點,在大數據、高併發下都會被放得更大。下面舉一個具體的例子來說明這一點。


除了sort()函數外,大家平時使用比較多的該數usort()函數了,因爲它可以允許你自定義排序規則。自然而然,對於下面這個場景,需要對每個學生的考試分數從高到低進行排名時,出於慣性,就會很自然繼續使用usort()函數。簡短的實現代碼是:

<?php// 學生數組$students = array(    array('name' => '張三', 'point' => 76),    array('name' => '李四', 'point' => 98),    array('name' => '小明', 'point' => 95),    array('name' => '小紅', 'point' => 83),    array('name' => '阿布', 'point' => 88),);// 按分數從高到低排序usort($students, function($left, $right) {    if ($left['point'] == $right['point']) {        return 0;    }
return $left['point'] > $right['point'] ? -1 : 1;});// 輸出print_r($students);

上面代碼運行後,能得到正確的排序結果。看起來沒什麼問題,對吧?是的,確實看起來沒什麼問題。因爲這裏只有5個學生。但是,假設開發的系統是大型的系統,假設要排序的學生有廣東省的全部高考學生,以2017年爲例,廣東省高考人數大概是75.7萬,這時性能又會如何?


讓我們簡單來做一個模擬的小實驗。用事實結合xhprof性能分析工具得出的數據來說話。


先稍微加以改裝,在進行排序的前後加上xhprof的性能分析代碼。

// 開始xhprof性能分析xhprof_enable();
usort($students, function($left, $right) { if ($left['point'] == $right['point']) { return 0; }
return $left['point'] > $right['point'] ? -1 : 1;});// 結束xhprof性能分析$xhprof_data = xhprof_disable();

關於xhprof的使用,網上已經有很多資料說明,這裏不再詳細展開。最終,我們可以看到這樣的性能分析報告:

圖4-1 對5個學生使用usort()排序


接下來,我們可以把學生的數量加大一點。先增加到萬的級別,通過指數爆炸很容易做到這一點,以上面5個學生爲基礎,通過對這5個學生的數據進行11次翻倍後,就可以得到10240組數據。把下面造數據的代碼放在$students變量聲明後即可。

// 5 * (2 ^ 11) = 10240for ($i = 0; $i < 11;  $i ++) {    $students = array_merge($students, $students);}

再來看一下,這時代碼運行後的性能分析報告是怎樣的。

圖4-2 對一萬多個學生使用usort()排序


我們暫時先不來對比這些數據,繼續完成最後一批排序——模擬近70萬學生的成績排序。繼續把上面循環的次數從原來11次加大到17次,就可以得到655360組數據。這時,你會發現頁面響應已經明顯變慢了。在我測試時,基本使用了13秒。最終的xhprof性能分析報告如下:

圖4-3 第三組分析,對65萬多個學生使用usort()排序


最後,通過這三組數據,我們來做個彙總和比較,就能明顯發現問題所在了。對比幾個關鍵的性能指標:執行時間、內存使用情況和函數調用次數,可以得到以下表格:


表4-3 不同排序函數的性能比較

 

學生數量

執行時間

使用內存

函數調用次數

第一組

5個學生

82毫秒

約6KB

10次

第二組

10,240個學生

149毫秒

約6KB

119,234次

第三組

655,360個學生

約12.9秒

約6KB

11,575,795次


可以發現,隨着學生數量的增加,執行時間也明顯相應變大。特別對於函數調用次數,增長的幅度更爲明顯,並且是絕對值。即不管是什麼配置的服務器,函數調用次數都是不變的。那麼對於第三組,執行了接近12.9秒,時間都到哪裏去了呢?


學過操作系統的同學都知道,每次函數的調用都存在上下文切換的開銷,頻繁的函數調用,會產生頻繁的堆棧操作。這也是爲什麼C/C++語言會支持內聯函數。再來看一下第三組的調用鏈就能知道大部分時間,接近95.9%都花在了函數的調用上。剩下的4.1%時間則花在了自定義函數本身的執行上。

圖4-4 第三組的調用鏈


既然usort()函數存在性能問題,那麼應該改用哪個排序函數更合適、更優呢?還記得我們前面學過的array_multisort()函數嗎?來看下它的效果怎樣。根據前面所學的知識,不難把實現改成:

// 開始xhprof性能分析xhprof_enable(XHPROF_FLAGS_MEMORY + XHPROF_FLAGS_CPU);
$points = array();foreach ($students as $it) { $points[] = $it['point'];}
array_multisort($points, SORT_DESC, SORT_NUMERIC, $students);// 結束xhprof性能分析$xhprof_data = xhprof_disable();

代碼改好後,再來看一下xhprof的性能分析報告。有沒發現,執行時間已經降爲只有約2.7秒了!比原來的12.9秒,速度上提升了79%!並且函數調用次數僅有3次!但也不要開心太早,因爲array_multisort()函數會消耗更多的內存。這正是典型的空間換時間的做法。但作爲真實的終端用戶,他不會關心我們的服務器使用了多少內存,他只關心打開的網站是否順暢,能不能在更短的時間內瀏覽到他感興趣的商品。如果不行,他就會離我們而去。


圖4-5 對65萬多個學生改用array_multisort()排序


通過上面的數據對比,以及和改進後的方案對比,不難總結出,對於同一個函數,不同級別的數據量,其需要的執行時間是大有不同的。選擇合適的底層函數,對項目、對系統、對用戶都是非常有益的。簡單來說,在大型系統開發中,要慎用usort()函數。最後,爲了方便大家查看,貼一下最終完整的代碼。

<?php// 學生數組$students = array(    array('name' => '張三', 'point' => 76),    array('name' => '李四', 'point' => 98),    array('name' => '小明', 'point' => 95),    array('name' => '小紅', 'point' => 83),    array('name' => '阿布', 'point' => 88),);// 製造更多的學生數據// 5 * (2 ^ 11) = 10240// 5 * (2 ^ 16) = 655360for ($i = 0; $i < 17; $i ++) {    $students = array_merge($students, $students);}// 開始xhprof性能分析xhprof_enable(XHPROF_FLAGS_MEMORY + XHPROF_FLAGS_CPU);//usort($students, function($left, $right) {//    if ($left['point'] == $right['point']) {//        return 0;//    }////    return $left['point'] > $right['point'] ? -1 : 1;//});
$points = array();foreach ($students as $it) { $points[] = $it['point'];}
array_multisort($points, SORT_DESC, SORT_NUMERIC, $students);// 結束xhprof性能分析$xhprof_data = xhprof_disable();// print_r($students);
$XHPROF_ROOT = realpath(dirname(__FILE__));include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php";include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php";// save raw data for this profiler run using default// implementation of iXHProfRuns.$xhprof_runs = new XHProfRuns_Default();// save the run under a namespace "xhprof_foo"$run_id = $xhprof_runs->save_run($xhprof_data, "xhprof_foo");
echo "http://localhost/?run=$run_id&source=xhprof_foo\n";

也許還會存在其他的排序陷阱,大家可以在實際項目開發中,多加留意,平時多總結。接下來,繼續討論關於數組更多高級的用法。


本文分享自微信公衆號 - 小白開放平臺(yesapi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章