【问题标题】:delete [] performance issues删除 [] 性能问题
【发布时间】:2011-06-17 18:21:53
【问题描述】:

我写了一个程序,计算流水车间调度问题。

我需要帮助来优化我的程序中最慢的部分:

首先是数组二维数组分配:

this->_perm = new Chromosome*[f];

//... for (...)

this->_perm[i] = new Chromosome[fM1];

它工作得很好,但是当我尝试删除数组时出现问题:

delete [] _perm[i];

执行上面的行需要很长时间。 Chromosome 是由大约 300k 个元素组成的数组 - 分配它需要不到一秒的时间,但删除需要一分钟以上。

如果有任何改进删除部分的建议,我将不胜感激。

【问题讨论】:

  • Chromosome的析构函数中有什么昂贵的东西吗?
  • 嗯,_perm 是染色体的二维数组,其中每个染色体都包含 Gene* 基因的数组;。要安排的任务与基因一样多。每个 Gene 包含两个数组:int start[2] 和 int end[2]。
  • 显而易见的问题:为什么需要这数百万的分配?为什么你需要指针?难道不能全部存储在少量对象中,具有更好的缓存局部性和启动性能吗?
  • 您的构造函数或析构函数中是否有任何磁盘或其他 I/O?这是除了每个元素多次调用析构函数之外唯一会导致构造和破坏的巨大差异的事情。另外,这是在什么平台上运行的? PC,嵌入式?

标签: c++ arrays performance memory-management


【解决方案1】:

一般来说,您应该永远不要在 C++ 中手动管理内存。这将导致泄漏、双重删除和所有kinds of nasty inconveniences。为此使用适当的资源处理类。例如,std::vector 用于管理动态分配的数组。


要回到手头的问题,您首先需要知道delete [] _perm[i] 做了什么:它为该数组中的每个Chromosome 对象调用析构函数,然后释放内存。现在您在循环中执行此操作,这意味着这将调用 all Chromosome 析构函数并执行 f 释放。正如在对您的问题的评论中已经提到的那样,非常Chromosome析构函数是真正的罪魁祸首。尝试对此进行调查。


但是,您可以更改内存处理以提高分配和解除分配的速度。 As Nawaz has shown,您可以分配一大块内存并使用它。我会使用std::vector 作为缓冲区:

void f(std::size_t row, std::size_t col)
{
  int sizeMemory = sizeof(Chromosome) * row * col;
  std::vector<unsigned char> buffer(sizeMemory); //allocation of memory at once!

  vector<Chromosome*> chromosomes(row);

  // use algorithm as shown by Nawaz
  std::size_t j = 0 ;
  for(std::size_t i = 0 ; i < row ; i++ )
  {
      //...
  }

  make_baby(chromosomes); //use chromosomes

  in_place_destruct(chromosomes.begin(), chromosomes.end());

  // automatic freeing of memory holding pointers in chromosomes
  // automatic freeing of buffer memory
}

template< typename InpIt >
void in_place_destruct(InpIt begin, InpIt end)
{
  typedef std::iterator_traits<InpIt>::value_type value_type; // to call dtor
  while(begin != end)
    (begin++)->~value_type(); // call dtor
}

然而,尽管通过 std::vector 处理所有内存这仍然不是完全异常安全的,因为它需要显式调用 Chromosome 析构函数。 (如果make_baby() 抛出异常,函数f() 将提前中止。虽然向量的析构函数将删除它们的内容,但一个只包含指针,另一个将其内容视为原始内存。没有守卫在看在该原始内存中创建的实际对象。)


我能看到的最佳解决方案是使用一维数组包装在一个允许二维访问 到该数组中的元素。 (毕竟,在当前硬件上,内存是一维的,所以系统已经在这样做了。)这是一个草图:

class chromosome_matrix {
public:
  chromosome_matrix(std::size_t row, std::size_t col)
   : row_(row), col_(col), data_(row*col)
  {
    // data_ contains row*col constructed Chromosome objects
  }

  // note needed, compiler generated dtor will do the right thing
  //~chromosome_matrix()

   // these rely on pointer arithmetic to access a column
        Chromosome* operator[](std::size_t row)       {return &data_[row*col_];}
  const Chromosome* operator[](std::size_t row) const {return &data_[row*col_];}

private:
  std::size_t row_;
  std::size_t col_;
  std::vector<chromosomes> data_
};

void f(std::size_t row, std::size_t col)
{
  chromosome_matrix cm(row, col);

  Chromosome* column = ch[0];          // get a whole column
  Chromosome& chromosome1 = column[0]; // get one object

  Chromosome& chromosome2 = cm[1][2];  // access object directly

  // make baby
}

【讨论】:

    【解决方案2】:

    检查你的析构函数。

    如果您分配的是内置类型(例如 int),那么分配其中的 300,000 个将比相应的删除更昂贵。但这是一个相对术语,在单个块中分配 300k 是相当快的。

    当您分配 300k 染色体时,分配器必须分配 300k * 大小的 Chromosome 对象,并且正如您所说的那样快-除此之外我看不到它做太多事情(即构造函数调用被优化为虚无) )

    然而,当你开始删除时,它不仅释放了所有内存,而且还为每个对象调用了析构函数,如果它很慢,我猜每个对象的析构函数占用了一个很小但很明显的,当你有 300k 的时候。

    【讨论】:

    • 如果 ctor 的性能影响可以忽略不计,那么 dtor 执行任何明显的操作并非不可能,而是不寻常的。
    • 不一定 - 有一个范式,其中 ctor 为空,然后在构造后通过 Initialise 调用初始化对象。我见过它,甚至可能用过一次。
    • 即使你有两阶段构造(哦,太可怕了!),初始化必须以某种方式发生。正如我所说,构建初始化比反初始化便宜得多的样本很容易,但 IME 在实践中很少见。
    • 这取决于您在班级内的数据存储。在实践中发生的事情并不重要,重要的是在提问者的课堂上发生了什么。因为他使用 new 来分配对象数组(而不是向量),所以我猜他在类中有 POD 类型,所以分配应该像单个 malloc 一样快!我还假设他正在进行大量检查、扩展清理其他对象,甚至登录他的 dtor 导致问题。
    【解决方案3】:

    我建议您使用新展示位置。分配和释放都可以在一个语句中完成!

    int sizeMemory = sizeof(Chromosome) * row * col;
    char* buffer = new char[sizeMemory]; //allocation of memory at once!
    
    vector<Chromosome*> chromosomes;
    chromosomes.reserve(row);
    int j = 0 ;
    for(int i = 0 ; i < row ; i++ )
    {
        //only construction of object. No allocation!
        Chromosome *pChromosome = new (&buffer[j]) Chromosome[col]; 
        chromosomes.push_back(pChromosome);
        j = j+ sizeof(Chromosome) * col;
    }
    
    for(int i = 0 ; i < row ; i++ )
    {
          for(int j = 0 ; j < col ; j++ )
          {
             //only destruction of object. No deallocation!
             chromosomes[i][j].~Chromosome();
          }
    }
    delete [] buffer; //actual deallocation of memory at once!
    

    【讨论】:

    • 问题是我不能一次完成所有事情——这就是为什么我必须在与使用 new 相同的循环中使用 delete。我的程序首先是 Flow Shop 的遗传算法,但现在我在相同的结构上编写蛮力(因为大多数表示保持不变)。对于 5 个任务,有 factorial(nTasks) * factorial(nTasks+ (nTasks- 1)) 可能性 - 我没有足够的 RAM 来一次分配所有任务
    • @Overpain:内存分配和释放可以一次完成。至少这部分可以快速完成,而无需太多了解您的情况。其他一切都在您的控制之下。您可以随意调用对象的析构函数;也可以一一删除!
    • @Nawaz:我不知道Chromosome ctor 是否会抛出异常,更不用说实际使用这些染色体的代码了。这就是为什么我会坚持使用异常安全的解决方案。 (即使代码现在永远不能抛出,它可能在维护之后。)你不能轻易地使这个异常安全,因为需要执行破坏所有对象的循环。解决这个问题的最好方法是将该循环放入类的析构函数中。但是一旦你这样做了,你也可以让二维访问成为一个围绕一维数组的纯包装器。看我的回答。
    • @sbi:但无论如何你都必须破坏这些对象;所以当你说如果我在循环中破坏对象时它不是异常安全的,这让我感到困惑。您能否进一步解释一下它与将此循环放入类的析构函数有何不同?
    • @Nawaz:您的代码所做的就是构造所有这些对象,然后立即销毁它们。一方面,如果其中一个 ctor 抛出,您将不得不调用迄今为止构建的所有对象的 dtor,但不能调用那些从未构建过的对象。此外,在构造和销毁之间可能会有代码使用这些对象。这也可能会抛出,在这种情况下,所有对象都需要调用它们的 dtor。在这两种情况下,您的代码都不会破坏任何对象。这是错误的 - 尤其如此,因为他们所在的内存将被自动销毁。
    【解决方案4】:

    std::vector 可以提供帮助。

    还有特殊的内存分配器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-31
      • 1970-01-01
      • 2014-05-29
      • 2010-10-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多