如何解决带有私有向量.push_back的OpenMP在循环完成后不会释放所有内存
在并行for循环中,使用push_back填充私有向量会导致在循环结束并删除对向量的所有引用后留下大量内存。
std::vector<float> points;
for (int k = 0; k < 2e7; k++)
{
points.push_back(k);
}
虽然以这种方式填充它,但在循环结束并退出作用域之后,并没有留下任何明显的内存。
std::vector<float> points;
points.resize(2e7);
for (int k = 0; k < 2e7; k++)
{
points[k] = k;
}
关于这种内存使用原因的任何想法以及如何清除它?
使用pcl库,我的实际用例更加复杂,而且似乎没有问题,我在并行循环内生成点云,这会留下过多的内存。
下面是我管理过的最简单的示例,演示了该问题。 前5个循环使用预分配的代码,然后5-20循环使用有问题的代码
------------------- omp_test START -------------------
0 endofloop 510 MB 3 MB
1 endofloop 510 MB 3 MB
2 endofloop 510 MB 3 MB
3 endofloop 510 MB 3 MB
4 endofloop 510 MB 3 MB
5 endofloop 510 MB 3 MB
6 endofloop 510 MB 227 MB
7 endofloop 510 MB 227 MB
8 endofloop 510 MB 227 MB
9 endofloop 510 MB 227 MB
10 endofloop 510 MB 227 MB
11 endofloop 510 MB 227 MB
12 endofloop 510 MB 227 MB
13 endofloop 510 MB 227 MB
14 endofloop 510 MB 227 MB
15 endofloop 510 MB 227 MB
16 endofloop 510 MB 227 MB
17 endofloop 510 MB 227 MB
18 endofloop 510 MB 227 MB
19 endofloop 510 MB 227 MB
20 endofloop 510 MB 227 MB
21 endofloop 510 MB 227 MB
22 endofloop 510 MB 227 MB
23 endofloop 510 MB 227 MB
24 endofloop 510 MB 227 MB
25 endofloop 510 MB 227 MB
26 endofloop 510 MB 227 MB
27 endofloop 510 MB 227 MB
28 endofloop 510 MB 227 MB
29 endofloop 510 MB 227 MB
30 endofloop 510 MB 227 MB
31 endofloop 510 MB 227 MB
32 endofloop 510 MB 227 MB
33 endofloop 510 MB 227 MB
34 endofloop 510 MB 227 MB
35 endofloop 510 MB 227 MB
36 endofloop 510 MB 227 MB
37 endofloop 510 MB 227 MB
38 endofloop 510 MB 227 MB
39 endofloop 510 MB 227 MB
40 endofloop 510 MB 227 MB
41 endofloop 510 MB 227 MB
42 endofloop 510 MB 227 MB
我已经在Ubuntu上使用g ++ 10和9。
g++ -fopenmp -DNDEBUG -O2 nestedtest.cpp && ./a.out
nestedtest.cpp
#include <iostream>
#include <stdlib.h>
#include <omp.h>
#include <vector>
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include <ctime>
#include <memory>
#include <unistd.h>
int parseLine(char *line) //functions for reading memory usage
{
// This assumes that a digit will be found and the line ends in " Kb".
int i = strlen(line);
const char *p = line;
while (*p < '0' || *p > '9')
p++;
line[i - 3] = '\0';
i = atoi(p);
return i;
}
int getValue() //functions for reading memory usage
{ //Note: this value is in KB!
FILE *file = fopen("/proc/self/status","r");
int result = -1;
char line[128];
while (fgets(line,128,file) != NULL)
{
if (strncmp(line,"VmSize:",7) == 0)
{
result = parseLine(line);
break;
}
}
fclose(file);
return result;
}
int getValueP() //functions for reading memory usage
{ //Note: this value is in KB!
FILE *file = fopen("/proc/self/status","VmRSS:",6) == 0)
{
result = parseLine(line);
break;
}
}
fclose(file);
return result;
}
int main(int argc,char *argv[])
{
std::cout << "------------------- omp_test START -------------------" << std::endl;
for (int i = 0; i < 100; i++)
{
#pragma omp parallel for schedule(dynamic,1) num_threads(8)
// #pragma omp parallel for schedule(dynamic) firstprivate(points)
for (int j = 0; j < 20; j++)
{
if (i > 5 && i < 20)
{
std::vector<float> points;
for (int k = 0; k < 2e7; k++)
{
points.push_back(k);
}
}
else
{
std::vector<float> points;
points.resize(2e7);
for (int k = 0; k < 2e7; k++)
{
points[k] = k;
}
}
}
#pragma omp critical
{
usleep(10000);
std::cout << i << "\tendofloop " << getValue() / 1024 << " MB\t" << (getValueP() / 1024) << " MB" << std::endl;
}
}
exit(0);
}
// end: main()
解决方法
您观察到的只是内存分配的工作原理。
glibc使用两种不同类型的分配,详细说明here。简而言之,小的分配就是从称为arenas的较大的内存分配中切出的片段。当您释放此类作品时,它会返回到其舞台上,并且流程的RSS不会减少。使用匿名内存映射直接从OS中获取更大的分配。释放大量分配会取消对内存区域的映射,只要没有新页面映射到其他内存区域,RSS的值就会恢复为之前的值。
之所以使用两种不同的算法,是因为要求内核执行内存映射是一项缓慢的操作,因为它涉及到对进程页表的修改。切出较大块的片段完全是在用户空间中进行的,但是对于较大的分配来说不是最佳选择。
当您在push_back()
的循环中std::vector
而不先调整其大小时,向量将根据需要连续重新分配,使其存储大小增加一倍。最初,分配将足够小并且将来自竞技场,直到到达从操作系统开始内存映射的地步。当您执行points.resize(2e7)
时,您将强制向量一次分配所有存储,这直接由匿名内存映射操作提供服务。
glibc使用每个线程的竞技场,并为每个线程创建一个新的竞技场。如果在主线程末端释放了块,则主线程的竞技场可能会缩小。其他线程的领域并不那么容易缩小。这是由于用于竞技场分配内存的机制不同。通过移动程序中断(也称为数据段的末尾)来(取消)分配主要的中断。其他领域则使用匿名内存映射。
当OpenMP产生一个新线程时,它从一个小舞台开始。当向量开始要求越来越大的存储块时,竞技场将增长以适应需求。一方面,向量变得如此之大,以至于开始接收匿名内存映射。当这些映射不太大时,与其在破坏向量时取消对它们的映射,不如将它们添加到舞台上作为缓存以供将来的内存分配(取消映射也非常昂贵)。不仅如此,切换到匿名内存映射的阈值也被向上更新。这就是每个线程最终在每个竞技场中拥有32 MiB高速缓存内存的原因,将其乘以7个线程得出224 MiB。从主竞技场添加几个MiB,您将获得227 MiB。
如果在每个线程中缓存32 MiB的内存使您感到不安,只需通过MALLOC_MMAP_THRESHOLD_
环境变量将glibc设置为固定值,即可告诉glibc不要动态增加内存映射阈值:
$ ./a.out
...
5 endofloop 524 MB 3 MB
6 endofloop 524 MB 227 MB
...
$ MALLOC_MMAP_THRESHOLD_=4194304 ./a.out
...
5 endofloop 524 MB 3 MB
6 endofloop 524 MB 4 MB
...
这样做可能会对性能产生负面影响。
glibc提供了非标准的malloc_stats()
调用(包括malloc.h
),该调用将打印出有关竞技场的常规信息。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。