代码之家  ›  专栏  ›  技术社区  ›  NikiC

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

  •  34
  • NikiC  · 技术社区  · 14 年前

    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倍以上。

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


    with function call:    3.152988910675
    without function call: 1.4107749462128
    


    with function call:    2.3795559406281
    without function call: 0.90840601921082
    

    这里的差异再次变得稍大(2.62)。(但另一方面,两种方法的执行时间都显著下降)。

    5 回复  |  直到 5 年前
        1
  •  46
  •   Artefacto    14 年前

    函数调用在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 特别复杂。然后是函数本身的实现,它必须展开堆栈,检查类型,转换zvals,并可能将它们与实际工作分离。。。

        2
  •  5
  •   GordonM    8 年前

    调用用户函数的开销真的那么大吗?或者说现在真的有那么大了?自从最初提出这个问题以来,PHP和计算机硬件在近7年里都有了飞跃性的发展。

    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 vintage i5的桌面(更具体地说,Intel Core i5-6500 CPU@3.20GHz 4)上的PHP 7结果如下:

    内联调用mt_rand()花费了3.51816201202秒 调用用户函数花费了7.2354700565338秒

    这对于增加源代码的复杂性几乎不会产生任何有意义的性能好处。

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

        3
  •  4
  •   rich remer    11 年前

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

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

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

    return $str->length < $length;
    

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

        4
  •  3
  •   TheDigitalOrchard    11 年前

    函数调用是昂贵的,因为上面的@artefactor很好地解释了这个原因。请注意,它们的性能与所涉及的参数/参数的数量直接相关。这是我在开发自己的应用程序框架时非常关注的一个领域。当有可能避免函数调用时,我会这样做。

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

    下面的快速测试对于一个数字是正确的,对于其他任何数字都是错误的。

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

    比…快得多 是数字() 是\整数()

        5
  •  3
  •   Bing    8 年前

    我认为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;
    }
    

    本例中唯一的区别是函数调用本身。在100000次运行(如上所述)的情况下,我们发现使用输出中的函数调用有<1%的差异:

    with function call: 2.4601600170135 
    without function call: 2.4477159976959
    

    当然,这一切都取决于你的职能 你认为贵的东西 . 如果 nothing() 返回 $x*2 $x = $i.$x $x = $i.($x*2) 在使用函数调用时,我们会看到大约4%的损失。