Description
Lors de l'allocation et de la désallocation de morceaux de mémoire de taille aléatoire avec 4 threads ou plus en utilisant la construction parallèle d'openmp, le programme semble commencer à fuir des quantités considérables de mémoire dans la seconde moitié de la période d'allocation et de désallocation. du programme de test durée d'exécution. La mémoire consommée passe ainsi de 1 050 Mo à 1 500 Mo, voire plus, sans que la mémoire supplémentaire soit réellement utilisée.
Comme valgrind ne montre aucun problème, je dois supposer que ce qui semble être une fuite de mémoire est en fait un effet accentué de la fragmentation de la mémoire.
Il est intéressant de noter que l'effet n'apparaît pas encore si 2 threads effectuent 10000 allocations chacun, mais qu'il apparaît fortement si 4 threads effectuent 5000 allocations chacun. De même, si la taille maximale des blocs alloués est réduite à 256 kb (au lieu de 1mb), l'effet s'affaiblit.
Une forte concurrence peut-elle accentuer la fragmentation à ce point ? Ou s'agit-il plutôt d'un bogue dans le tas ?
Description du programme d'essai
Le programme de démonstration est construit pour obtenir un total de 256 Mo de morceaux de mémoire de taille aléatoire à partir du tas, en effectuant 5000 allocations. Si la limite de mémoire est atteinte, les blocs alloués en premier seront désalloués jusqu'à ce que la consommation de mémoire tombe en dessous de la limite. Une fois les 5000 allocations effectuées, toute la mémoire est libérée et la boucle se termine. Tout ce travail est effectué pour chaque thread généré par openmp.
Ce schéma d'allocation de la mémoire nous permet de tabler sur une consommation de mémoire d'environ 260 Mo par thread (y compris certaines données de gestion).
Programme de démonstration
Comme c'est vraiment quelque chose que vous pourriez vouloir tester, vous pouvez télécharger le programme d'exemple avec un simple fichier makefile à l'adresse suivante boîte de dépôt .
Lorsque vous exécutez le programme tel quel, vous devez disposer d'au moins 1400 Mo de mémoire vive. N'hésitez pas à adapter les constantes du code à vos besoins.
Par souci d'exhaustivité, le code réel suit :
#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;
}
Le système d'essai
D'après ce que j'ai vu jusqu'à présent, le matériel compte beaucoup. Le test pourrait nécessiter des ajustements s'il était exécuté sur une machine plus rapide.
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
Essais
Une fois le fichier makefile exécuté, vous devriez obtenir un fichier nommé ompmemtest
. Pour interroger l'utilisation de la mémoire au fil du temps, j'ai utilisé les commandes suivantes :
./ompmemtest &
top -b | grep ompmemtest
Ce qui donne le résultat assez impressionnant de fragmentation ou des fuites. La consommation de mémoire attendue avec 4 threads est de 1090 MB, qui est devenu 1500 MB au fil du temps :
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
A noter : J'ai pu reproduire ce problème en compilant avec gcc 4.3, 4.4 et 4.6(trunk) .
0 votes
Je pense que vous voudrez utiliser tcmalloc de google (voir les données du profil dans la réponse).
2 votes
Il s'agit d'un test très synthétique, les gestionnaires de tas ayant été écrits pour tirer parti des programmes pas l'allocation de morceaux de mémoire de taille aléatoire. La fragmentation sera certainement un problème. Et plus il y a de threads, plus la fragmentation est rapide.
1 votes
Ce test est en effet synthétique, mais il a été écrit pour comprendre pourquoi notre programme actuel semble fuir, bien que valgrind n'ait rien trouvé. Il ne montre la fuite/fragmentation que si plus de threads sont utilisés. Comme ce test reproduit très bien le problème, il est bien adapté à l'objectif visé.
1 votes
C'est purement anecdotique, mais j'ai passé une grande partie de ma carrière à écrire des serveurs 24/7 fortement multithreadés dans le secteur de la finance, et la fragmentation de la mémoire n'a jamais été un problème.
1 votes
Il existe de nombreux programmes d'allocation de mémoire (Hoard, ptmalloc, tcmalloc, etc.) destinés à être utilisés dans les applications à fil conducteur, chacun présentant des avantages et des inconvénients en fonction de ce que vous faites. Je suis tombé sur une comparaison de certains d'entre eux l'autre jour à locklessinc.com/benchmarks.shtml qui pourraient vous intéresser.