PHP高级编程-回归原生态-数组排序陷阱

原创
02/16 10:00
阅读数 0

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)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部