
曾经记得鸟哥 Laruence 提过不建议使用"魔术方法",自此之后一旦涉及使用魔术方法的地方,我都会下意识的想一下,这样写真的好吗?由于这一到两年来一直忙于工作和学习新的知识,所以在这道坎儿上一直没有做深入的探索一直恍恍惚惚过去了,今年是我进行深入学习的一年,因此现在必须对这个问题做个了结了。我们先来看看鸟哥 Laruence 博客曾经提到的:
当我把 PPT 分享给公司的同事的时候, 会有人质疑, 魔术方法都不让用? 优化的建议, 是建议, 是防止大家滥用, 肆无忌惮的用. 如果你能在写代码的时候, 能意识到, 什么慢, 什么快, 从而避免一些没有必要的对魔术方法的调用, 那就是这个优化建议所追求的效果了
面对我的疑惑,我的方案是:
目前个人能力有限,只能通过这种方式,如果你有更好的方案或者建议可以告诉我,谢谢, haha~
首先我们先来看看构造函数__construct 的实验, php 脚本如下:
<?php /** * 魔术方法性能探索 * * 构造函数 * * @author TIGERB <https://github.com/TIGERB> */ require('./function.php'); if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 构造函数使用类名 */ class ClassOne { public function classOne() { # code... } } /** * 构造函数使用魔术函数__construct */ class ClassTwo { public function __construct() { # code... } } $a = getmicrotime(); if ($is_use_magic === 'no_magic') { new ClassOne(); }else { new ClassTwo(); } $b = getmicrotime(); echo ($b-$a) . "\n"; // PHP5.6 中连续调用脚本 10000 次 sh test 10000 no_magic php5 construct // 运行数据统计脚本 sh analysis ./logs/__construct_no_magic_php5.log 10000 // 结果 avg: 34μm max: 483μm min: 26μm // PHP5.6 中连续调用脚本 10000 次 sh test 10000 magic php5 construct // 运行数据统计脚本 sh analysis ./logs/__construct_magic_php5.log 10000 // 结果 avg: 28μm max: 896μm min: 20μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 no_magic php construct // 运行数据统计脚本 sh analysis ./logs/__construct_no_magic_php.log 10000 // 结果 avg: 19μm max: 819μm min: 13μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 magic php construct // 运行数据统计脚本 sh analysis ./logs/__construct_magic_php.log 10000 // 结果 avg: 14μm max: 157μm min: 10μm 通过上面的数据我们可以看出:
使用__construct 作为构造函数的脚本执行的平均时间是要快于使用类名作为构造函数的,__大概快 5 到 6 微秒__,不论是在 php5.6 还是 php7.0 中。
接着,我们来看看__call 的实验, php 脚本如下:
<?php /** * 魔术方法性能探索 * * 构造函数 * * @author TIGERB <https://github.com/TIGERB> */ require('./function.php'); if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 构造函数使用类名 */ class ClassOne { public function __construct() { # code... } public function test() { # code... } } /** * 构造函数使用魔术函数__construct */ class ClassTwo { public function __construct() { # code... } public function __call($method, $argus) { # code... } } $a = getmicrotime(); if ($is_use_magic === 'no_magic') { $instance = new ClassOne(); $instance->test(); }else { $instance = new ClassTwo(); $instance->test(); } $b = getmicrotime(); echo ($b-$a) . "\n"; // PHP5.6 中连续调用脚本 10000 次 sh test 10000 no_magic php5 call // 运行数据统计脚本 sh analysis ./logs/__call_no_magic_php5.log 10000 // 结果 avg: 27μm max: 206μm min: 20μm // PHP5.6 中连续调用脚本 10000 次 sh test 10000 magic php5 call // 运行数据统计脚本 sh analysis ./logs/__call_magic_php5.log 10000 // 结果 avg: 29μm max: 392μm min: 22μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 no_magic php call // 运行数据统计脚本 sh analysis ./logs/__call_no_magic_php.log 10000 // 结果 avg: 16μm max: 256μm min: 10μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 magic php call // 运行数据统计脚本 sh analysis ./logs/__call_magic_php.log 10000 // 结果 avg: 18μm max: 2459μm min: 11μm 通过上面的数据我们可以看出:
使用__call 的脚本执行的平均时间是要慢于不使用,__大概慢 2 微秒__,不论是在 php5.6 还是 php7.0 中。
接着,我们来看看__callStatic 的实验, php 脚本如下:
<?php /** * 魔术方法性能探索 * * 静态重载函数 * * @author TIGERB <https://github.com/TIGERB> */ require('./function.php'); if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 存在 test 静态方法 */ class ClassOne { public function __construct() { # code... } public static function test() { # code... } } /** * 使用重载实现 test */ class ClassTwo { public function __construct() { # code... } public static function __callStatic($method, $argus) { # code... } } $a = getmicrotime(); if ($is_use_magic === 'no_magic') { ClassOne::test(); }else { ClassTwo::test(); } $b = getmicrotime(); echo ($b-$a) . "\n"; // PHP5.6 中连续调用脚本 10000 次 sh test 10000 no_magic php5 callStatic // 运行数据统计脚本 sh analysis ./logs/__callStatic_no_magic_php5.log 10000 // 结果 avg: 25μm max: 129μm min: 19μm // PHP5.6 中连续调用脚本 10000 次 sh test 10000 magic php5 callStatic // 运行数据统计脚本 sh analysis ./logs/__callStatic_magic_php5.log 10000 // 结果 avg: 28μm max: 580μm min: 20μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 no_magic php callStatic // 运行数据统计脚本 sh analysis ./logs/__callStatic_no_magic_php.log 10000 // 结果 avg: 14μm max: 130μm min: 9μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 magic php callStatic // 运行数据统计脚本 sh analysis ./logs/__callStatic_magic_php.log 10000 // 结果 avg: 14μm max: 159μm min: 10μm 通过上面的数据我们可以看出:
在 php5.6 中使用__callStatic 的脚本执行的平均时间是要慢于不使用,__大概慢 3 微秒__;在 php7.0 中使用__callStatic 的脚本执行的平均时间是要大致等于不使用__callStatic 的;
接着,我们来看看__set 的实验, php 脚本如下:
<?php /** * 魔术方法性能探索 * * 设置私有属性__set * * @author TIGERB <https://github.com/TIGERB> */ require('./function.php'); if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 实现公共方法设置私有属性 */ class ClassOne { /** * 私有属性 * * @var string */ private $someVariable = 'private'; public function __construct() { # code... } public function setSomeVariable($value = '') { $this->someVariable = $value; } } /** * 使用_set 设置私有属性 */ class ClassTwo { /** * 私有属性 * * @var string */ private $someVariable = 'private'; public function __construct() { # code... } public function __set($name = '', $value = '') { $this->$name = $value; } } $a = getmicrotime(); if ($is_use_magic === 'no_magic') { $instance = new ClassOne(); $instance->setSomeVariable('public'); }else { $instance = new ClassTwo(); $instance-&g;someVariable = 'public'; } $b = getmicrotime(); echo ($b-$a) . "\n"; // PHP5.6 中连续调用脚本 10000 次 sh test 10000 no_magic php5 set // 运行数据统计脚本 sh analysis ./logs/__set_no_magic_php5.log 10000 // 结果 avg: 31μm max: 110μm min: 24μm // PHP5.6 中连续调用脚本 10000 次 sh test 10000 magic php5 set // 运行数据统计脚本 sh analysis ./logs/__set_magic_php5.log 10000 // 结果 avg: 33μm max: 138μm min: 25μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 no_magic php set // 运行数据统计脚本 sh analysis ./logs/__set_no_magic_php.log 10000 // 结果 avg: 15μm max: 441μm min: 11μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 magic php set // 运行数据统计脚本 sh analysis ./logs/__set_magic_php.log 10000 // 结果 avg: 17μm max: 120μm min: 11μm 通过上面的数据我们可以看出:
使用__set 的脚本执行的平均时间是要慢于不使用,__大概慢 2 微秒__,不论是在 php5.6 还是 php7.0 中。
接着,我们来看看__get 的实验, php 脚本如下:
<?php /** * 魔术方法性能探索 * * 读取私有属性__get * * @author TIGERB <https://github.com/TIGERB> */ require('./function.php'); if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 实现公共方法获取私有属性 */ class ClassOne { /** * 私有属性 * * @var string */ private $someVariable = 'private'; public function __construct() { # code... } public function getSomeVariable() { return $this->someVariable; } } /** * 使用_get 获取私有属性 */ class ClassTwo { /** * 私有属性 * * @var string */ private $someVariable = 'private'; public function __construct() { # code... } public function __get($name = '') { return $this->$name; } } $a = getmicrotime(); if ($is_use_magic === 'no_magic') { $instance = new ClassOne(); $instance->getSomeVariable(); }else { $instance = new ClassTwo(); $instance->someVariable; } $b = getmicrotime(); echo ($b-$a) . "\n"; // PHP5.6 中连续调用脚本 10000 次 sh test 10000 no_magic php5 get // 运行数据统计脚本 sh analysis ./logs/__get_no_magic_php5.log 10000 // 结果 avg: 28μm max: 590μm min: 20μm // PHP5.6 中连续调用脚本 10000 次 sh test 10000 magic php5 get // 运行数据统计脚本 sh analysis ./logs/__get_magic_php5.log 10000 // 结果 avg: 28μm max: 211μm min: 22μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 no_magic php get // 运行数据统计脚本 sh analysis ./logs/__get_no_magic_php.log 10000 // 结果 avg: 16μm max: 295μm min: 10μm // PHP7.0 中连续调用脚本 10000 次 sh test 10000 magic php get // 运行数据统计脚本 sh analysis ./logs/__get_magic_php.log 10000 // 结果 avg: 19μm max: 525μm min: 12μm 通过上面的数据我们可以看出:
在 php5.6 中使用__get 的脚本执行的平均时间是要大致等于不使用__get 的;在 php7.0 中使用__get 的脚本执行的平均时间是要慢于不使用,__大概慢 3 微秒__。
这里主要测试了__construct(), __call(), __callStatic(), __get(), __set()这五个常用的且可有其他实现方式代替的魔法函数。通过上面的测试再回来解答我的疑惑
- 魔术方法真的性能比较差吗?
答:除了使用__construct 之外,这里使用其他的魔法方法的时间大致慢 10 微妙以内。
- PHP7 里使用魔术方法的性能还是存在问题吗?
答:在 PHP7 中使用与不使用魔术方法之间的差异和在 PHP5.6 中近乎一致。
- 我们应该如何合理的使用魔术方法?
答:通过整个测试我们可以看出使不使用魔法方法这之间的执行时间差异大致都是在 10 微妙以内的,所以如果魔法方法可以很好的节省我们的开发成本和优化我们的代码结构,我们应该可以考虑牺牲掉这不到 10 微妙。而__construct 是要快的,所以使用__construct 应该没什么异议。
https://github.com/TIGERB/easy-tips/tree/master/php/magic-function
单位错误:纠正μm 为 μs
文字错误:微妙为微秒
函数使用不当:microtime()支持float 传参 true
1 fatjiong 2017-03-05 23:49:43 +08:00 支持分享。 |
3 gouchaoer 2017-03-06 00:01:57 +08:00 via Android 很不错的尝试,不过你测试方法有点问题,我们知道 php-cli 启动后会做很多准备工作,实际上测试逻辑占了很少时间。你可以在 php 内部 loop 很多次 |
5 xiaoshangmin 2017-03-06 01:22:49 +08:00 via iPhone 厉害了我滴哥 |
6 Sunyanzi 2017-03-06 01:29:32 +08:00 脚本欠严谨 ... 不过还是赞这个测试的态度 ... 顺便指个问题 ... 微秒是 μs 不是 μm ... 另外 microtime 支持传入一个参数你可以看看 ... |
7 fuxkcsdn 2017-03-06 09:01:25 +08:00 via iPhone microtime(true), php 内部循环就好 p.s.实际上想当然也是魔术方法慢,用魔术方法肯定会多几个判断的 |
8 cenxun 2017-03-06 09:05:39 +08:00 赞一个,有图表就更溜了 ~~ |
9 wyntau 2017-03-06 09:38:24 +08:00 厉害了~ |
10 vus520 2017-03-06 10:06:26 +08:00 666 |
13 Felldeadbird 2017-03-06 10:08:10 +08:00 支持楼主。不过除了执行速度问题外,不推荐用魔术方法主要是 由于 魔术方法容易改变 类中属性,或者说 埋下一些 很难调试的坑。 不过呢,也不是说不能用。合理使用就没问题了。 |
16 TIGERB OP @Felldeadbird thx~ 我的一些愚见,哈哈:如果完全符合面向对象的编程,类内部的属性是不应该直接暴露出去的,要符合闭原则,但是往往我们的实际编程中是有这个需求的,我们通常的做法可以: 1. 设置属性为 public => 不符合面向对象设计原则 2. 实现一个修改私有属性的 public 方法 => 不够统一 但是__get,__set 其实就是实现统一可控 |
17 peinhu 2017-03-06 11:40:07 +08:00 技术好文要顶!但是有个疑问,为什么 max 和 min 相差这么多?是不是你开启了 opcache 的缘故,测试前做个预热比较好。 |
19 baizhebz 2017-03-06 12:13:25 +08:00 Laravel 里有不少对__call()的应用。 |
21 iminto 2017-03-06 21:16:53 +08:00 瞎扯淡。。。 快 5 到 6 微秒。。。咋不说快 5 到 6 纳秒,皮秒呢 |
22 TIGERB OP @iminto 只是因为鸟哥曾对使用魔法方法的建议,我自己想看看这之间真正的差异而已,结果快了慢了多少都不是目的,了结了我曾经心头的疑虑才是目的。 |
23 iminto 2017-03-07 16:27:24 +08:00 @TIGERB 几微秒的差距,有误差和预热的存在,你的测试结果没有意义。还有某鸟这句话是 10 年前说的吧,你现在来测这个问题也没有意义。 当然,你就是只是了结曾经心头的疑虑。 |
25 gouchaoer 2017-03-08 11:30:12 +08:00 这个文章发的到处都是了, 3L 提的那个你怎么还没改,我根据你的代码测出来效果刚好相反, magic 方法比一般方法慢一倍: ``` <?php /** * 魔术方法性能探索 * * 构造函数 * * @author TIGERB <https://github.com/TIGERB> */ if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 构造函数使用类名 */ class ClassOne { public function __construct() { # code... } public function test() { $i=0; $i++; $arr=[]; $arr['test']=1; # code... } } /** * 构造函数使用魔术函数__construct */ class ClassTwo { public function __construct() { # code... } public function __call($method, $argus) { $i=0; $i++; $arr=[]; $arr['test']=1; # code... } } $a = microtime(true); if ($is_use_magic === 'no_magic') { echo "no_magic\n"; $instance = new ClassOne(); for($i=0;$i<10000000;$i++) $instance->test(); }else { echo "magic\n"; $instance = new ClassTwo(); for($i=0;$i<10000000;$i++) $instance->test(); } $b = microtime(true); echo ($b-$a) . "\n"; ``` [root@iZ239ui2iq8Z luminati-proxy-solution]# php56 test.php no_magic no_magic 3.8641951084137 [root@iZ239ui2iq8Z luminati-proxy-solution]# php56 test.php magic magic 7.3454520702362 [root@iZ239ui2iq8Z luminati-proxy-solution]# php70 test.php no_magic no_magic 1.3668580055237 [root@iZ239ui2iq8Z luminati-proxy-solution]# php70 test.php magic magic 2.172700881958 |
26 gouchaoer 2017-03-08 11:36:03 +08:00 构造函数方法测出来和你的结论是一致的: ``` <?php /** * 魔术方法性能探索 * * 构造函数 * * @author TIGERB <https://github.com/TIGERB>; */ if (!isset($argv[1])) { die('error: variable is_use_magic is empty'); } $is_use_magic = $argv[1]; /** * 构造函数使用类名 */ class ClassOne { public function classOne() { # code... } } /** * 构造函数使用魔术函数__construct */ class ClassTwo { public function __construct() { # code... } } $a = microtime(true); if ($is_use_magic === 'no_magic') { echo "no_magic\n"; for($i=0;$i<10000000;$i++) $instance = new ClassOne(); }else { echo "magic\n"; for($i=0;$i<10000000;$i++) $instance = new ClassTwo(); } $b = microtime(true); echo ($b-$a) . "\n"; ``` [root@iZ239ui2iq8Z luminati-proxy-solution]# php56 test2.php no_magic no_magic 2.2117800712585 [root@iZ239ui2iq8Z luminati-proxy-solution]# php56 test2.php magic magic 2.1968791484833 [root@iZ239ui2iq8Z luminati-proxy-solution]# php70 test2.php no_magic no_magic 1.2584509849548 [root@iZ239ui2iq8Z luminati-proxy-solution]# php70 test2.php magic magic 1.2768771648407 |