【发布时间】:2011-08-18 01:45:52
【问题描述】:
说明
当使用 openmp 的 parallel for 构造分配和释放具有 4 个或更多线程的随机大小的内存块时,程序似乎在 test-program's 运行时的后半部分开始泄漏大量内存。因此,它将消耗的内存从 1050 MB 增加到 1500 MB 或更多,而不会实际使用额外的内存。
由于 valgrind 没有显示任何问题,我必须假设看似内存泄漏实际上是内存碎片的突出影响。
有趣的是,如果 2 个线程每个进行 10000 次分配,效果还没有显示出来,但是如果 4 个线程每个进行 5000 次分配,效果就很明显了。此外,如果分配的块的最大大小减少到 256kb(从 1mb),效果会变弱。
重并发可以那么强调碎片化吗?还是这更有可能是堆中的错误?
测试程序说明
构建演示程序以从堆中获取总共 256 MB 随机大小的内存块,执行 5000 次分配。如果达到内存限制,首先分配的块将被释放,直到内存消耗低于限制。一旦执行了 5000 次分配,所有内存都会被释放并且循环结束。所有这些工作都是针对 openmp 生成的每个线程完成的。
这种内存分配方案允许我们预计每个线程的内存消耗约为 260 MB(包括一些簿记数据)。
演示程序
由于这确实是您可能想要测试的东西,您可以使用简单的 makefile 从dropbox 下载示例程序。
按原样运行程序时,您应该至少有 1400 MB 的可用 RAM。随意调整代码中的常量以满足您的需要。
为了完整起见,实际代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <vector>
#include <deque>
#include <omp.h>
#include <math.h>
typedef unsigned long long uint64_t;
void runParallelAllocTest()
{
// constants
const int NUM_ALLOCATIONS = 5000; // alloc's per thread
const int NUM_THREADS = 4; // how many threads?
const int NUM_ITERS = NUM_THREADS;// how many overall repetions
const bool USE_NEW = true; // use new or malloc? , seems to make no difference (as it should)
const bool DEBUG_ALLOCS = false; // debug output
// pre store allocation sizes
const int NUM_PRE_ALLOCS = 20000;
const uint64_t MEM_LIMIT = (1024 * 1024) * 256; // x MB per process
const size_t MAX_CHUNK_SIZE = 1024 * 1024 * 1;
srand(1);
std::vector<size_t> allocations;
allocations.resize(NUM_PRE_ALLOCS);
for (int i = 0; i < NUM_PRE_ALLOCS; i++) {
allocations[i] = rand() % MAX_CHUNK_SIZE; // use up to x MB chunks
}
#pragma omp parallel num_threads(NUM_THREADS)
#pragma omp for
for (int i = 0; i < NUM_ITERS; ++i) {
uint64_t long totalAllocBytes = 0;
uint64_t currAllocBytes = 0;
std::deque< std::pair<char*, uint64_t> > pointers;
const int myId = omp_get_thread_num();
for (int j = 0; j < NUM_ALLOCATIONS; ++j) {
// new allocation
const size_t allocSize = allocations[(myId * 100 + j) % NUM_PRE_ALLOCS ];
char* pnt = NULL;
if (USE_NEW) {
pnt = new char[allocSize];
} else {
pnt = (char*) malloc(allocSize);
}
pointers.push_back(std::make_pair(pnt, allocSize));
totalAllocBytes += allocSize;
currAllocBytes += allocSize;
// fill with values to add "delay"
for (int fill = 0; fill < (int) allocSize; ++fill) {
pnt[fill] = (char)(j % 255);
}
if (DEBUG_ALLOCS) {
std::cout << "Id " << myId << " New alloc " << pointers.size() << ", bytes:" << allocSize << " at " << (uint64_t) pnt << "\n";
}
// free all or just a bit
if (((j % 5) == 0) || (j == (NUM_ALLOCATIONS - 1))) {
int frees = 0;
// keep this much allocated
// last check, free all
uint64_t memLimit = MEM_LIMIT;
if (j == NUM_ALLOCATIONS - 1) {
std::cout << "Id " << myId << " about to release all memory: " << (currAllocBytes / (double)(1024 * 1024)) << " MB" << std::endl;
memLimit = 0;
}
//MEM_LIMIT = 0; // DEBUG
while (pointers.size() > 0 && (currAllocBytes > memLimit)) {
// free one of the first entries to allow previously obtained resources to 'live' longer
currAllocBytes -= pointers.front().second;
char* pnt = pointers.front().first;
// free memory
if (USE_NEW) {
delete[] pnt;
} else {
free(pnt);
}
// update array
pointers.pop_front();
if (DEBUG_ALLOCS) {
std::cout << "Id " << myId << " Free'd " << pointers.size() << " at " << (uint64_t) pnt << "\n";
}
frees++;
}
if (DEBUG_ALLOCS) {
std::cout << "Frees " << frees << ", " << currAllocBytes << "/" << MEM_LIMIT << ", " << totalAllocBytes << "\n";
}
}
} // for each allocation
if (currAllocBytes != 0) {
std::cerr << "Not all free'd!\n";
}
std::cout << "Id " << myId << " done, total alloc'ed " << ((double) totalAllocBytes / (double)(1024 * 1024)) << "MB \n";
} // for each iteration
exit(1);
}
int main(int argc, char** argv)
{
runParallelAllocTest();
return 0;
}
测试系统
就我目前所见,硬件非常重要。如果在更快的机器上运行,测试可能需要调整。
Intel(R) Core(TM)2 Duo CPU T7300 @ 2.00GHz
Ubuntu 10.04 LTS 64 bit
gcc 4.3, 4.4, 4.6
3988.62 Bogomips
测试
一旦你执行了makefile,你应该得到一个名为ompmemtest的文件。为了查询一段时间内的内存使用情况,我使用了以下命令:
./ompmemtest &
top -b | grep ompmemtest
这会产生令人印象深刻的碎片或泄漏行为。 4 个线程的预期内存消耗为 1090 MB,随着时间的推移变为 1500 MB:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11626 byron 20 0 204m 99m 1000 R 27 2.5 0:00.81 ompmemtest
11626 byron 20 0 992m 832m 1004 R 195 21.0 0:06.69 ompmemtest
11626 byron 20 0 1118m 1.0g 1004 R 189 26.1 0:12.40 ompmemtest
11626 byron 20 0 1218m 1.0g 1004 R 190 27.1 0:18.13 ompmemtest
11626 byron 20 0 1282m 1.1g 1004 R 195 29.6 0:24.06 ompmemtest
11626 byron 20 0 1471m 1.3g 1004 R 195 33.5 0:29.96 ompmemtest
11626 byron 20 0 1469m 1.3g 1004 R 194 33.5 0:35.85 ompmemtest
11626 byron 20 0 1469m 1.3g 1004 R 195 33.6 0:41.75 ompmemtest
11626 byron 20 0 1636m 1.5g 1004 R 194 37.8 0:47.62 ompmemtest
11626 byron 20 0 1660m 1.5g 1004 R 195 38.0 0:53.54 ompmemtest
11626 byron 20 0 1669m 1.5g 1004 R 195 38.2 0:59.45 ompmemtest
11626 byron 20 0 1664m 1.5g 1004 R 194 38.1 1:05.32 ompmemtest
11626 byron 20 0 1724m 1.5g 1004 R 195 40.0 1:11.21 ompmemtest
11626 byron 20 0 1724m 1.6g 1140 S 193 40.1 1:17.07 ompmemtest
请注意:我可以在使用 gcc 4.3、4.4 和 4.6(trunk) 进行编译时重现此问题。
【问题讨论】:
-
我想你会想使用谷歌的 tcmalloc(请参阅答案中的配置文件数据)
-
这是一个高度综合的测试,堆管理器的编写是为了利用程序不分配随机大小的内存块。碎片化肯定是个问题。更多的线程会更快地分裂。
-
这个测试确实是合成的,但它是为了弄清楚为什么我们的实际程序会出现泄漏,尽管 valgrind 没有找到任何东西。如果使用更多线程,它只会显示泄漏/碎片。由于此测试很好地重现了该问题,因此非常适合其预期目的。
-
纯属轶事,但我职业生涯的大部分时间都在金融行业编写大量多线程的 24/7 服务器,内存碎片从来都不是问题。
-
周围有许多内存分配程序(Hoard、ptmalloc、tcmalloc 等)可用于线程应用程序 - 每个程序都有一些优点和缺点,具体取决于您在做什么。前几天我在locklessinc.com/benchmarks.shtml 看到了一些你可能会感兴趣的比较。
标签: c++ multithreading memory openmp fragmentation