【问题标题】:Reducing memory footprint of c++ program utilising large vectors利用大向量减少 c++ 程序的内存占用
【发布时间】:2019-09-01 09:26:11
【问题描述】:

在将问题规模扩大到我交给一个自编码程序的过程中,我开始遇到 Linux 的 OOM 杀手。 Valgrind(在 CPU 上运行时)和 cuda-memcheck(在 GPU 上运行时)都不会报告任何内存泄漏。在遍历内部循环时,内存使用量不断扩大,而我在此循环结束时明确清除了保存最大数据块的向量。如何确保这种内存占用消失?

已执行内存泄漏检查,所有内存泄漏均已修复。尽管如此,内存不足错误仍会继续终止程序(通过 OOM Killer)。手动监控内存消耗显示内存利用率增加,即使在明确清除包含数据的向量之后也是如此。

要知道的关键是有三个嵌套循环,一个外部包含手头的子问题。中间循环循环蒙特卡洛试验,内部循环运行试验内部所需的一些顺序过程。伪代码如下:

std::vector<object*> sub_problems;

sub_problems.push_back(retrieved_subproblem_from_database);

for(int sub_problem_index = 0; sub_problem_index < sub_problems.size(); ++sub_problem_index){
  std::vector< std::vector<float> > mc_results(100000, std::vector<float>(5, 0.0));
  for(int mc_trial = 0; mc_trial < 100000; ++mc_trial){
    for(int sequential_process_index = 0; sequential_process_index < 5; ++sequential_process_index){
      mc_results[mc_trial][sequential_process_index] = specific_result;
    }
  }

  sub_problems[sub_problem_index]->storeResultsInObject(mc_results);
  // Do some other things
  sub_problems[sub_problem_index]->deleteMCResults();
}

deleteMCResults 如下所示:

bool deleteMCResults() {
  for (int i = 0; i < asset_values.size(); ++i){
    object_mc_results[i].clear();
    object_mc_results[i].shrink_to_fit();
  }
  object_mc_results.clear();
  object_mc_results.shrink_to_fit();
  return true;
}

如何确保内存消耗完全依赖于中间和内循环而不是外循环?第二个、第三个和第四个等理论上可以使用与第一次迭代完全相同的内存空间/地址。

【问题讨论】:

  • std::vector&lt;object*&gt; sub_problems; - 这些是new'ed 对象吗? IE。每个object 的堆分配?因为这会带来每次分配的开销。 sn-p 没有显示这是否是问题;一些大物体不是问题,但许多小物体是问题。
  • 请注意,shrink_to_fit 不需要这样做。对嵌套向量做任何事情都没有意义,因为它们在循环后被销毁。

标签: c++ linux memory-management out-of-memory


【解决方案1】:

也许我从字面上看你的伪代码,但看起来你有两个mc_results 变量,一个在for 循环中声明,另一个是deleteMCResults 正在访问。

无论如何,我有两个关于如何调试的建议。首先,不要让OOM killer 罢工,这需要很长时间,是不可预测的,并且可能会杀死一些重要的东西,而是使用ulimit -v 来限制进程大小。将其设置为合理的值,例如 1000000(约 1GB),并努力将您的进程保持在该值之下。

其次,开始删除或注释掉除分配和释放内存的程序部分之外的所有内容。要么你会找到你的罪魁祸首,要么你会制作一个小到足以发布整个程序的程序。

【讨论】:

  • 感谢您的建议。只是为了澄清变量:结果存储在对象的实例中(由 ref 传递,然后是一个简单的 private_var = 参数。使用相同的名称有点误导,毕竟我是从对象实例的本地范围,它存储为私有 var。在对其进行一些额外分析后,我删除了上述结果。实际上,向量的向量可以最大为 500MB。
【解决方案2】:

deleteMCResults() 可以写得简单很多。

void deleteMCResults() {
  decltype(object_mc_results) empty;
  std::swap(object_mc_results, empty);
}

但是在这种情况下,我想知道您是否真的要释放内存。正如你所说,迭代可以重用相同的内存,所以也许你应该用returnMCResultsMemory() 替换deleteMCResults()。然后将mc_results 的声明提升出循环,并在returnMCResultsMemory() 返回后将其值重置为5.0。

【讨论】:

    【解决方案3】:

    您展示的代码可以轻松改进一件事。但是,要进行全面分析,这确实是不够的,也不够精确。提取相关示例 ([mcve]) 并在 codereview.stackexchange.com 上请求审查可能会改善结果。

    可以做的简单的事情是用五个浮点数的数组替换五个浮点数的内部向量。每个向量(在典型实现中)由三个指针组成,指向分配内存的开始和结束,另一个用于标记已使用量。实际存储需要单独分配,这反过来会产生一些开销(以及访问数据时的性能开销,关键字“引用位置”)。这三个指针在普通 64 位机器上需要 24 个八位字节。与五个浮点数相比,它们只需要 20 个八位字节。即使将这些浮点数填充到 24 个八位字节,您仍然可以从省略单独分配中受益。

    为了试试这个,只需将内部向量替换为std::array (https://en.cppreference.com/w/cpp/container/array)。奇怪的是您不必更改太多代码,原始数组,std::arraystd::vector 具有非常相似的接口。

    【讨论】:

    • 好建议!将它们更改为数组,好点。所以总的来说,这个向量的向量,或数组的向量,或数组的数组,可以是 2mln * 60 的维度,用浮点数填充。即使包括开销,这也不应该超过 4gb。 free -m 在每次中间循环迭代时返回一个不断增长的堆栈,使用量超过 30gb!成功的程序终止释放所有内存。这不能仅仅用 STL 的选择来解释,对吧?
    • 不,不是。如前所述,信息不足。尽管如此,一些可疑的事情仍然存在,例如使用原始指针,这总是很危险的。此外,如果您已经知道泄漏发生的位置并且在关机时正确释放,那么您可以编造一个minimal reproducible example
    猜你喜欢
    • 1970-01-01
    • 2016-11-09
    • 2012-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多