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

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

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

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

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

  2. Представление

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

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

EDIT И доказать разницу в производительности:

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

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

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

Теперь объекты – это еще одна история. Для объектов значительно быстрее использовать оператор switch даже с 4 аргументами. И аргумент 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 аргументами или менее, это значительно быстрее, чтобы запустить его через коммутатор. С 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 />"; } с <?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 />"; } 

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