使用显式参数调用函数 vs 使用call_user_func_array()函数

11

本周早些时候我看到了一段代码(不幸的是,我无法找回),我对作者实现__call()魔术方法的方式很感兴趣。 代码大致如下:

class Sample
{
    protected function test()
    {
        var_dump(func_get_args());
    }
    public function __call($func, $args)
    {
        if(!method_exists($this, $func))
        {
            return null;
        }
        switch(count($args))
        {
            case 0:
                return $this->$func();
            case 1:
                return $this->$func($args[0]);
            case 2:
                return $this->$func($args[0], $args[1]);
            case 3:
                return $this->$func($args[0], $args[1], $args[2]);
            case 4:
                return $this->$func($args[0], $args[1], $args[2], $args[3]);
            case 5:
                return $this->$func($args[0], $args[1], $args[2], $args[3], $args[4]);
            default:
                return call_user_func_array($this->$func, $args);
        }
    }
}
$obj = new Sample();
$obj->test("Hello World"); // Would be called via switch label 1
正如您所看到的,作者本可以只使用call_user_func_array()并且完全放弃switch语句,因此我认为这背后应该有(希望是)一些明智的原因。 我唯一能想到的原因可能是函数调用call_user_func_array()的一些开销,但那似乎不足以使用大量case语句。这里是否有我没有理解到的角度?

看起来很糟糕,但肯定有原因。 - Jan Dragsbaek
在我看来,这是相当愚蠢的哈哈,我不认为这段代码背后有任何合理的推理。 - jValdron
并非所有的代码都有存在的充分理由。 - Adam Wagner
使用call_user_func_array()时,传递参数引用存在问题...请参见https://dev59.com/vkrSa4cB1Zd3GeqPZtQ0 - ken
@ircmaxwell:请查看http://codepad.viper-7.com/Nflfai(php 5.2)和http://codepad.viper-7.com/qsrBeN(php 5.3)。 - ken
显示剩余5条评论
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
19
原因是在call_user_func_array上有额外的开销。它有一个额外函数调用的开销。通常情况下,这在微秒级别,但在两种情况下可能非常重要:
  1. 递归函数调用 由于它添加了一个调用到堆栈中,它将使堆栈使用量翻倍。因此,如果堆栈用完会导致应用程序崩溃(使用xdebug或内存约束)。在应用程序(或部分)中,使用这种风格的方法可以将堆栈使用量减少多达33%(这可能是应用程序运行和崩溃之间的区别)
  2. 性能 如果您经常调用该函数,则这些微秒可能会显著增加。由于这是一个框架(看起来像Lithium完成的某些操作),在应用程序的生命周期中可能会被调用数十次,数百次甚至数千次。因此,尽管每个单独的调用都是微观优化,但效果会显著累加。
因此,是的,您可以删除switch并将其替换为call_user_func_array,并且在功能方面100%相同。但是,您将失去上述两个优化好处。 编辑并证明性能差异: 我决定自己进行基准测试。下面是我使用的确切源代码链接:

http://codepad.viper-7.com/s32CSb(也包含在本答案底部供参考)

现在,我已经在Linux系统、Windows系统和codepad网站上进行了测试(2个命令行、1个在线和1个启用了XDebug),所有这些都运行在5.3.6或5.3.8上。

结论

由于结果相当长,我先概括一下。

如果你要频繁调用它,那么这不是微优化。当然,单个调用的差异微不足道。但如果要经常使用它,可以节省相当多的时间。

现在值得注意的是,除了一个测试以外,所有测试都没有启用XDebug。这非常重要,因为xdebug似乎会显著改变基准测试的结果。

以下是原始结果:

Linux

With 0 Arguments:
test1 in 0.0898239612579 Seconds
test2 in 0.0540208816528 Seconds
testObj1 in 0.118539094925 Seconds
testObj2 in 0.0492739677429 Seconds

With 1 Arguments:
test1 in 0.0997269153595 Seconds
test2 in 0.053689956665 Seconds
testObj1 in 0.137704849243 Seconds
testObj2 in 0.0436580181122 Seconds

With 2 Arguments:
test1 in 0.0883569717407 Seconds
test2 in 0.0551269054413 Seconds
testObj1 in 0.115921974182 Seconds
testObj2 in 0.0550417900085 Seconds

With 3 Arguments:
test1 in 0.0809321403503 Seconds
test2 in 0.0630970001221 Seconds
testObj1 in 0.124716043472 Seconds
testObj2 in 0.0640230178833 Seconds

With 4 Arguments:
test1 in 0.0859131813049 Seconds
test2 in 0.0723040103912 Seconds
testObj1 in 0.137611865997 Seconds
testObj2 in 0.0707349777222 Seconds

With 5 Arguments:
test1 in 0.109707832336 Seconds
test2 in 0.122457027435 Seconds
testObj1 in 0.201376914978 Seconds
testObj2 in 0.217674016953 Seconds

我实际上运行了它大约十几次,结果是一致的。因此,你可以清楚地看到,在该系统上,对于具有3个或更少参数的函数,使用switch语句明显更快。对于4个参数,它足够接近,可以作为微小优化。对于5个参数,它会变慢(由于switch语句的开销)。

现在,对象就是另一回事了。对于对象,即使有4个参数,使用switch语句也要快得多。而5个参数则稍微慢一些。

Windows

With 0 Arguments:
test1 in 0.078088998794556 Seconds
test2 in 0.040416955947876 Seconds
testObj1 in 0.092448949813843 Seconds
testObj2 in 0.044382095336914 Seconds

With 1 Arguments:
test1 in 0.084033012390137 Seconds
test2 in 0.049020051956177 Seconds
testObj1 in 0.098193168640137 Seconds
testObj2 in 0.055608987808228 Seconds

With 2 Arguments:
test1 in 0.092596054077148 Seconds
test2 in 0.059282064437866 Seconds
testObj1 in 0.10753011703491 Seconds
testObj2 in 0.06486701965332 Seconds

With 3 Arguments:
test1 in 0.10003399848938 Seconds
test2 in 0.073707103729248 Seconds
testObj1 in 0.11481595039368 Seconds
testObj2 in 0.072822093963623 Seconds

With 4 Arguments:
test1 in 0.10518193244934 Seconds
test2 in 0.076627969741821 Seconds
testObj1 in 0.1221661567688 Seconds
testObj2 in 0.080114841461182 Seconds

With 5 Arguments:
test1 in 0.11016392707825 Seconds
test2 in 0.14898705482483 Seconds
testObj1 in 0.13080286979675 Seconds
testObj2 in 0.15970706939697 Seconds

与Linux一样,在除了5个参数的情况下,它对于每种情况都更快(这是可以预料的)。所以这里没有什么不正常的。

Codepad

With 0 Arguments:
test1 in 0.094165086746216 Seconds
test2 in 0.046183824539185 Seconds
testObj1 in 0.088129043579102 Seconds
testObj2 in 0.046132802963257 Seconds

With 1 Arguments:
test1 in 0.093621969223022 Seconds
test2 in 0.054486036300659 Seconds
testObj1 in 0.11912703514099 Seconds
testObj2 in 0.053775072097778 Seconds

With 2 Arguments:
test1 in 0.099776029586792 Seconds
test2 in 0.072152853012085 Seconds
testObj1 in 0.10576200485229 Seconds
testObj2 in 0.065294027328491 Seconds

With 3 Arguments:
test1 in 0.11053204536438 Seconds
test2 in 0.088426113128662 Seconds
testObj1 in 0.11045718193054 Seconds
testObj2 in 0.073081970214844 Seconds

With 4 Arguments:
test1 in 0.11662006378174 Seconds
test2 in 0.085783958435059 Seconds
testObj1 in 0.11683893203735 Seconds
testObj2 in 0.081549882888794 Seconds

With 5 Arguments:
test1 in 0.12763905525208 Seconds
test2 in 0.15642619132996 Seconds
testObj1 in 0.12538290023804 Seconds
testObj2 in 0.16010403633118 Seconds

这显示了与 Linux 相同的图片。如果使用 4 个或更少的参数,通过 switch 运行它会显著提高速度。但是使用 5 个参数时,通过 switch 运行会显著降低速度。

使用 XDebug 的 Windows

With 0 Arguments:
test1 in 0.31674790382385 Seconds
test2 in 0.31161189079285 Seconds
testObj1 in 0.40747404098511 Seconds
testObj2 in 0.32526516914368 Seconds

With 1 Arguments:
test1 in 0.32827591896057 Seconds
test2 in 0.33025598526001 Seconds
testObj1 in 0.38013815879822 Seconds
testObj2 in 0.3494348526001 Seconds

With 2 Arguments:
test1 in 0.33168315887451 Seconds
test2 in 0.35207295417786 Seconds
testObj1 in 0.37523794174194 Seconds
testObj2 in 0.38242697715759 Seconds

With 3 Arguments:
test1 in 0.33901619911194 Seconds
test2 in 0.36867690086365 Seconds
testObj1 in 0.41470503807068 Seconds
testObj2 in 0.3860080242157 Seconds

With 4 Arguments:
test1 in 0.35170817375183 Seconds
test2 in 0.39288783073425 Seconds
testObj1 in 0.39424705505371 Seconds
testObj2 in 0.39747595787048 Seconds

With 5 Arguments:
test1 in 0.37077689170837 Seconds
test2 in 0.59246301651001 Seconds
testObj1 in 0.41220307350159 Seconds
testObj2 in 0.60260510444641 Seconds

现在这个故事有了不同的转折。在这种情况下,启用XDebug(但没有覆盖率分析,只是打开了扩展),使用switch优化几乎总是比较慢的。这很奇怪,因为许多基准测试都是在启用xdebug的开发环境中运行的。然而,生产环境通常不会运行xdebug。因此,这是一个纯粹的教训,告诉我们要在正确的环境中执行基准测试。

来源

<?php

function benchmark($callback, $iterations, $args) {
    $st = microtime(true);
    $callback($iterations, $args);
    $et = microtime(true);
    $time = $et - $st;
    return $time;
}

function test() {

}

function test1($iterations, $args) {
    $func = 'test';
    for ($i = 0; $i < $iterations; $i++) {
        call_user_func_array($func, $args);
    }
}

function test2($iterations, $args) {
    $func = 'test';
    for ($i = 0; $i < $iterations; $i++) {
        switch (count($args)) {
            case 0:
                $func();
                break;
            case 1:
                $func($args[0]);
                break;
            case 2:
                $func($args[0], $args[1]);
                break;
            case 3:
                $func($args[0], $args[1], $args[2]);
                break;
            case 4:
                $func($args[0], $args[1], $args[2], $args[3]);
                break;
            default:
                call_user_func_array($func, $args);
        }
    }
}

class Testing {

    public function test() {

    }

    public function test1($iterations, $args) {
        for ($i = 0; $i < $iterations; $i++) {
            call_user_func_array(array($this, 'test'), $args);
        }
    }

    public function test2($iterations, $args) {
        $func = 'test';
        for ($i = 0; $i < $iterations; $i++) {
            switch (count($args)) {
                case 0:
                    $this->$func();
                    break;
                case 1:
                    $this->$func($args[0]);
                    break;
                case 2:
                    $this->$func($args[0], $args[1]);
                    break;
                case 3:
                    $this->$func($args[0], $args[1], $args[2]);
                    break;
                case 4:
                    $this->$func($args[0], $args[1], $args[2], $args[3]);
                    break;
                default:
                    call_user_func_array(array($this, $func), $args);
            }
        }
    }

}

function testObj1($iterations, $args) {
    $obj = new Testing;
    $obj->test1($iterations, $args);
}

function testObj2($iterations, $args) {
    $obj = new Testing;
    $obj->test2($iterations, $args);
}

$iterations = 100000;

$results = array('test1' => array(), 'test2' => array(), 'testObj1' => array(), 'testObj2' => array());
foreach ($results as $callback => &$result) {
    $args = array();
    for ($i = 0; $i < 6; $i++) {
        $result[$i] = benchmark($callback, $iterations, $args);
        $args[] = 'abcdefghijklmnopqrstuvwxyz';
    }
}
unset($result);
$merged = array(0 => array(), 1 => array(), 2 => array(), 3 => array(), 4 => array());

foreach ($results as $callback => $result) {
    foreach ($result as $args => $time) {
        $merged[$args][$callback] = $time;
    }
}

foreach ($merged as $args => $matrix) {
    echo "With $args Arguments:<br />";
    foreach ($matrix as $callback => $time) {
        echo "$callback in $time Seconds<br />";
    }
    echo "<br />";
}

1
事实上,这种扩展是*不等价的。考虑一下如果你调用了call_user_func_array([$obj, 'test'], [&$obj])或者(在PHP 5.4之前)$obj->test(&$obj)。扩展将把传递的引用变成传递的值,而call_user_func_array将保留它。 - Artefacto
@Artefacto:但是Call Time Pass By Reference已经被弃用(并且会抛出警告)。所以没有它,实际上是一样的。我基于推荐用法来回答问题(不依赖废弃的功能)。 - ircmaxell
1
虽然 $obj->test(&$obj) 是 call-time 传递引用(在 PHP 5.4 中无法使用),但是 call_user_func_array([$obj, 'test'], [&$obj]) 在这种特定情况下不是,因为调用 __call 时规则会放宽(否则你将永远无法通过 __call 传递引用)。 - Artefacto
顺便提一下,如果您想确认,这个针对__call方法的放宽是在这里做出的:http://lxr.php.net/opengrok/xref/PHP_TRUNK/Zend/zend_execute_API.c#888 - Artefacto

1

您可以在phpsavant模板类中找到这个。PMJ得到了一个关于call_user_func*()速度慢的提示,并且发现前五个参数中有90%的工作可以更快地处理。其他任何内容都将以缓慢的方式处理。我找不到有关如何讨论的帖子,但这是他确定问题的页面。http://paul-m-jones.com/archives/182


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,