【问题标题】:Why are PHP function calls *so* expensive?为什么 PHP 函数调用如此昂贵?
【发布时间】:2010-09-11 15:53:35
【问题描述】:

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;

这首先使用函数 (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)。 (但另一方面,这两种方法的执行时间都显着下降)。

【问题讨论】:

  • 这是一个很大的假设。你有多确定 strlen 比 isset 重 30%?
  • 做分析,而不是“基准”
  • @Col:如果你能告诉我在哪里可以找到负责此问题的代码,那就太好了。更好地解释哪些部分花费的时间最长。 (更清楚地说:我并不愚蠢。如果我知道负责的代码在那里,我就不会在这里问了。我在这里问是因为我不知道,我希望其他人知道.)
  • @Kendall:毕竟,你是对的。我刚刚在没有 XDebug 的情况下进行了测试,得到了非常不同的结果(见编辑)。非常感谢您指出。
  • 作者现为PHP核心开发者。我希望这里的人们多一点理解,有时人们会问问题来学习。

标签: php function


【解决方案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 中操作码的实现。在名称前加上 ZEND_,例如ZEND_INIT_FCALL_BY_NAME 并搜索。

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

【讨论】:

  • 感谢 Artefacto,这对我很有帮助。我将看看这些定义。 +1
【解决方案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 的台式机(更具体地说,Intel® Core™ i5-6500 CPU @ 3.20GHz × 4)上 PHP 7 的结果如下:

内联调用 mt_rand() 耗时 3.5181620121002 秒 调用用户函数耗时 7.2354700565338 秒

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

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

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

【讨论】:

  • 在运行 PHP 5.6.30 的 Xeon E5 服务器上,内联耗时 15.9 秒,用户函数耗时 30.9 秒。
【解决方案3】:

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

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

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

越界检查通常会像这样实现:

return $str->length < $length;

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

【讨论】:

  • strlen 只是一个结构成员的查找。它不会循环播放。
  • 也许吧,但原来的测试仍然没有测试函数调用的时间。 isset 是一种语言结构。 empty、isset 等东西的性能都比语言的其他部分好得多。一个有效的测试只是一个函数和一个内联循环。这些测试毫无意义。
  • 我刚刚对我的 MBP 进行了一些测试。这台超载笔记本电脑上的函数调用需要 0.00000107109547 秒。那不贵。昂贵的是因为有人认为函数调用很昂贵,所以他们避免使用函数。
  • 我知道 isset 不是一个函数 - 这就是在示例中使用它的全部意义:它使用函数调用 (strlen) 测试功能等效的代码一次,而没有(isset)。
  • 但是为什么不在函数循环中测试与在非函数循环中测试相同的东西呢?它添加了使测试无效的不匹配。真正的测试是测试 F() { isset($a); 之间的差异。 } 和 isset($a);至少,strlen 版本是将结果压入堆栈,然后执行 lte 操作,这超出了测试的范围。
【解决方案4】:

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

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

下面的快速测试将对数字为 TRUE,而对其他任何东西都为 FALSE。

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

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

【讨论】:

  • 如果 $x 可以是具有 __toString 方法的对象,那么该测试可能会很慢。你有在那种情况下仍然很快的方法吗?
  • @Brilliand 我使用布尔检查 ($var*1) 从 xml 进行类型转换。它的速度大约是is_numeric 的两倍。
  • 此外,($x == '0'.$x) 方法对于假定为数字的 '12.3k' 之类的值也会失败。对于完整的上下文和您的兴趣,这是我用于 xml 解析器类的优化的无函数类型转换方法:((($var = $data*1) &amp;&amp; --$data+1 === $var) || (string)($var = (float)$data) == $data) ? $data === "0" ? (int)$var:$var: ($data === 'TRUE' ? TRUE: ($data === 'FALSE' ? FALSE: $data)); /cc: @Brilliand
  • @hexalys 我刚刚运行了一个基准测试——它比我的机器上的is_numeric 快得多,但它会为某些操作数类型(例如数组)产生错误。 (在我们的生产服务器上它也比is_numeric慢两倍左右,它有一些我不明白的功能优化。)
  • @Brilliand 好的,很有趣。绝对不适合混合类型。 :) 适用于自动类型转换字符串的上下文。
【解决方案5】:

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

<?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 次运行(如上所示),我们看到使用输出中的函数调用的差异

with function call: 2.4601600170135 
without function call: 2.4477159976959

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

【讨论】:

    猜你喜欢
    • 2017-06-04
    • 1970-01-01
    • 2019-11-06
    • 2015-07-12
    • 2011-05-03
    • 1970-01-01
    • 2014-05-18
    • 2011-06-18
    • 1970-01-01
    相关资源
    最近更新 更多