PHP 函数调用开销有多大?

Posted

技术标签:

【中文标题】PHP 函数调用开销有多大?【英文标题】:How Significant Is PHP Function Call Overhead? 【发布时间】:2013-08-01 01:41:06 【问题描述】:

我对 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 端点的调用。在$products 设置在$customer 上之前,$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 违规,而牺牲了一个简单的、非特定领域的示例。

【问题讨论】:

10% 到 70% 的开销通过调用什么函数? 恕我直言,如果需要这种级别的优化,那么 PHP 一开始就不是正确的工具。 在您的第一个示例中,您需要调用 $e->getCode() 两次(增加开销),但第二部分是将结果分配给变量并多次使用它。良好的编码习惯。在第二个示例中,您再次进行防御性编程是一种很好的编程习惯。 这完全取决于函数的实际作用,以及执行需要多长时间:$e-&gt;getCode(); 非常简单,并且带有微优化的味道;但是,如果连续多次重复调用,则进行 db 调用以返回 1000 行的函数将非常重要。分析您的代码应该为您提供函数调用的时序图 @Anigel,我在问题中添加了一项基准测试。 【参考方案1】:

PHP 函数调用开销正好是 15.5355%。

:) 只是搅拌锅。

说真的,这里有几个很好的链接:

Is it possible to have too many functions in a PHP application?

functions vs repeated code

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

如其他地方所述,在决定是调用函数还是将代码“内联”时,一个重要的考虑因素是从某个代码块中调用该函数的次数。该函数被调用的次数越多,就越值得考虑进行内联工作。

结果(以秒为单位的时间)

调用函数方法 |在线方法 |差异 |百分比差异

1000 次迭代(4 次运行)

0.0039088726043701 | 0.0031478404998779 | 0.00076103210449219 | 19.4694

0.0038208961486816 | 0.0025999546051025 | 0.0012209415435791 | 31.9543

0.0030159950256348 | 0.0029480457305908 | 6.7949295043945E-5 | 2.2530

0.0031449794769287 | 0.0031390190124512 | 5.9604644775391E-6 | 0.1895

1,000,000 次迭代(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 版本。【参考方案2】:

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

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

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

这确实是函数调用开销的来源。如果传递了函数参数,则必须在调用函数之前将它们全部复制。因此,将庞大的数组作为函数参数传递是一种糟糕的设计。

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

【讨论】:

哪些研究...链接? 该参数对 PHP 无效。在堆栈上保存寄存器是所有语言的标准过程,而且速度非常快。但是 PHP 中的函数调用比 C++ 甚至 Java 中的要慢得多。我不知道 Zend 引擎到底在做什么,但主要的性能损失不仅仅是保存寄存器造成的。 Php 存储指向数组的指针,而不是整个数组。不管数组有多大,开销都是一样的。另外,如上所述,这不是 C,主要开销不是来自寄存器。【参考方案3】:

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

您可能需要考虑其他PHP implementations。

如果您正在编写应使用 PHP 编写的应用程序(通过网络将数据从 DB 转储到浏览器),那么函数调用开销并不大。当然不要特意去复制代码,因为你害怕使用函数会产生太多开销。

【讨论】:

【参考方案4】:

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

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

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

【讨论】:

以上是关于PHP 函数调用开销有多大?的主要内容,如果未能解决你的问题,请参考以下文章

ctypes 与 C 扩展

input() 函数的输入可以有多大?

为啥调用函数时会有开销?

在套接字库中调用 recv 时,我的 recv 缓冲区应该有多大

2017/05/05学习笔记

继承构造函数在 C++ 中有多大用处?