为什么 PHP 函数调用 *so* 很昂贵?

2022-08-30 12:16:37

PHP 中的函数调用成本很高。这里有一个小的基准测试来测试它:

<?php
const RUNS = 1000000;

// 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;

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

我得到以下输出:

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)。(但就目前而言,这两种方法的执行时间都大幅下降)。


答案 1

函数调用在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中检查操作码的实现。在名称前面加上前缀,例如 for 和 search。ZEND_ZEND_INIT_FCALL_BY_NAME

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


答案 2

调用用户函数的开销真的那么大吗?或者更确切地说,它现在真的那么大吗?自最初提出这个问题以来,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 的台式机(更具体地说,英特尔®酷睿™ i5-6500 CPU @ 3.20GHz × 4)上的 PHP 7 结果如下:

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

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

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

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


推荐