我有一个简单的对象,它可以有相同类型的子对象。
这个对象有一个toHTML方法,它的作用类似于:
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
问题是,当对象很复杂时,比如很多有孩子的孩子等等,内存使用率会飙升。
如果我简单地print_r
为这个对象提供的多维数组,我会得到大约1MB的内存使用量,但在我将数组转换为我的对象并执行print $root->toHtml()
之后,它需要10MB!!
我该怎么解决这个问题?
========================================
制作了一个类似于我真实代码(但更小)的简单类:
class obj{
protected $name;
protected $children = array();
public function __construct($name){
$this->name = $name;
}
public static function build($name, $array = array()){
$obj = new self($name);
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(){
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
}
}
测试:
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
print_r($big);
// memory_get_peak_usage() shows 0.61 MB
$root = obj::build('root', $big);
// memory_get_peak_usage() shows 18.5 MB wtf lol
print $root->toHTML();
// memory_get_peak_usage() shows 24.6 MB
问题是,您正在缓冲内存中的所有数据,而实际上并不需要这样做,因为您只是输出数据,而不是实际处理数据。
与其缓冲内存中的所有内容,如果你只想输出它,你应该把它输出到它要去的任何地方:
public function toHTMLOutput($outputStream){
fwrite($outputStream, '<div>' . $this->name . '</div>';
fwrite($outputStream, '<ul>');
foreach($this->children as $child){
fwrite($outputStream, '<li>');
$child->toHTMLOutput($outputStream);
fwrite($outputStream, '</li>');}
}
fwrite($outputStream, '</ul>');
}
$stdout = fopen('php://stdout', 'w');
print $root->toHTMLOutput($stdout);
或者如果您想将输出保存到文件
$stdout = fopen('htmloutput.html', 'w');
print $root->toHTMLOutput($stdout);
显然,我只为toHTML()
函数实现了它,但对build
函数也应该采用相同的原理,这可能会导致您跳过一个单独的toHTML函数。
简介
由于您将要输出HTML,因此不需要间接地保存它以消耗内存。
这里有一个简单的类:
- 从多维数组生成菜单
- 内存高效使用迭代器
- 可写入
Socket
、Stream
、File
、array
、Iterator
等
示例
$it = new ListBuilder(new RecursiveArrayIterator($big));
// Use Echo
$m = memory_get_peak_usage();
$it->display();
printf("%0.5fMB'n", (memory_get_peak_usage() - $m) / (1024 * 1024));
输出
0.03674MB
其他输出接口
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
简单比较
// Use Echo
$m = memory_get_peak_usage();
$it->display();
$responce['echo'] = sprintf("%0.5fMB'n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to Stream or File eg ( Socket or HTML file)
$m = memory_get_peak_usage();
$it->display(fopen("php://output", "w"));
$responce['stream'] = sprintf("%0.5fMB'n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to ArrayIterator
$m = memory_get_peak_usage();
$it->display($array = new ArrayIterator());
$responce['iterator'] = sprintf("%0.5fMB'n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to Array
$m = memory_get_peak_usage();
$it->display($array = []);
$responce['array'] = sprintf("%0.5fMB'n", (memory_get_peak_usage() - $m) / (1024 * 1024));
echo "'n'nResults 'n";
echo json_encode($responce, 128);
输出
Results
{
"echo": "0.03684MB'n",
"stream": "0.00081MB'n",
"iterator": "32.04364MB'n",
"array": "0.00253MB'n"
}
使用的类别
class ListBuilder extends RecursiveIteratorIterator {
protected $pad = "'t";
protected $o;
public function beginChildren() {
$this->output("%s<ul>'n", $this->getPad());
}
public function endChildren() {
$this->output("%s</ul>'n", $this->getPad());
}
public function current() {
$this->output("%s<li>%s</li>'n", $this->getPad(1), parent::current());
return parent::current();
}
public function getPad($n = 0) {
return str_repeat($this->pad, $this->getDepth() + $n);
}
function output() {
$args = func_get_args();
$format = array_shift($args);
$var = vsprintf($format, $args);
switch (true) {
case $this->o instanceof ArrayIterator :
$this->o->append($var);
break;
case is_array($this->o) || $this->o instanceof ArrayObject :
$this->o[] = $var;
break;
case is_resource($this->o) && (get_resource_type($this->o) === "file" || get_resource_type($this->o) === "stream") :
fwrite($this->o, $var);
break;
default :
echo $var;
break;
}
}
function display($output = null) {
$this->o = $output;
$this->output("%s<ul>'n", $this->getPad());
foreach($this as $v) {
}
$this->output("%s</ul>'n", $this->getPad());
}
}
结论
正如您所看到的,使用迭代器进行循环很快,但将值存储在迭代器或对象中可能没有那么高的内存效率。
数组中的元素总数略高于100000。
数组中的每个元素只有一个字节(布尔值),因此超过100000个元素需要100000字节~0.1MB
您的每个对象大约为100字节,它是100*100000=100000000字节~10MB
但是你有18MB,那么这个8是从哪里来的呢?
如果你运行这个代码
<?php
$c = 0; //we use this to count object isntances
class obj{
protected $name;
protected $children = array();
public static $c=0;
public function __construct($name){
global $c;
$c++;
$this->name = $name;
}
public static function build($name, $array = array()){
global $c;
$b = memory_get_usage();
$obj = new self($name);
$diff = memory_get_usage()-$b;
echo $c . ' diff ' . $diff . '<br />'; //display change in allocated size
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(){
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
}
}
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
$root = obj::build('root', $big);
您会注意到一个变化是恒定的,但创建为1024、2048、4096。。。
我没有任何关于它的文章或手册页面的链接,但我的猜测是php保存了对数组中初始大小为1024的每个创建对象的引用。当您使此阵列充满时,其大小将加倍,为新对象腾出空间。
如果你拿2048对象的差值减去对象的大小(你在其他行中的常数值),除以2048,你总是会得到32标准大小的指针
因此,对于100000个物体,这个阵列的大小增加到131072个元素。131072*32=4194304B=4MB
这个计算只是近似的,但我认为它回答了你的问题,为什么需要这么多内存。
为了回答如何保持低内存——避免对大数据集使用对象。
显然,对象很好,但原始数据类型更快、更小。
也许您可以让它与一个包含数据数组的对象一起工作。如果没有更多关于这些对象以及它们需要什么方法/接口的信息,很难提出任何替代方案。
有一件事可能会让你措手不及,那就是你可能因为递归而接近于破坏你的堆栈。在这种情况下,创建一个将树作为一个整体进行渲染的渲染函数可能是有意义的,而不是依赖递归来为您进行渲染。有关这方面的信息主题,请参阅尾调用递归和尾调用优化。
为了坚持代码的当前结构并避免您可能面临的许多资源问题,最简单的解决方案可能是简单地传入html字符串作为引用,如:
class obj{
protected $name;
protected $children = array();
public function __construct($name){
$this->name = $name;
}
public static function build($name, $array = array()){
$obj = new self($name);
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(&$html = ""){
$html .= '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child){
$html .= '<li>';
$html .= $child->toHTML($html);
$html .= '</li>';
}
$html .= '</ul>';
}
}
这将使您在解析递归调用时避免拖沓一堆重复的部分树渲染。
至于树的实际构建,我认为很多内存使用只是处理这么大数据的代价,你可以选择要么渲染,而不是建立一个层次模型来渲染(只是渲染输出,而不是构建一棵树),或者,根据数据在站点中的使用方式,使用某种缓存策略来缓存对象树的副本或渲染的html的副本。如果您可以控制入站数据,则可以将无效的相关缓存键添加到该工作流中,以防止缓存过时。