如何在unset()函数中找到触发垃圾回收的“低内存”和“空闲CPU周期”调用?

7
我经常发现以下引用被用来解释PHP的unset()不会立即触发"垃圾回收",而是在看到适当时才执行(重点是我的):
“unset()正如它的名字所说-取消设置变量。它不会强制立即释放内存。PHP的垃圾收集器将在看到适当时进行处理-故意尽快,在那些CPU周期不再需要的情况下,或者在脚本将要耗尽内存之前,以先发生的为准。如果您正在执行$whatever = null;,那么您正在重写变量的数据。您可能会更快地获得已释放/缩小的内存,但它可能会从真正需要它们的代码中更早地窃取CPU周期,导致更长的总执行时间。”
我想知道这种"低内存"和"空闲CPU周期"触发垃圾回收的C代码是如何工作的,以及在PHP 5.2.x和PHP 5.3+之间是否有区别。
所以我下载了PHP 5.2.17的C源文件,试图找到适当的代码部分。也许我只是眼瞎,或者我的C技能太低,但我未能找到这样的代码。 请问有人可以指点我正确的C文件吗?
编辑: 在寻找上述引用的示例用法时,我意识到一些奇怪的事情。 其中一些示例,例如https://dev59.com/gHRB5IYBdhLWcg3wkH9N#584982,通过使用以下URL引用php.net上的评论来引用此引用:http://us2.php.net/manual/en/function.unset.php#86347。 浏览到此URL仅显示unset()手册的顶部。缺少评论#86347。
检查wayback机器显示,这个评论确实存在于2008年10月,但在2012年9月或之后某个时间消失了(原因不明)。
也许这引用是错误的?
还是有人可以指向正确的C文件吗?

1
我很想知道垃圾收集器实际上是如何决定工作的。真正的触发器是什么?因为“在适当时候进行”并不是自说明的。+1 - Daryl Gill
我相信这是你想要查看的地方:http://lxr.php.net/xref/PHP_5_2/main/alloca.c - user557846
@duskwuff:这里是一个例子:https://dev59.com/gHRB5IYBdhLWcg3wkH9N - Jürgen Thelen
我在答案引用的页面中找不到任何引用的文本。文档可能已经更改。 - user149341
3
如果用户笔记中写了某些内容,除非你有强有力的证据相信它是正确的,否则应该始终假设它是错误的 ;) - NikiC
显示剩余3条评论
1个回答

6

好的,现在是PHP神话破除时间!请先阅读PHP文档,了解垃圾回收机制的工作原理,因为我将假设您在这方面具有一些基础知识:

第二个文档特别解释了何时会触发循环垃圾收集器的运行。它与“空闲CPU周期”或“低内存”无关——它完全基于存在的潜在垃圾对象数量:

打开垃圾收集器时,当根缓冲区满时执行如上所述的找到循环算法。根缓冲区具有10,000个可能的根的固定大小。

也就是说,每当累积了一定数量的潜在垃圾对象时(与这些对象的大小无关),循环垃圾收集器都会运行。查看zend_gc.c中的代码可以确认这一点——那里肯定没有检查总可用内存量,也绝对没有为了让GC在CPU空闲时运行而需要的线程。因此,我认为我们可以称这部分内容为“已破除”。


接下来,让我们看看$x = nullunset($x)之间实际的区别可能是什么。首先,让我们使用以下类作为我们的测试假人:

class NoisyDestructor {
    function __destruct() {
        print "Destructor called\n";
    }
}

现在,让我们看一下将变量设置为null和使用“unset()”之间的区别:
现在,让我们来看一下将变量设置为null和使用“unset()”之间的区别:
$x = new NoisyDestructor();
print "Created\n";
$x = null;
print "Nulled\n";

print "\n";

$x = new NoisyDestructor();
print "Created\n";
unset($x);
print "Unset\n";

当我们运行此代码时,会看到:
Created
Destructor called
Nulled

Created
Destructor called
Unset

等一下-这两个序列完全相同!它们之间没有任何功能上的差别。那么,性能如何呢?

class Thing { }

$start = microtime(true);
for ($i = 0; $i < 1e6; $i++) {
    $x = new Thing();
    $x = null;
}
printf("%f sec for null\n", microtime(true) - $start);

$start = microtime(true);
for ($i = 0; $i < 1e6; $i++) {
    $x = new Thing();
    unset($x);
}
printf("%f sec for unset\n", microtime(true) - $start);

现在,我使用我的笔记本电脑进行测试,使用 PHP 5.4 版本,得到以下结果:

0.130396 sec for null
0.175086 sec for unset

不仅将变量设置为null和取消设置它之间的性能差异非常小,考虑到我们必须运行此循环多少次才能看到此结果,而且事实上与该评论所声称的完全相反:unset()要慢大约25%!这个PHP神话已经被彻底打破。
TL;DR:你找到的引用完全是错误的。(似乎该引用因为正是这个原因被从PHP.net中删除了。)

1
你看到 null 和 unset 之间的时间差异是因为你在全局范围内进行了测试。在函数范围内,差异会小得多。全局范围使用 symtable,其中 unset 代码将不断删除和添加桶(这很慢)。在本地范围内,没有 symtable,一切都通过 CV 表处理,因此只需要销毁和创建 zval,对于两种代码来说,这个过程几乎是相同的。 - NikiC
@duskwuff:谢谢,非常好的答案^^ - Jürgen Thelen
@duskwuff 再问一个问题:zend_gc.c 是在 PHP 5.3 中引入的(如果我没记错的话)。PHP 5.2 中的垃圾回收机制是如何工作的?与 PHP 5.3+ 有什么不同吗? - Jürgen Thelen
PHP 5.2 中没有垃圾回收器,因此循环引用永远不会被回收。但是仍然有引用计数,所以当大多数对象不再被引用时,它们仍然会被回收。 - user149341

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接