PHP函数调用开销的重要性


How Significant Is PHP Function Call Overhead?

我对PHP相对陌生,并且慢慢地学习了该语言特有的特性。我经常遇到的一件事是,我(所以我被告知)使用了太多的函数调用,通常会被要求做一些事情来解决它们。这里有两个例子:

// Change this:
} catch (Exception $e) {
  print "It seems that error " . $e->getCode() . " occured";
  log("Error: " . $e->getCode());
}
// To this:
} catch (Exception $e) {
  $code = $e->getCode();
  print "It seems that error " . $code . " occured";
  log("Error: " . $code);
}

第二个示例

// Change this:
$customer->setProducts($products);
// To this:
if (!empty($products)) {
  $customer->setProducts($products);
}

在第一个例子中,我发现将$e->getCode()分配给$code会引起轻微的认知开销;"什么是‘$code’?啊,这是来自异常的代码。"而第二个例子增加了循环复杂性。在这两个例子中,我发现优化是以可读性和可维护性为代价的。

性能提升值得吗?还是这种微观优化

我应该注意的是,我们目前还停留在PHP 5.2上。

我做了一些非常粗略的台架测试,发现函数调用性能的命中率在10%到70%之间,这取决于我台架测试的性质。我承认这很重要。但在catch块被命中之前,有一个对数据库和HTTP端点的调用。在$customer上设置$products之前,$products数组发生了复杂排序最终,这种优化是否证明了使代码更难阅读和维护的成本是合理的或者,尽管这些例子是简化的,有人发现第二个例子和第一个一样容易阅读吗?

有人能引用关于这方面的好文章或研究吗?

编辑:

一个示例台架测试:

<?php
class Foo {
        private $list;
        public function setList($list) {
                $this->list = $list;
        }
}
$foo1 = new Foo();
for($i = 0; $i < 1000000; $i++) {
        $a = array();
        if (!empty($a))
                $foo1->setList($a);
}
?>

使用time命令运行该文件。在一台特定的机器上,几次运行后平均需要0.60秒。注释掉if (!empty($a))会导致它平均需要3.00秒才能运行。

澄清:这些是示例。第一个示例演示了可怕的异常处理和可能的DRY冲突,而牺牲了一个简单的、非特定于域的示例。

目前还没有人讨论服务器硬件与函数调用开销的关系。

当调用一个函数时,CPU的所有寄存器都包含与当前执行点相关的数据。所有CPU的寄存器都必须保存到内存中(通常是进程的堆栈),否则就没有希望返回到那个执行点并恢复执行。从函数返回时,必须从内存(通常是进程堆栈)中恢复所有CPU寄存器。

因此,我们可以看到一系列嵌套函数调用是如何给进程增加开销的。CPU的寄存器必须一次又一次地保存在堆栈上,并一次又一遍地恢复才能从函数中恢复。

这实际上是函数调用开销的来源。如果传递了函数参数,那么在调用函数之前,这些参数必须全部重复。因此,将巨大的数组作为函数参数传递是一种糟糕的设计。

关于getters/ssetters的使用开销,已经对面向对象的PHP进行了研究。删除所有getter/setter可将执行时间缩短约50%。这仅仅是由于函数调用开销。

PHP函数调用开销精确到15.5355%。

:)只是搅拌锅。

说真的,这里有几个关于这个主题的好链接:

PHP应用程序中是否可能有太多的函数?

功能与重复代码

这些链接中关于代码可维护性与速度的讨论解决了OP所暗示的(可能更重要的)问题,但为了添加一些数据,这些数据也可能是相关的,希望对未来遇到这个线程的人有用,以下是在2011 Macbook Pro上运行以下代码的结果(驱动器空间很小,运行的程序太多)。

正如其他地方所指出的,在决定是调用函数还是将代码"放入"时要考虑的一个重要因素是;"直列";是从某个代码块中调用函数的次数。函数被调用的次数越多,就越值得考虑联机工作。

结果(以秒为单位的次数)

调用函数方法|内联方法|差异| 差异百分比

1000次迭代(4次运行)

0.00398088726043701 |0.00314784098779 |0.00076103210449219 |19.4694

0.003828961486816 | 0.002599546051025 | 0.0012209415435791 | 31.9543

0.0030159950256348 | 0.0029480457305908 | 6.794929503945e-5 | 2.2530

0.0034149794769287|0.00331909124512|5.9604644775391E-6|0.1895

1000000次迭代(4次运行)

3.1843111515045|2.6896121501923 |0.49469900131226|15.5355

3.131945848465 | 2.7114839553833 | 0.42046189308167 | 13.4249

3.0256152153015 | 2.7648048400879 | 0.26081037521362 | 8.6201

3.1251409053802 | 2.7397727966309 | 0.38536810874939 | 12.3312

function postgres_friendly_number($dirtyString) {
    
    $cleanString = str_ireplace("(", "-", $dirtyString);
    $badChars = array("$", ",", ")");
    $cleanString = str_ireplace($badChars, "", $cleanString);
    
    return $cleanString;
    
}

//main
$badNumberString = '-$590,832.61';
$iterations = 1000000;
$startTime = microtime(true);
for ($i = 1; $i <= $iterations; $i++) {
    $goodNumberString = postgres_friendly_number($badNumberString);
}
$endTime = microtime(true);
$firstTime = ($endTime - $startTime); 
$startTime = microtime(true);
for ($i = 1; $i <= $iterations; $i++) {
    $goodNumberString = str_ireplace("(", "-", $badNumberString);
    $badChars = array("$", ",", ")");
    $goodNumberString = str_ireplace($badChars, "", $goodNumberString);
}
$endTime = microtime(true); 
$secondTime = ($endTime - $startTime); 
$timeDifference = $firstTime - $secondTime;
$percentDifference = (( $timeDifference / $firstTime ) * 100);

规范的PHP实现非常缓慢,因为它很容易实现,而且PHP所针对的应用程序不需要像快速函数调用那样的原始性能。

您可能需要考虑其他PHP实现。

如果您正在编写应该用PHP编写的应用程序(通过网络将数据从DB转储到浏览器),那么函数调用开销并不显著。当然,不要因为担心使用函数会带来太多开销而特意重复代码。

I您混淆了术语。一般函数调用开销表示调用函数返回所涉及的开销。而不是内联处理。它不是函数调用的总成本。这只是准备参数和返回值以及执行分支的成本。

问题是PHP和其他弱类型脚本风格的语言在确定函数是否有副作用方面非常糟糕。因此,他们将进行多次调用,而不是将函数的结果存储为临时函数。如果函数正在做一些复杂的事情,这将是非常低效的。

所以,底线是:只调用一次函数并存储和重用结果!不要用相同的参数多次调用同一个函数。(没有充分的理由)