为什么PHP函数调用*如此*昂贵?

时间:2010-09-11 15:53:35

标签: php function

PHP中的函数调用很昂贵。这是一个测试它的小基准:

// create test string
$string = str_repeat('a', 1000);
$maxChars = 500;

// with function call
$start = microtime(true);
for ($i = 0; $i < RUNS; ++$i) {
    strlen($string) <= $maxChars;
}
echo 'with function call: ', microtime(true) - $start, "\n";

// without function call
$start = microtime(true);
for ($i = 0; $i < RUNS; ++$i) {
    !isset($string[$maxChars]);
}
echo 'without function call: ', microtime(true) - $start;

这首先使用函数(strlen)测试功能相同的代码,然后不使用函数(isset不是函数)。

我得到以下输出:

with function call:    4.5108239650726
without function call: 0.84017300605774

正如您所看到的,使用函数调用的实现比不调用任何函数的实现慢了五(5.38)倍。

我想知道为什么函数调用如此昂贵。什么是主要瓶颈?它是哈希表中的查找吗?或者那么慢?


我重新审视了这个问题,并决定再次运行基准测试,XDebug完全禁用(不仅仅是分析已禁用)。这表明,我的测试相当复杂,这一次,我得到了10000000次运行:

with function call:    3.152988910675
without function call: 1.4107749462128

这里函数调用只有大约两倍(2.23)慢,所以差别要小得多。


我刚刚在PHP 5.4.0快照上测试了上面的代码,得到了以下结果:

with function call:    2.3795559406281
without function call: 0.90840601921082

这里差异再次略大(2.62)。 (但是在这方面,两种方法的执行时间都显着下降了。)

5 个答案:

答案 0 :(得分:43)

PHP中的函数调用很昂贵,因为有很多东西要完成。

请注意isset不是函数(它有一个特殊的操作码),所以它更快。

对于这样一个简单的程序:

<?php
func("arg1", "arg2");

有六个(每个参数有四个+)操作码:

1      INIT_FCALL_BY_NAME                                       'func', 'func'
2      EXT_FCALL_BEGIN                                          
3      SEND_VAL                                                 'arg1'
4      SEND_VAL                                                 'arg2'
5      DO_FCALL_BY_NAME                              2          
6      EXT_FCALL_END                                            

您可以在zend_vm_def.h中检查操作码的实现。将ZEND_添加到名称中,例如ZEND_INIT_FCALL_BY_NAME和搜索。

ZEND_DO_FCALL_BY_NAME特别复杂。然后是函数本身的实现,它必须展开堆栈,检查类型,转换zval并可能将它们分开并实际工作......

答案 1 :(得分:5)

我认为他们不是。你实际上根本没有测试函数调用。您正在测试低级越界检查(isset)和遍历字符串以计算字节数(strlen)之间的差异。

我找不到任何特定于PHP的信息,但strlen通常实现类似(包括函数调用开销):

$sp += 128;
$str->address = 345;
$i = 0;
while ($str[$i] != 0) {
    $i++;
}
return $i < $length;

越界检查通常会实现如下:

return $str->length < $length;

第一个是循环。第二个是简单的测试。

答案 2 :(得分:4)

调用用户函数的开销真的那么大吗?或者说它现在真的那么大吗?自从最初提出这个问题以来,PHP和计算机硬件在近7年内取得了突飞猛进的发展。

我在下面编写了自己的基准测试脚本,它直接和通过用户函数调用在循环中调用mt_rand():

const LOOPS = 10000000;

function myFunc ($a, $b)
{
    return mt_rand ($a, $b);
}

// Call mt_rand, simply to ensure that any costs for setting it up on first call are already accounted for
mt_rand (0, 1000000);

$start = microtime (true);
for ($x = LOOPS; $x > 0; $x--)
{
    mt_rand (0, 1000000);
}
echo "Inline calling mt_rand() took " . (microtime(true) - $start) . " second(s)\n";

$start = microtime (true);
for ($x = LOOPS; $x > 0; $x--)
{
    myFunc (0, 1000000);
}
echo "Calling a user function took " . (microtime(true) - $start) . " second(s)\n";

基于2016年份i5桌面的PHP 7(更具体地说,英特尔®酷睿™i5-6500 CPU @ 3.20GHz×4)的结果如下:

内联调用mt_rand()花了3.5181620121002秒 调用用户函数需要7.2354700565338秒

调用用户函数的开销似乎大约是运行时的两倍。但它需要1000万次迭代才能变得特别引人注目。这意味着在大多数情况下,内联代码和用户功能之间的差异可能可以忽略不计。您应该只关心程序最内层循环中的那种优化,即使这样,只有基准测试显示出明显的性能问题。其他任何东西都是,这对于源代码中增加的复杂性几乎没有产生有意义的性能优势。

如果你的PHP脚本很慢,那么几乎可以肯定的是它会降低到I / O或算法选择不当而不是函数调用开销。连接到数据库,执行CURL请求,写入文件甚至只是回显到stdout都比调用用户函数要贵几个数量级。如果你不相信我,让mt_rand和myfunc回显他们的输出,看看脚本运行的速度有多慢!

在大多数情况下,优化PHP脚本的最佳方法是最小化它必须执行的I / O量(例如,只选择数据库查询中需要的内容,而不是依赖于PHP来过滤掉不需要的行)或者通过memcache之类的东西来缓存I / O操作,以降低文件,数据库,远程站点等的I / O成本

答案 3 :(得分:3)

由于@Artefacto上面的完美解释,函数调用很昂贵。请注意,它们的性能与所涉及的参数/参数的数量直接相关。这是我在开发自己的应用程序框架时非常关注的一个领域。当它有意义并且可以避免函数调用时,我会这样做。

一个这样的例子是我的代码中最近用一个简单的布尔测试替换is_numeric()is_integer()调用,特别是当可能对这些函数进行多次调用时。虽然有些人可能认为这种优化毫无意义,但我发现通过这种优化工作,我网站的响应能力有了显着的提升。

对于数字,以下快速测试将为TRUE,对于其他任何内容,则为FALSE。

if ($x == '0'.$x) { ... }

is_numeric()is_integer()快得多。同样,只有在有意义的情况下,使用某些优化才是完全有效的。

答案 4 :(得分:3)

我认为富豪的答案实际上非常准确。您将苹果与橙子与原始示例进行比较。试试这个:

<?php
$RUNS = 100000;
// with function call
$x = "";
$start = microtime(true);
for ($i = 0; $i < $RUNS; ++$i) {
    $x = $i.nothing($x);
}
echo 'with function call: ', microtime(true) - $start, "\n<br/>";

// without function call
$x = "";
$start = microtime(true);
for ($i = 0; $i < $RUNS; ++$i) {
    $x = $i.$x;
}
echo 'without function call: ', microtime(true) - $start;

function nothing($x) {
    return $x;
}

此示例中唯一的区别是函数调用本身。有100,000次运行(如上所述),我们看到使用输出函数调用的差异小于1%:

with function call: 2.4601600170135 
without function call: 2.4477159976959

当然这一切都取决于你的功能 你认为昂贵的。如果nothing()返回$x*2(我们将$x = $i.$x的非函数调用替换为$x = $i.($x*2),我们会看到使用函数调用损失约4%。