带有私有向量的 OpenMP .push_back 在循环完成后不会释放所有内存
OpenMP with private vector .push_back doesnt free all memory after loop finish
在并行 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 使用有问题的代码
代码输出:(循环计数,VIRT 内存,RES 内存)
------------------- 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] = '[=15=]';
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", "r");
int result = -1;
char line[128];
while (fgets(line, 128, file) != NULL)
{
if (strncmp(line, "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。简而言之,小分配是从称为竞技场的较大内存分配中切出的片段。当您释放这样的一块时,它会返回到它的竞技场并且该过程的 RSS 不会减少。使用匿名内存映射直接从 OS 获取更大的分配。释放大量分配会取消内存区域的映射,这会将 RSS 的值恢复为其先前的值,前提是没有新页面映射到其他内存区域。
之所以有两种不同的算法,是因为要求内核执行内存映射是一项缓慢的操作,因为它涉及修改进程页表。切出较大块的碎片完全发生在用户 space 中,但对于大型分配来说并不是最优的。
当您 push_back()
循环到 std::vector
而不先调整它的大小时,向量将根据需要连续重新分配,使其存储大小加倍。最初,分配将足够小,并且将来自竞技场,直到它到达从 OS 开始内存映射的点。当您执行 points.resize(2e7)
时,您会强制 vector 一次分配所有存储,这由匿名内存映射操作直接提供。
glibc 使用 per-thread arenas 并为每个线程创建一个新的 arena。如果您在主线程的最后释放块,主线程的竞技场可能会缩小。其他线程的竞技场不会轻易缩小。这是由于用于为竞技场分配内存的不同机制。主要的是通过移动程序中断(也称为数据段的末尾)来(取消)分配。其他竞技场使用匿名内存映射。
当 OpenMP 生成新线程时,它从一个小区域开始。当 vector 开始请求越来越大的内存块时,arena 会扩大以适应需求。有一次,向量变得如此之大以至于开始接收匿名内存映射。当这些映射不是太大时,不是在 vector 销毁时取消映射,而是将它们添加到 arena 作为未来内存分配的缓存(取消映射也非常昂贵)。不仅如此,切换到匿名内存映射的阈值也会向上更新。这就是为什么每个线程最终在每个区域中都有 32 MiB 的缓存内存,乘以 7 个线程得到 224 MiB。从主竞技场添加几个 MiB,您将获得 227 MiB。
如果在每个线程中缓存 32 MiB 的内存让您感到不安,只需告诉 glibc 不要通过 MALLOC_MMAP_THRESHOLD_
环境变量将其设置为固定值来动态增加内存映射阈值:
$ ./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 提供 non-standard malloc_stats()
调用(包括 malloc.h
),它打印关于竞技场的一般信息。
在并行 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 使用有问题的代码
代码输出:(循环计数,VIRT 内存,RES 内存)
------------------- 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] = '[=15=]';
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", "r");
int result = -1;
char line[128];
while (fgets(line, 128, file) != NULL)
{
if (strncmp(line, "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。简而言之,小分配是从称为竞技场的较大内存分配中切出的片段。当您释放这样的一块时,它会返回到它的竞技场并且该过程的 RSS 不会减少。使用匿名内存映射直接从 OS 获取更大的分配。释放大量分配会取消内存区域的映射,这会将 RSS 的值恢复为其先前的值,前提是没有新页面映射到其他内存区域。
之所以有两种不同的算法,是因为要求内核执行内存映射是一项缓慢的操作,因为它涉及修改进程页表。切出较大块的碎片完全发生在用户 space 中,但对于大型分配来说并不是最优的。
当您 push_back()
循环到 std::vector
而不先调整它的大小时,向量将根据需要连续重新分配,使其存储大小加倍。最初,分配将足够小,并且将来自竞技场,直到它到达从 OS 开始内存映射的点。当您执行 points.resize(2e7)
时,您会强制 vector 一次分配所有存储,这由匿名内存映射操作直接提供。
glibc 使用 per-thread arenas 并为每个线程创建一个新的 arena。如果您在主线程的最后释放块,主线程的竞技场可能会缩小。其他线程的竞技场不会轻易缩小。这是由于用于为竞技场分配内存的不同机制。主要的是通过移动程序中断(也称为数据段的末尾)来(取消)分配。其他竞技场使用匿名内存映射。
当 OpenMP 生成新线程时,它从一个小区域开始。当 vector 开始请求越来越大的内存块时,arena 会扩大以适应需求。有一次,向量变得如此之大以至于开始接收匿名内存映射。当这些映射不是太大时,不是在 vector 销毁时取消映射,而是将它们添加到 arena 作为未来内存分配的缓存(取消映射也非常昂贵)。不仅如此,切换到匿名内存映射的阈值也会向上更新。这就是为什么每个线程最终在每个区域中都有 32 MiB 的缓存内存,乘以 7 个线程得到 224 MiB。从主竞技场添加几个 MiB,您将获得 227 MiB。
如果在每个线程中缓存 32 MiB 的内存让您感到不安,只需告诉 glibc 不要通过 MALLOC_MMAP_THRESHOLD_
环境变量将其设置为固定值来动态增加内存映射阈值:
$ ./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 提供 non-standard malloc_stats()
调用(包括 malloc.h
),它打印关于竞技场的一般信息。