PHP Conference Japan 2024

性能注意事项

我们已经在上一节中提到,仅仅收集可能的根对性能的影响非常小,但这是在将 PHP 5.2 与 PHP 5.3 进行比较时。尽管与 PHP 5.2 中完全不记录可能的根相比,记录可能的根速度较慢,但 PHP 5.3 中对 PHP 运行时的其他更改阻止了这种特定的性能损失甚至出现。

性能受到影响的两个主要方面。第一个方面是减少内存使用量,第二个方面是垃圾回收机制执行内存清理时的运行时延迟。我们将研究这两个问题。

减少内存使用量

首先,实现垃圾回收机制的全部原因是通过尽快清理循环引用的变量来减少内存使用量。在 PHP 的实现中,这发生在根缓冲区已满时,或者在调用函数gc_collect_cycles()时。在下图中,我们显示了以下脚本在 PHP 5.2 和 PHP 5.3 中的内存使用情况,不包括 PHP 在启动时使用的基本内存。

示例 #1 内存使用示例

<?php
class Foo
{
public
$var = '3.14159265359';
public
$self;
}

$baseMemory = memory_get_usage();

for (
$i = 0; $i <= 100000; $i++ )
{
$a = new Foo;
$a->self = $a;
if (
$i % 500 === 0 )
{
echo
sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
?>
Comparison of memory usage between PHP 5.2 and PHP 5.3

在这个非常学术的例子中,我们创建了一个对象,其中一个属性设置为指向对象本身。当脚本中的$a变量在循环的下一轮迭代中被重新赋值时,通常会发生内存泄漏。在这种情况下,泄漏了两个 zval 容器(对象 zval 和属性 zval),但只找到一个可能的根:已取消设置的变量。当根缓冲区在 10,000 次迭代后(共有 10,000 个可能的根)已满时,垃圾回收机制启动并释放与这些可能的根关联的内存。这在 PHP 5.3 的锯齿状内存使用图中可以非常清楚地看到。每 10,000 次迭代后,机制就会启动并释放与循环引用的变量关联的内存。该机制本身在这个例子中不必做太多工作,因为泄漏的结构非常简单。从图中可以看出,PHP 5.3 中的最大内存使用量约为 9 Mb,而在 PHP 5.2 中,内存使用量持续增加。

运行时减速

垃圾回收机制影响性能的第二个方面是垃圾回收机制启动以释放“泄漏”内存所花费的时间。为了查看这有多大影响,我们稍微修改了之前的脚本,以允许更多次的迭代并删除中间内存使用情况数据。第二个脚本如下所示

示例 #2 垃圾回收性能影响

<?php
class Foo
{
public
$var = '3.14159265359';
public
$self;
}

for (
$i = 0; $i <= 1000000; $i++ )
{
$a = new Foo;
$a->self = $a;
}

echo
memory_get_peak_usage(), "\n";
?>

我们将运行此脚本两次,一次在zend.enable_gc设置开启时,一次在设置关闭时

示例 #3 运行上述脚本

time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php

在我的机器上,第一个命令似乎始终大约需要 10.7 秒,而第二个命令大约需要 11.4 秒。这大约是 7% 的减速。但是,脚本使用的最大内存量从 931Mb 减少到 10Mb,减少了 98%。此基准测试不是非常科学,甚至不代表现实生活中的应用程序,但它确实证明了此垃圾回收机制提供的内存使用优势。好消息是,对于此特定脚本,减速始终为 7%,而内存节省功能随着在脚本执行期间发现更多循环引用而节省越来越多的内存。

PHP 的内部垃圾回收统计信息

可以从 PHP 内部获取更多关于垃圾回收机制如何运行的信息。但是,要做到这一点,您必须重新编译 PHP 以启用基准测试和数据收集代码。您必须在使用所需选项运行./configure之前,将CFLAGS环境变量设置为-DGC_BENCH=1。以下序列应该可以解决问题

示例 #4 重新编译 PHP 以启用垃圾回收基准测试

export CFLAGS=-DGC_BENCH=1
./config.nice
make clean
make

当您使用新构建的 PHP 二进制文件再次运行上述示例代码时,您将看到 PHP 完成执行后显示以下内容

示例 #5 垃圾回收统计信息

GC Statistics
-------------
Runs:               110
Collected:          2072204
Root buffer length: 0
Root buffer peak:   10000

      Possible            Remove from  Marked
        Root    Buffered     buffer     grey
      --------  --------  -----------  ------
ZVAL   7175487   1491291    1241690   3611871
ZOBJ  28506264   1527980     677581   1025731

第一块中显示了最有用的统计信息。您可以在这里看到垃圾回收机制运行了 110 次,并且总共有超过 200 万个内存分配在这些 110 次运行中被释放。一旦垃圾回收机制至少运行了一次,“根缓冲区峰值”始终为 10000。

结论

通常,PHP 中的垃圾回收器只会当循环回收算法实际运行时才会导致速度下降,而在正常的(较小)脚本中,根本不会有任何性能损失。

但是,在循环回收机制确实对普通脚本运行的情况下,它提供的内存减少功能可以让更多此类脚本在您的服务器上并发运行,因为总共使用的内存减少了。

对于运行时间较长的脚本(例如冗长的测试套件或守护进程脚本),这些好处最为明显。此外,对于通常比 Web 脚本运行时间更长的 » PHP-GTK 应用程序,新的机制应该会在解决随着时间推移而出现的内存泄漏方面产生相当大的影响。

添加注释

用户贡献的注释 2 条注释

Talisman
9 年前
不幸的是,正如上述示例中阐述的那样,GC 倾向于助长懒惰的编程方式。
显然,GC 在协助内存管理方面确实存在优势,并且有助于维护系统的稳定性,但这并不是不进行代码规划和测试的借口。
始终批判性地、客观地重新阅读您的代码,以确保您不会无意中引入内存泄漏。
Dmitry dot Balabka at gmail dot com
6 年前
无需重新编译 PHP 即可获取 GC 性能统计信息。从 Xdebug 2.6 版本开始,您可以启用将统计信息收集到文件中的功能(默认目录 /tmp,文件名 gcstats.%p)

php -dxdebug.gc_stats_enable=1 your_script.php
To Top