6

我有一个简单的对象事物,它能够拥有相同类型的孩子。

这个对象有一个 toHTML 方法,它执行如下操作:

$html  = '<div>' . $this->name . '</div>';
$html .= '<ul>';

foreach($this->children as $child)
  $html .= '<li>' . $child->toHTML() . '</li>';

$html .= '</ul>';

return $html;

问题是当对象很复杂时,比如很多有孩子的孩子等等,内存使用量会猛增。

如果我只是print_r提供这个对象的多维数组,我会得到 1 MB 的内存使用量,但是在我将数组转换为我的对象并执行print $root->toHtml()它之后需要 10 MB !

我怎样才能解决这个问题?

=====================================

制作了一个与我的真实代码相似的简单类(但更小):

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
4

4 回答 4

3

问题是您正在缓冲内存中的所有数据,实际上您并不需要这样做,因为您只是在输出数据,而不是实际处理它。

如果您只想输出它,而不是缓冲内存中的所有内容,您应该将其输出到它要去的任何地方:

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 函数。

于 2013-06-22T18:02:01.573 回答
1

可能会引起您注意的一件事是,由于递归,您可能快要炸掉您的堆栈了。在这种情况下,创建一个渲染函数来处理整个树以进行渲染而不是依赖递归来为您进行渲染可能是有意义的。有关这方面的信息主题,请参阅尾调用递归和尾调用优化。

要坚持代码的当前结构并避免您可能面临的许多资源问题,最简单的解决方案可能是简单地将 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 的副本。如果您可以控制入站数据,则可以将相关缓存键添加到该工作流程中,以防止缓存过时。

于 2013-06-30T05:41:16.087 回答
1

介绍

由于您将要输出 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());
    }
}

结论

如您所见,使用迭代器进行循环很快,但将值存储在迭代器或对象中可能没有那么高效的内存。

于 2013-06-27T23:39:38.693 回答
1

您的数组中的元素总数略高于 100000。

您的数组的每个元素只有一个字节(布尔值),因此对于超过 100000 个元素,它需要 100000bytes ~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 - C 中指针的标准大小。

所以对于 100000 个对象,这个数组增长到 131072 个元素的大小。131072*32 = 4194304B = 4MB

这个计算只是近似值,但我认为它回答了你的问题,什么需要这么多内存。

要回答如何保持低内存 - 避免将对象用于大量数据。

显然对象很好,但原始数据类型更快更小。

也许你可以让它与一个包含数据数组的对象一起工作。如果没有有关此对象及其所需方法/接口的更多信息,很难提出任何替代方案。

于 2013-06-24T23:11:30.877 回答