Вызов функции с явными параметрами vs. call_user_func_array()

Я видел фрагмент кода на этой неделе (что, к сожалению, я не могу получить), и мне любопытно, как автор начал реализовывать магический метод __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() и полностью отказаться от коммутатора, так что это привело бы к тому, что я полагал, что (надеюсь) некоторые разумные рассуждения позади этого.

Единственная причина, по которой я мог бы подумать, - это, возможно, некоторые накладные расходы на вызов функции call_user_func_array(), но это не кажется достаточно хорошей причиной для используйте кучу заявлений о делах. Есть ли такой угол, который, кажется, не получается?

Ответ 1

Причина в том, что на call_user_func_array есть накладные расходы. У этого есть накладные расходы на дополнительный вызов функции. Обычно это находится в диапазоне микросекунд, но это может стать важным в двух случаях:

  • Рекурсивные вызовы функций

    Поскольку он добавляет еще один вызов стеку, он удваивает количество использования стека. Таким образом, вы можете столкнуться с проблемами (с xdebug или ограничениями памяти), что приведет к сбою вашего приложения, если у вас закончится стека. В приложениях (или частях) использование этого подхода в стиле может сократить использование стека на целых 33% (что может быть разницей между запуском и сбоем приложения)

  • Производительность

    Если вы много раз вызываете функцию, то эти микросекунды могут значительно увеличиться. Поскольку это находится в рамках (похоже, что-то сделано Lithium), его, скорее всего, назовут десятками, сотнями или даже тысячами раз в течение срока действия приложения. Таким образом, даже если каждый индивидуальный вызов является микро-оптимизацией, эффект значительно увеличивается.

Итак, вы можете удалить коммутатор и заменить его на call_user_func_array, и он будет на 100% одинаковым по функциональности. Но вы потеряете два преимущества оптимизации, упомянутые выше.

ИЗМЕНИТЬ И чтобы доказать разницу в производительности:

Я решил сделать сам тест. Здесь ссылка на точный источник, который я использовал:

http://codepad.viper-7.com/s32CSb (также в нижней части этого ответа для справки)

Теперь я протестировал его в системе Linux, системе Windows и сайте кодовой страницы (2 командной строки и 1 в сети и 1 с включенным XDebug). Все запущенные версии 5.3.6 или 5.3.8

Заключение

Поскольку результаты довольно длинные, я сначала подведу итог.

Если вы так много называете это, не микро-оптимизация для этого. Конечно, индивидуальный вызов - незначительная разница. Но если он будет использоваться много, он может сэкономить немало времени.

Теперь стоит отметить, что все, кроме одного из этих тестов, запускаются с помощью XDebug off. Это чрезвычайно важно, так как 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 или менее аргументами. Для 4 аргументов это достаточно близко, чтобы квалифицироваться как микро-оптимизация. Для 5 это медленнее (из-за накладных расходов оператора switch).

Теперь объекты - это еще одна история. Для объектов значительно быстрее использовать оператор switch даже с 4 аргументами. И аргумент 5 немного медленнее.

Окна

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 аргументами или менее, это значительно быстрее, чтобы запустить его через коммутатор. С 5 аргументами он значительно медленнее с коммутатором.

Windows с XDebug

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 (но без анализа покрытия, только с включенным расширением), почти всегда медленнее использовать оптимизацию коммутатора. Это любопытно, так как многие тесты выполняются в dev-блоках с включенным 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 />";
}

Ответ 2

Вы можете найти это в классах шаблонов phpsavant. PMJ получил подсказку о том, как медленно call_user_func *() и понял, что 90% работы будут обрабатываться с помощью первых пяти параметров намного быстрее. Все остальное будет обрабатываться медленным способом. Я не могу найти сообщение с обсуждением о том, как это сделать, но это страница, где он идентифицирует проблему. http://paul-m-jones.com/archives/182