字符串赋值中的累积内存使用:$a = $a . $b vs $a .= $b

3

你们中的一些人可能熟悉PHP在不同字符串情况下如何处理内存。

当一个字符串被重新赋值时,它并不是“更新”,而是克隆。至少这是我目前的理解。

$a = 'a';
$b = 'b';
$a = $a . $b; // uses sizeof($a)*2 + sizeof($b) bytes
$a .= $b; // uses sizeof($a) + sizeof($b) bytes
在我正在开发的模板引擎中,这意味着巨大的内存消耗。对于一个页面字符串,我使用了超过128mb的内存,实际上,它远远少于512kb。这是因为字符串一遍又一遍地被复制。 简单来说,每次我执行像这样的操作时,都会进行这些副本:
$page = str_replace($find, $replace, $page)

通常情况下,是否有避免创建此克隆的解决办法?

我进行了基准测试,这将产生相同的输出,但完全不同的内存消耗。第一个会消耗大量的内存,但第二个只会消耗实际字符串大小所需的内存。

$iterations = 100000;
$a = 'a';
$b = 'b';
echo "start peak memory usage " . (memory_get_peak_usage()/1024).'k<br>';
echo "start current memory usage " . (memory_get_usage()/1024).'k<br>';

for($i = 0; $i<$iterations; $i++) {
    $a = $a . $b;
}
echo "end peak memory usage " . (memory_get_peak_usage()/1024).'k<br>';
echo "end current memory usage " . (memory_get_usage()/1024).'k<br>';

对比:

$iterations = 100000;
$a = 'a';
$b = 'b';
echo "start peak memory usage " . (memory_get_peak_usage()/1024).'k<br>';
echo "start current memory usage " . (memory_get_usage()/1024).'k<br>';

for($i = 0; $i<$iterations; $i++) {
    $a .= $b;
}
echo "end peak memory usage " . (memory_get_peak_usage()/1024).'k<br>';
echo "end current memory usage " . (memory_get_usage()/1024).'k<br>';

对于模板引擎而言,避免不必要的内存消耗的最佳方法是什么?在开发环境中这不是问题,但在生产中它可能成为可扩展性问题。

自然地,速度也是我关心的问题,所以替代方案应该与这个差不多。

最后,我认为这也与变量作用域有关。请随意纠正我,因为我不是专业人士。我的理解是,当一个函数或方法结束时,PHP垃圾回收器(?)会将变量“取消设置”,但在我的情况下,我们正在处理的$ page自然存在于整个脚本的持续时间中,因为它是一个类变量,并且可以通过$ this->page进行访问,因此旧实例无法“取消设置”。

编辑16.10.2014: 就这个问题进行跟进,我做了一些测试,并倾向于将页面分解成多个部分的解决方案。以下是结构的简单草图,向下解释。

class PageObjectX {
    $_parent;
    __constructor(&$parent) { $this->_parent = $parent; }
    /* has a __toString() method, handles how the variable/section is outputted. */
}

class Page {
    $_parts;
    $_source_parts;
    $_variables;

    public function __constructor($s) {
        $this->_source_parts = preg_split($s, ...);
        foreach($this->_source_parts as $part) {
            $this->_parts[] = new PageObject($this, ...); }
    }

    public function ___toString() { return implode('', $this->_parts); }

    public function setVariables($k, $v) { $this->_variables[$k] = $v; }
}
我所做的是将模板字符串分解为部分的数组。这些部分包括普通字符串、变量、需要从数据库获取的字符串以及区域/部分。
数组的管理被封装在Page类中,它具有对象元素:PageVariable、PageString、PageRepeatable、PagePlaintext。每个对象都提供了toString()方法,允许不同类型的部分控制它们的显示,有助于保持类的规模小而易于管理。对我来说感觉很“清洁”。
每个PageN类通过对父级的引用从主类获取其数据。因此,所有全局变量都设置为Page类,并且页面类处理单个查询以获取所有已翻译的字符串等。
可重复使用区域可能并不直观。我使用可重复使用区域来显示列表或可以多次重复的内容,如新闻项目。内容会改变,但结构不会变。因此,我向Page传递以下数组,当可重复使用区域名称“news”查找其数据时,例如会获取两个新闻项目的数据。
$regions['news'][0]['news title'] = 'Todays news';
$regions['news'][0]['news desc'] = 'The united nations...';
$regions['news'][1]['news title'] = 'Yesterdays news';
$regions['news'][1]['news desc'] = 'Meanwhile in Afghanistan the rebels...';
如果页面元素没有数据,可以在__toString()中轻松地将其排除。这减少了模板中未使用部分的清理需求。 这种方法的整体性能似乎相当不错。与初始比较相比,内存消耗约为一半。2M vs 4M。我预计在大型页面中,它将以更好的比率呈现,因为测试页面非常简单。 与字符串版本相比,速度提高显着,其中清理需要相当多的资源。字符串版本为0.6秒,而此方法仅需0.1秒。 我会发布最终结果的更新,但这就是我目前所拥有的。希望这对那些从谷歌上偶然发现此页面的人有所帮助;)

PHP垃圾回收器并非一直运行,因为它是一项非常昂贵的操作。它只会定期运行或在内存压力较大时才会运行。PHP会通过标记“准备回收”的内容来帮助垃圾回收,例如函数局部变量和其他已经超出作用域的内容,但这些内容可能要到很久以后甚至永远不会被清理。 - Marc B
那么你的模板引擎基于多次执行 str_replace 吗?这对我来说似乎是你方法的一个普遍概念问题。为了解决这个问题,我的第一个想法是首先解析你的模板并将其分成几个部分,例如 "static text <DYNAMIC CONTENT> static text" 可以转换为 array("static text ", new DynamicContent(), " static text") 或类似的东西。然后,您可以使用此解析结果数据结构使用简单的 echo 命令构建页面。您甚至可以缓存已解析的数据以获得更好的模板处理性能。 - Hauke P.
1
将模板转换为PHP,然后运行它。 - Ry-
这里是模板引擎的简短解释。首先,它会加载一个带有占位符和区域的HTML字符串。占位符,例如 {?user}<?content> 将被填充。还有一些可以重复的区域 - 比如右侧的“热门问题”链接。部分内容可以以多种方式显示、隐藏和操作。其思想是将HTML和PHP分开。页面可以完全重新设计,添加和删除元素而不需要更改PHP中的任何一行代码。唯一的目的不是 str_replace,但它经常用于将 {?user} 操作为 "Joh Doe"。 - Willem van Schevikhoven
Hauke有一个好的解决方案,不要将字符串连接起来,而是将它们保存在一个数组中。在Drupal中,他们准备了一个包含所有{?user}字符串的数组,以及一个对应的Joh Doe数组。然后他们使用一个str_replace()函数。这意味着源代码和数组中的所有字符串只被分配一次。(复制数组只会增加每个字符串的引用计数。) - Alexis Wilke
我一定会花一些时间思考这个替代方案,谢谢alexis-wilke和hauke-p。 - Willem van Schevikhoven
2个回答

2
在您的具体示例中($page = str_replace($find, $replace, $page);),无法避免创建 $page 的副本。这适用于所有需要按值传递参数的函数(与字符串相关或不相关)。然而,PHP 的垃圾回收器应该会定期释放这些未使用的副本。 如果您仍然遇到过度的内存使用情况,我强烈建议您检查您的代码。确保变量具有明确定义的作用域,并且仅存储所需数据。有一些可用的工具可以帮助诊断 PHP 的内存使用情况,例如php-memprof。 此外,我还要验证您是否正在使用最新版本的 PHP,因为垃圾回收持续改进

0
你使用哪个系统?对我来说,这并没有太大的区别: 在一个简单的脚本中: 峰值325.1k,当前218.7k vs. 峰值219.6k,当前218.7k 在一个类的函数中: 峰值327.2k,当前220.8k vs. 峰值221.8k,当前220.8k 我预计峰值的差异可能来自最后一个操作,其中$a被连接,并且仍在使用旧值。这可以解释近100k的峰值差异。

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