PHP 魔法函数性能分析

2017-03-05 23:41:25 +08:00
 TIGERB

前言

曾经记得鸟哥 Laruence 提过不建议使用"魔术方法",自此之后一旦涉及使用魔术方法的地方,我都会下意识的想一下,这样写真的好吗?由于这一到两年来一直忙于工作和学习新的知识,所以在这道坎儿上一直没有做深入的探索一直恍恍惚惚过去了,今年是我进行深入学习的一年,因此现在必须对这个问题做个了结了。我们先来看看鸟哥 Laruence 博客曾经提到的:

当我把 PPT 分享给公司的同事的时候, 会有人质疑, 魔术方法都不让用? 优化的建议, 是建议, 是防止大家滥用, 肆无忌惮的用. 如果你能在写代码的时候, 能意识到, 什么慢, 什么快, 从而避免一些没有必要的对魔术方法的调用, 那就是这个优化建议所追求的效果了

疑惑

  1. 魔术方法真的性能比较差吗?
  2. PHP7 里使用魔术方法的性能还是存在问题吗?
  3. 我们应该如何合理的使用魔术方法?

方案

面对我的疑惑,我的方案是:

目前个人能力有限,只能通过这种方式,如果你有更好的方案或者建议可以告诉我,谢谢, haha~

测试

__construct

首先我们先来看看构造函数__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

接着,我们来看看__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

接着,我们来看看__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

接着,我们来看看__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->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

接着,我们来看看__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()这五个常用的且可有其他实现方式代替的魔法函数。通过上面的测试再回来解答我的疑惑

  1. 魔术方法真的性能比较差吗?

答:除了使用__construct 之外,这里使用其他的魔法方法的时间大致慢 10 微妙以内。

  1. PHP7 里使用魔术方法的性能还是存在问题吗?

答:在 PHP7 中使用与不使用魔术方法之间的差异和在 PHP5.6 中近乎一致。

  1. 我们应该如何合理的使用魔术方法?

答:通过整个测试我们可以看出使不使用魔法方法这之间的执行时间差异大致都是在 10 微妙以内的,所以如果魔法方法可以很好的节省我们的开发成本和优化我们的代码结构,我们应该可以考虑牺牲掉这不到 10 微妙。而__construct 是要快的,所以使用__construct 应该没什么异议。

脚本源码

https://github.com/TIGERB/easy-tips/tree/master/php/magic-function

3562 次点击
所在节点    程序员
27 条回复
iminto
2017-03-06 21:16:53 +08:00
瞎扯淡。。。
快 5 到 6 微秒。。。咋不说快 5 到 6 纳秒,皮秒呢
TIGERB
2017-03-06 23:02:03 +08:00
@iminto 只是因为鸟哥曾对使用魔法方法的建议,我自己想看看这之间真正的差异而已,结果快了慢了多少都不是目的,了结了我曾经心头的疑虑才是目的。
iminto
2017-03-07 16:27:24 +08:00
@TIGERB 几微秒的差距,有误差和预热的存在,你的测试结果没有意义。还有某鸟这句话是 10 年前说的吧,你现在来测这个问题也没有意义。
当然,你就是只是了结曾经心头的疑虑。
TIGERB
2017-03-07 17:15:33 +08:00
@iminto 是啊~
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
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
TIGERB
2017-03-08 11:42:18 +08:00
@gouchaoer 我求的是使用不使用之间的时间差值,并且跑了 10000 次,加载 php-cli 时间抖动的应该是影响不大了吧, php 内部 loop 我之前试过发现有些不对劲,我再试试。 thx~

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/345170

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX