Описание
При распределении и освобождении фрагментов памяти произвольного размера с 4 или более потоками с использованием openmp для построения, программа, похоже, начинает пропускать значительные объемы памяти во второй половине тестовой программы. Таким образом, он увеличивает свою потребляемую память от 1050 МБ до 1500 МБ или более без фактического использования дополнительной памяти.
Поскольку valgrind не показывает никаких проблем, я должен предположить, что то, что кажется утечкой памяти, фактически является подчеркнутым эффектом фрагментации памяти.
Интересно, что эффект пока не проявляется, если 2 потока составляют 10000 распределений каждый, но он сильно показывает, если 4 потока составляют 5000 распределений каждый. Кроме того, если максимальный размер выделенных блоков уменьшается до 256 кб (от 1 мб), эффект становится слабее.
Может ли тяжелый concurrency сделать так много фрагментации? Или это скорее будет ошибкой в куче?
Описание тестовой программы
Демонстрационная программа построена для получения в общей сложности 256 Мбайт блоков памяти произвольного размера из кучи, делая 5000 распределений. Если предел памяти достигнут, выделенные фрагменты будут освобождены до тех пор, пока потребление памяти не опустится ниже предела. После 5000 распределений, где выполняется, вся память освобождается и цикл завершается. Вся эта работа выполняется для каждого потока, генерируемого openmp.
Эта схема распределения памяти позволяет нам рассчитывать на потребление памяти ~ 260 МБ на поток (включая некоторые данные бухгалтерского учета).
Демо-программа
Поскольку это действительно то, что вы, возможно, захотите проверить, вы можете загрузить образец программы с помощью простого make файла из dropbox.
При запуске программы как есть, вы должны иметь как минимум 1400 МБ ОЗУ. Не стесняйтесь настраивать константы в коде в соответствии с вашими потребностями.
Для полноты, фактический код следует:
#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 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
Тестирование
После выполнения make файла вы должны получить файл с именем ompmemtest
. Чтобы запросить использование памяти во времени, я использовал следующие команды:
./ompmemtest &
top -b | grep ompmemtest
Это дает весьма впечатляющую фрагментацию или протекание. Ожидаемое потребление памяти с 4 потоками составляет 1090 МБ, которое со временем стало 1500.
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).