39 votes

Le multithreading accentue-t-il la fragmentation de la mémoire ?

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é.

22voto

sehe Points 123151

Ok, j'ai ramassé l'appât.

Il s'agit d'un système avec

Intel(R) Core(TM)2 Quad CPU    Q9550  @ 2.83GHz
4x5666.59 bogomips

Linux meerkat 2.6.35-28-generic-pae #50-Ubuntu SMP Fri Mar 18 20:43:15 UTC 2011 i686 GNU/Linux

gcc version 4.4.5

             total       used       free     shared    buffers     cached
Mem:       8127172    4220560    3906612          0     374328    2748796
-/+ buffers/cache:    1097436    7029736
Swap:            0          0          0

Course naïve

Je viens de l'exécuter

time ./ompmemtest 
Id 0 about to release all memory: 258.144 MB
Id 0 done, total alloc'ed -1572.7MB 
Id 3 about to release all memory: 257.854 MB
Id 3 done, total alloc'ed -1569.6MB 
Id 1 about to release all memory: 257.339 MB
Id 2 about to release all memory: 257.043 MB
Id 1 done, total alloc'ed -1570.42MB 
Id 2 done, total alloc'ed -1569.96MB 

real    0m13.429s
user    0m44.619s
sys 0m6.000s

Rien de spectaculaire. Voici la sortie simultanée de vmstat -S M 1

Données brutes Vmstat

procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
 0  0      0   3892    364   2669    0    0    24     0  701 1487  2  1 97  0
 4  0      0   3421    364   2669    0    0     0     0 1317 1953 53  7 40  0
 4  0      0   2858    364   2669    0    0     0     0 2715 5030 79 16  5  0
 4  0      0   2861    364   2669    0    0     0     0 6164 12637 76 15  9  0
 4  0      0   2853    364   2669    0    0     0     0 4845 8617 77 13 10  0
 4  0      0   2848    364   2669    0    0     0     0 3782 7084 79 13  8  0
 5  0      0   2842    364   2669    0    0     0     0 3723 6120 81 12  7  0
 4  0      0   2835    364   2669    0    0     0     0 3477 4943 84  9  7  0
 4  0      0   2834    364   2669    0    0     0     0 3273 4950 81 10  9  0
 5  0      0   2828    364   2669    0    0     0     0 3226 4812 84 11  6  0
 4  0      0   2823    364   2669    0    0     0     0 3250 4889 83 10  7  0
 4  0      0   2826    364   2669    0    0     0     0 3023 4353 85 10  6  0
 4  0      0   2817    364   2669    0    0     0     0 3176 4284 83 10  7  0
 4  0      0   2823    364   2669    0    0     0     0 3008 4063 84 10  6  0
 0  0      0   3893    364   2669    0    0     0     0 4023 4228 64 10 26  0

Cette information vous dit-elle quelque chose ?

Google Thread Caching Malloc

Maintenant, pour s'amuser vraiment, ajoutez un peu de piment

time LD_PRELOAD="/usr/lib/libtcmalloc.so" ./ompmemtest 
Id 1 about to release all memory: 257.339 MB
Id 1 done, total alloc'ed -1570.42MB 
Id 3 about to release all memory: 257.854 MB
Id 3 done, total alloc'ed -1569.6MB 
Id 2 about to release all memory: 257.043 MB
Id 2 done, total alloc'ed -1569.96MB 
Id 0 about to release all memory: 258.144 MB
Id 0 done, total alloc'ed -1572.7MB 

real    0m11.663s
user    0m44.255s
sys 0m1.028s

Ça a l'air plus rapide, non ?

procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
 4  0      0   3562    364   2684    0    0     0     0 1041 1676 28  7 64  0
 4  2      0   2806    364   2684    0    0     0   172 1641 1843 84 14  1  0
 4  0      0   2758    364   2685    0    0     0     0 1520 1009 98  2  1  0
 4  0      0   2747    364   2685    0    0     0     0 1504  859 98  2  0  0
 5  0      0   2745    364   2685    0    0     0     0 1575 1073 98  2  0  0
 5  0      0   2739    364   2685    0    0     0     0 1415  743 99  1  0  0
 4  0      0   2738    364   2685    0    0     0     0 1526  981 99  2  0  0
 4  0      0   2731    364   2685    0    0     0   684 1536  927 98  2  0  0
 4  0      0   2730    364   2685    0    0     0     0 1584 1010 99  1  0  0
 5  0      0   2730    364   2685    0    0     0     0 1461  917 99  2  0  0
 4  0      0   2729    364   2685    0    0     0     0 1561 1036 99  1  0  0
 4  0      0   2729    364   2685    0    0     0     0 1406  756 100  1  0  0
 0  0      0   3819    364   2685    0    0     0     4 1159 1476 26  3 71  0

Au cas où vous souhaiteriez comparer les résultats de vmstat

Valgrind --tool massif

Il s'agit de la tête de sortie de ms_print après valgrind --tool=massif ./ompmemtest (malloc par défaut) :

--------------------------------------------------------------------------------
Command:            ./ompmemtest
Massif arguments:   (none)
ms_print arguments: massif.out.beforetcmalloc
--------------------------------------------------------------------------------

    GB
1.009^                                                                     :  
     |       ##::::@@:::::::@@::::::@@::::@@::@::::@::::@:::::::::@::::::@::: 
     |       # :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::: 
     |       # :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::: 
     |      :# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::: 
     |      :# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::: 
     |      :# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |     ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |     ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |     ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |     ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |     ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |   ::::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |   : ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |   : ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |  :: ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     |  :: ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     | ::: ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     | ::: ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
     | ::: ::# :: :@ :::: ::@ : ::::@ :: :@ ::@::::@: ::@:::::: ::@::::::@::::
   0 +----------------------------------------------------------------------->Gi
     0                                                                   264.0

Number of snapshots: 63
 Detailed snapshots: [6 (peak), 10, 17, 23, 27, 30, 35, 39, 48, 56]

Google HEAPPROFILE

Malheureusement, la vanille valgrind ne fonctionne pas avec tcmalloc J'ai donc changé de cheval à mi-parcours au profilage du tas avec google-perftools

gcc openMpMemtest_Linux.cpp -fopenmp -lgomp -lstdc++ -ltcmalloc -o ompmemtest

time HEAPPROFILE=/tmp/heapprofile ./ompmemtest
Starting tracking the heap
Dumping heap profile to /tmp/heapprofile.0001.heap (100 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0002.heap (200 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0003.heap (300 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0004.heap (400 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0005.heap (501 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0006.heap (601 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0007.heap (701 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0008.heap (801 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0009.heap (902 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0010.heap (1002 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0011.heap (2029 MB allocated cumulatively, 1031 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0012.heap (3053 MB allocated cumulatively, 1030 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0013.heap (4078 MB allocated cumulatively, 1031 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0014.heap (5102 MB allocated cumulatively, 1031 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0015.heap (6126 MB allocated cumulatively, 1033 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0016.heap (7151 MB allocated cumulatively, 1029 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0017.heap (8175 MB allocated cumulatively, 1029 MB currently in use)
Dumping heap profile to /tmp/heapprofile.0018.heap (9199 MB allocated cumulatively, 1028 MB currently in use)
Id 0 about to release all memory: 258.144 MB
Id 0 done, total alloc'ed -1572.7MB 
Id 2 about to release all memory: 257.043 MB
Id 2 done, total alloc'ed -1569.96MB 
Id 3 about to release all memory: 257.854 MB
Id 3 done, total alloc'ed -1569.6MB 
Id 1 about to release all memory: 257.339 MB
Id 1 done, total alloc'ed -1570.42MB 
Dumping heap profile to /tmp/heapprofile.0019.heap (Exiting)

real    0m11.981s
user    0m44.455s
sys 0m1.124s

Contactez-moi pour obtenir des détails complets

Mise à jour

Aux commentaires : J'ai mis à jour le programme

--- omptest/openMpMemtest_Linux.cpp 2011-05-03 23:18:44.000000000 +0200
+++ q/openMpMemtest_Linux.cpp   2011-05-04 13:42:47.371726000 +0200
@@ -13,8 +13,8 @@
 void runParallelAllocTest()
 {
    // constants
-   const int  NUM_ALLOCATIONS = 5000; // alloc's per thread
-   const int  NUM_THREADS = 4;       // how many threads?
+   const int  NUM_ALLOCATIONS = 55000; // alloc's per thread
+   const int  NUM_THREADS = 8;        // 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)

Il a fonctionné pendant plus de 5m3s. Vers la fin, une capture d'écran de htop montre qu'en effet, l'ensemble réservé est légèrement plus élevé, allant vers 2.3g :

  1  [||||||||||||||||||||||||||||||||||||||||||||||||||96.7%]     Tasks: 125 total, 2 running
  2  [||||||||||||||||||||||||||||||||||||||||||||||||||96.7%]     Load average: 8.09 5.24 2.37 
  3  [||||||||||||||||||||||||||||||||||||||||||||||||||97.4%]     Uptime: 01:54:22
  4  [||||||||||||||||||||||||||||||||||||||||||||||||||96.1%]
  Mem[|||||||||||||||||||||||||||||||             3055/7936MB]
  Swp[                                                  0/0MB]

  PID USER     NLWP PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
 4330 sehe        8  20   0 2635M 2286M   908 R 368. 28.8 15:35.01 ./ompmemtest

Comparaison des résultats avec une exécution de tcmalloc : 4m12s, statistiques similaires présente des différences mineures ; la grande différence réside dans l'ensemble VIRT (mais cela n'est pas particulièrement utile à moins que vous n'ayez un espace d'adressage très limité par processus ). Le jeu RES est assez similaire, si vous voulez mon avis. La chose la plus importante à noter est un parallélisme accru ; tous les cœurs sont maintenant exploités au maximum. Ceci est évidemment dû à la réduction du besoin de verrouiller les opérations sur le tas lors de l'utilisation de tcmalloc :

Si la liste libre est vide : (1) Nous récupérons un ensemble d'objets à partir d'une liste libre centrale pour cette classe de taille (la liste libre centrale est partagée par tous les threads). (2) Nous les plaçons dans la liste libre locale du thread. (3) Nous renvoyons l'un des objets nouvellement récupérés aux applications.

  1  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]     Tasks: 172 total, 2 running
  2  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]     Load average: 7.39 2.92 1.11 
  3  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]     Uptime: 11:12:25
  4  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]
  Mem[||||||||||||||||||||||||||||||||||||||||||||              3278/7936MB]
  Swp[                                                                0/0MB]

  PID USER     NLWP PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
14391 sehe        8  20   0 2251M 2179M  1148 R 379. 27.5  8:08.92 ./ompmemtest

0 votes

Merci pour toutes vos suggestions d'outils ! Je vais effectuer vos tests moi-même et voir ce que j'obtiens. Peut-être que massif pourra me donner une sorte de rapport de fragmentation. D'après vos informations vmstat, il semble que vous n'ayez pas rencontré de problème de fragmentation car votre consommation de mémoire est restée la même. Pourriez-vous exécuter la simple vérification 'top' (voir nouveau Essais dans la question) afin que les résultats soient plus comparables à ceux que j'ai obtenus ? Si le problème n'apparaît pas, essayez d'augmenter le nombre de threads à 8 ou 16 - votre processeur est peut-être tout simplement trop rapide.

0 votes

Je viens d'essayer valgrind massif, et il semble qu'il ne soit pas adapté pour mesurer la fragmentation du tas ici, car il va forcer le programme à passer en mode parallèle. Cela réduit l'effet souligné à un strict minimum, en listant seulement 32 Mo de données de tas supplémentaires. Si la fragmentation avait été aussi élevée que celle mesurée, on aurait pu s'attendre à une valeur de 400 Mo sur ma machine.

0 votes

Avec 8 threads, la mémoire 'RES' ne dépasse jamais 2,1g ( 4025 sehe 20 0 2410m 2.1g 908 R 314 27.4 3:16.20 ompmemtest ). Il est évident qu'il n'est pas possible d'augmenter le nombre de threads à 16 sur le PAE.

1voto

Byron Points 371

Lors de la liaison du programme de test avec tcmalloc de google l'exécutable ne s'exécute pas seulement ~10% plus vite, mais présente également une fragmentation de la mémoire considérablement réduite ou insignifiante :

PID   USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
13441 byron     20   0  379m 334m 1220 R  187  8.4   0:02.63 ompmemtestgoogle                                                                        
13441 byron     20   0 1085m 1.0g 1220 R  194 26.2   0:08.52 ompmemtestgoogle                                                                        
13441 byron     20   0 1111m 1.0g 1220 R  195 26.9   0:14.42 ompmemtestgoogle                                                                        
13441 byron     20   0 1131m 1.1g 1220 R  195 27.4   0:20.30 ompmemtestgoogle                                                                        
13441 byron     20   0 1137m 1.1g 1220 R  195 27.6   0:26.19 ompmemtestgoogle                                                                        
13441 byron     20   0 1137m 1.1g 1220 R  195 27.6   0:32.05 ompmemtestgoogle                                                                        
13441 byron     20   0 1149m 1.1g 1220 R  191 27.9   0:37.81 ompmemtestgoogle                                                                        
13441 byron     20   0 1149m 1.1g 1220 R  194 27.9   0:43.66 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  188 28.2   0:49.32 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  194 28.2   0:55.15 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  191 28.2   1:00.90 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  191 28.2   1:06.64 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1356 R  192 28.2   1:12.42 ompmemtestgoogle

D'après les données dont je dispose, la réponse semble être la suivante :

L'accès multithread au tas peut accentuer la fragmentation si la bibliothèque de tas employée ne gère pas bien l'accès concurrent et si le processeur ne parvient pas à exécuter les threads de manière réellement concurrente. .

La bibliothèque tcmalloc ne montre pas de fragmentation significative de la mémoire en exécutant le même programme qui causait auparavant une perte de 400 Mo en raison de la fragmentation.

Mais pourquoi cela se produit-il ?

La meilleure idée que j'ai à proposer ici est une sorte d'artefact de verrouillage dans le tas.

Le programme de test allouera des blocs de mémoire de taille aléatoire, libérant les blocs alloués au début du programme pour rester dans sa limite de mémoire. Lorsqu'un thread est en train de libérer des blocs de mémoire, le programme de test alloue des blocs de mémoire de taille aléatoire. ancien qui se trouve dans un bloc de tas sur la "gauche", elle peut en fait être arrêtée car un autre thread est programmé pour s'exécuter, laissant un verrou (doux) sur ce bloc de tas. Le nouveau thread programmé veut allouer de la mémoire, mais ne peut même pas lire ce bloc de tas sur le côté "gauche" pour vérifier s'il y a de la mémoire libre, car il est en train d'être modifié. Il risque donc d'utiliser inutilement un nouveau bloc du côté "droit".

Ce processus pourrait ressembler à un déplacement de blocs du tas, où les premiers blocs (à gauche) restent peu utilisés et fragmentés, ce qui oblige à utiliser de nouveaux blocs à droite.

Je répète que ce problème de fragmentation ne se produit pour moi que si j'utilise 4 threads ou plus sur un système à double cœur qui ne peut gérer que deux threads plus ou moins simultanément. Lorsque deux threads seulement sont utilisés, les verrous (soft) sur le tas sont maintenus suffisamment courts pour ne pas bloquer l'autre thread qui souhaite allouer de la mémoire.

Par ailleurs, je n'ai pas vérifié le code réel de l'implémentation du tas de la glibc, et je ne suis rien d'autre qu'un novice dans le domaine des allocateurs de mémoire - tout ce que j'ai écrit est juste ce qu'il me semble, ce qui en fait une pure spéculation.

Une autre lecture intéressante pourrait être celle de tcmalloc documentation qui expose les problèmes courants liés aux tas et à l'accès multithread, dont certains peuvent également avoir joué un rôle dans le programme de test.

Il convient de noter qu'il ne restituera jamais de mémoire au système (voir le paragraphe "Caveats" dans la section tcmalloc documentation )

0 votes

some of which may have played their role in the test program too -- C'était le sujet du benchmark synthétique, si je ne me trompe pas :)

0 votes

Je ne sais pas exactement lesquels, d'où le fait que je ne suis pas sûr de pouvoir les utiliser. may dans le texte. N'hésitez pas à le reformuler :).

0 votes

Non, vous faites une déclaration erronée. Le gestionnaire de tas par défaut dispose d'un verrou global (voir dlmalloc ). Les accès concurrents sont donc simplement sérialisés. Ces données ne permettent pas de conclure que la fragmentation de la mémoire est liée au multithreading. Si vous voulez vraiment faire cette affirmation, vous devez comparer à un unique tout en exerçant la même pression sur le gestionnaire du tas.

0voto

Chris Pugmire Points 41

Oui, le malloc par défaut (selon la version de linux) fait des choses folles qui échouent massivement dans certaines applications multithématiques. En particulier, il conserve des tas (arènes) presque pour chaque thread afin d'éviter le verrouillage. C'est beaucoup plus rapide qu'un seul tas pour tous les threads, mais très inefficace au niveau de la mémoire (parfois). Vous pouvez régler cela en utilisant un code comme celui-ci qui désactive les arènes multiples (cela tue les performances, donc ne le faites pas si vous avez beaucoup de petites allocations !)

rv = mallopt(-7, 1);  // M_ARENA_TEST
rv = mallopt(-8, 1);  // M_ARENA_MAX

Ou, comme d'autres l'ont suggéré, en utilisant divers substituts à malloc.

En fait, il est impossible pour un malloc à usage général d'être toujours efficace puisqu'il ne sait pas comment il va être utilisé.

ChrisP.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X