性能考量

我们在上一节中已经提到过,仅仅收集可能的根节点对性能的影响微乎其微,但是这是将 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),但只找到一个可能的根节点:被 unset 的变量。当根缓冲区在 10,000 次迭代后填满(总共有 10,000 个可能的根节点)时,垃圾收集机制会启动并释放与这些可能的根节点相关的内存。这在 PHP 5.3 的锯齿状内存使用图中非常明显。在每 10,000 次迭代之后,机制会启动并释放与循环引用变量相关的内存。这种机制本身在这个例子中不需要做很多工作,因为泄漏的结构非常简单。从图中可以看出,PHP 5.3 中的最大内存使用量约为 9 Mb,而在 PHP 5.2 中,内存使用量会不断增加。

运行时减速

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

示例 #2 GC 性能影响

<?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% 的减速。但是,脚本使用的最大内存量减少了 98%,从 931Mb 降至 10Mb。这个基准测试并不十分科学,甚至不能代表实际应用程序,但它确实展示了这种垃圾收集机制提供的内存使用优势。好处是,对于这个特定脚本来说,减速始终是相同的 7%,而内存节省功能随着在脚本执行期间发现的循环引用越来越多而节省的内存也越来越多。

PHP 的内部 GC 统计信息

可以从 PHP 内部获取有关垃圾收集机制运行方式的更多信息。但为此,您必须重新编译 PHP 以启用基准测试和数据收集代码。在运行 ./configure 和您想要的选择之前,您必须将 CFLAGS 环境变量设置为 -DGC_BENCH=1。以下步骤应该可以解决问题:

示例 #4 重新编译 PHP 以启用 GC 基准测试

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

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

示例 #5 GC 统计信息

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 次,并且总共在这些 110 次运行中释放了超过 200 万个内存分配。一旦垃圾收集机制至少运行了一次,“根缓冲区峰值”始终为 10000。

结论

通常,PHP 中的垃圾收集器只会导致循环收集算法实际运行时减速,而在普通(较小)脚本中,根本不会出现性能影响。

但是,在循环收集机制确实针对普通脚本运行的情况下,它提供的内存减少将允许更多这些脚本在您的服务器上同时运行,因为总共使用的内存更少。

对于长时间运行的脚本,例如长测试套件或守护进程脚本,这种优势最为明显。此外,对于 » PHP-GTK 应用程序,这些应用程序通常比 Web 脚本运行时间更长,新机制应该在防止内存泄漏随时间推移而发生方面产生很大影响。

添加笔记

用户贡献笔记 2 条笔记

21
Talisman
8 年前
不幸的是,正如上面示例中所述,GC 有可能助长懒惰的编程习惯。
GC 在协助内存管理方面的好处显而易见,有助于维护稳定的系统,但这绝不是不合理规划和测试代码的借口。
始终批判性地、客观地重新阅读您的代码,以确保您没有无意中引入内存泄漏。
19
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