web-dev-qa-db-fra.com

Quelle est l'efficacité de malloc et comment les implémentations diffèrent-elles?

Si j'utilise malloc, malloc utilise-t-il toujours le même algorithme indépendamment de ce qu'il alloue ou regarde-t-il les données et sélectionne-t-il un algorithme approprié?

Pouvons-nous rendre malloc plus rapide ou plus intelligent en choisissant un algorithme plus efficace? Dans mes tests, le système officiel intégré malloc d'Ubuntu est 10 fois plus lent qu'un projet scolaire si mes résultats de test sont corrects. Quelle est la prise? Je suis surpris que malloc soit si mauvais dans les tests car il devrait être optimisé. Utilise-t-il toujours le même algorithme? Existe-t-il une implémentation de référence de malloc? Si je veux regarder la source de malloc, que dois-je regarder? Les tests que je lance sont les suivants:

/* returns an array of arrays of char*, all of which NULL */
char ***alloc_matrix(unsigned rows, unsigned columns) {
    char ***matrix = malloc(rows * sizeof(char **));
    unsigned row = 0;
    unsigned column = 0;
    if (!matrix) abort();

    for (row = 0; row < rows; row++) {
        matrix[row] = calloc(columns, sizeof(char *));
        if (!matrix[row]) abort();
        for (column = 0; column < columns; column++) {
            matrix[row][column] = NULL;
        }
    }
    return matrix;
}

/* deallocates an array of arrays of char*, calling free() on each */
void free_matrix(char ***matrix, unsigned rows, unsigned columns) {
    unsigned row = 0;
    unsigned column = 0;
    for (row = 0; row < rows; row++) {
        for (column = 0; column < columns; column++) {
            /*    printf("column %d row %d\n", column, row);*/
            free(matrix[row][column]);
        }
        free(matrix[row]);
    }
    free(matrix);
}


int main(int agrc, char **argv) {

    int x = 10000;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

Le test est-il correct? J'utilise également ce test:

   for (i = 0; i < 1000000; i++) {
        void *p = malloc(1024 * 1024 * 1024);
        free(p);
   }
  • mise à jour

Selon le commentaire, je devrais faire des morceaux de taille variable et libres dans un ordre différent de celui de l'allocation, alors j'essaie:

int main(int agrc, char **argv) {
     int i;
    srand(time(NULL));
    int randomnumber;
    int size = 1024;
    void *p[size];
    for (i = 0; i < size; i++) {
        randomnumber = Rand() % 10;
        p[i] = malloc(1024 * 1024 * randomnumber);
    }

    for (i = size-1; i >= 0; i--) {
        free(p[i]);
    }

    int x = 1024;
    char *** matrix = alloc_matrix(x, x);
    free_matrix(matrix, x, x);
    return (0);
}

Ensuite, mon malloc personnalisé n'est plus plus rapide:

$ time ./gb_quickfit 

real    0m0.154s
user    0m0.008s
sys 0m0.144s
dac@dac-Latitude-E7450:~/ClionProjects/omalloc/openmalloc/overhead$ time ./a.out 

real    0m0.014s
user    0m0.008s
sys 0m0.004s

L'algorithme que j'ai utilisé était:

void *malloc_quick(size_t nbytes) {

    Header *moreroce(unsigned);
    int index, i;
    index = qindex(nbytes);

    /* 
     * Use another strategy for too large allocations. We want the allocation
     * to be quick, so use malloc_first().
     */

    if (index >= NRQUICKLISTS) {
        return malloc_first(nbytes);
    }

    /* Initialize the quick fit lists if this is the first run. */
    if (first_run) {
        for (i = 0; i < NRQUICKLISTS; ++i) {
            quick_fit_lists[i] = NULL;
        }
        first_run = false;
    }


    /*
     * If the quick fit list pointer is NULL, then there are no free memory
     * blocks present, so we will have to create some before continuing.
     */

    if (quick_fit_lists[index] == NULL) {
        Header* new_quick_fit_list = init_quick_fit_list(index);
        if (new_quick_fit_list == NULL) {
            return NULL;
        } else {
            quick_fit_lists[index] = new_quick_fit_list;
        }
    }

    /*
     * Now that we know there is at least one free quick fit memory block,
     * let's use return that and also update the quick fit list pointer so that
     * it points to the next in the list.
     */

    void* pointer_to_return = (void *)(quick_fit_lists[index] + 1);
    quick_fit_lists[index] = quick_fit_lists[index]->s.ptr;
   /* printf("Time taken %d seconds %d milliseconds", msec/1000, msec%1000);*/
    return pointer_to_return;
}
8
Niklas

Il existe plusieurs implémentations de malloc et elles peuvent utiliser des algorithmes très différents. Deux implémentations très largement utilisées sont jemalloc et dlmalloc . Dans les deux cas, les liens contiennent beaucoup d'informations sur le processus de réflexion et les compromis effectués dans un répartiteur à usage général.

Gardez à l'esprit qu'une implémentation malloc a très peu d'informations, juste la taille de l'allocation demandée. Une implémentation free n'a que le pointeur et toutes les données que 'malloc' peut y avoir secrètement attachées. En tant que tel, il finit par y avoir une bonne quantité d'heuristique, c'est-à-dire une conjecture inspirée pour décider comment allouer et désallouer.

Certains problèmes qu'un allocateur doit résoudre sont les suivants:

  1. alignement - certains alignements de mémoire sont plus rapides que d'autres
  2. fragmentation - l'allocation et la libération naïves peuvent laisser des trous qui provoquent des ballonnements
  3. performances - revenir au système d'exploitation pour plus de mémoire peut être coûteux
  4. Demandes courantes - dans la pratique, les applications (en particulier C++) font souvent de petites allocations, donc les rendre efficaces peut être très utile

Compte tenu de tout cela, ce que vous constaterez, c'est que les allocateurs sont généralement des logiciels complexes, de sorte qu'en général, ils fonctionnent bien. Cependant, dans des cas spécifiques, il peut être préférable de gérer la mémoire en dehors de l'allocateur si vous en savez beaucoup plus sur la structure des données. De toute évidence, l'inconvénient est que vous devez faire le travail vous-même.

11
Alex

Si vous vous souciez seulement de l'efficacité, voici une implémentation conforme et très efficace :

void* malloc(size_t sz) {
  errno = ENOMEM;
  return NULL;
}

void free(void*p) {
  if (p != NULL) abort();
}

Je suis sûr que vous ne trouverez pas de malloc plus rapide.

Mais tout en restant conforme à la norme, cette implémentation est inutile (elle n'alloue jamais avec succès de la mémoire de tas). C'est une implémentation de plaisanterie.

Cela illustre que dans le monde réel, les implémentations malloc & free doivent faire compromis. Et diverses implémentations font divers compromis. Vous trouverez de nombreux tutoriels sur implémentations malloc . Pour déboguer fuites de mémoire dans vos programmes C, vous voudrez utiliser valgrind .

Notez que sur les systèmes Linux au moins, (presque) tous bibliothèques standard C sont logiciels libres , donc vous pouvez étudier leur code source ( en particulier celui pour malloc & free). musl-libc a du code source très lisible.

BTW, le code dans votre question est incorrect (et plantera avec mon malloc ci-dessus). Chaque appel à malloc peut échoue, et vous devriez le tester.

Vous voudrez peut-être lire Programmation Linux avancée et des informations plus générales sur les systèmes d'exploitation, par exemple Systèmes d'exploitation: trois pièces faciles .

Vous devriez probablement lire quelque chose sur garbage collection , au moins pour obtenir les principaux concepts et terminologie; peut-être en lisant le manuel GC . Notez que comptage de références peut être considéré comme une forme de GC (une mauvaise, qui ne gère pas bien cycles de référence ou graphiques cycliques ...).

Vous pourriez vouloir utiliser dans votre programme C le ramasse-miettes conservateur de Boehm : vous utiliseriez alors GC_MALLOC (ou, pour les données sans pointeurs comme des chaînes ou des tableaux numériques, GC_MALLOC_ATOMIC) au lieu de malloc et vous n'aurez plus la peine d'appeler free. Il y a quelques mises en garde lors de l'utilisation du GC de Boehm. Vous pourriez envisager d'autres schémas ou bibliothèques GC ...

NB: Pour tester l'échec de malloc sur un système Linux (malloc appelait parfois les appels système mmap (2) et/ou sbrk (2) Linux pour augmenter la espace d'adressage virtuel , mais le plus souvent, il essaie difficilement de réutiliser précédemment freed mémoire), vous pouvez appeler setrlimit (2) de manière appropriée avec RLIMIT_AS et/ou RLIMIT_DATA, souvent via le ulimit bash intégré de votre terminal Shell. Utilisez strace (1) pour connaître les appels système effectués par votre (ou un autre) programme.

19

Tout d'abord, malloc et free fonctionnent ensemble, donc tester malloc en soi est trompeur. Deuxièmement, peu importe leur qualité, ils peuvent facilement représenter le coût dominant dans n'importe quelle application, et la meilleure solution consiste à les appeler moins. Les appeler moins est presque toujours le moyen gagnant de corriger les programmes qui sont malloc - limités. Une façon courante de procéder consiste à recycler les objets utilisés. Lorsque vous avez terminé avec un bloc, plutôt que gratuit - ing, vous l'enchaînez sur une pile de blocs utilisés et le réutilisez la prochaine fois que vous en avez besoin.

6
Mike Dunlavey

Le principal problème avec votre implémentation malloc_quick() est qu'elle n'est pas thread-safe. Et oui, si vous omettez la prise en charge des threads de votre allocateur, vous pouvez obtenir un gain de performances significatif. J'ai vu une accélération similaire avec mon propre allocateur non thread-safe.

Cependant, une implémentation standard doit être thread-safe. Il doit prendre en compte tous les éléments suivants:

  • Différents threads utilisent malloc() et free() simultanément. Cela signifie que l'implémentation ne peut pas accéder à l'état global sans synchronisation interne.

    Étant donné que les verrous sont très chers, les implémentations typiques de malloc() essaient d'éviter autant que possible l'état global en utilisant le stockage local pour presque toutes les demandes.

  • Un thread qui alloue un pointeur n'est pas nécessairement le thread qui le libère. Cela doit être pris en charge.

  • Un thread peut constamment allouer de la mémoire et la transmettre à un autre thread pour la libérer. Cela rend la gestion du dernier point beaucoup plus difficile, car cela signifie que les blocs libres peuvent s'accumuler dans n'importe quel stockage de thread local. Cela oblige l'implémentation malloc() à fournir des moyens aux threads d'échanger des blocs libres, et nécessite probablement la saisie de verrous de temps en temps.

Si vous ne vous souciez pas des threads, tous ces points ne posent aucun problème, donc un allocateur non thread-safe n'a pas à payer pour leur gestion avec les performances. Mais, comme je l'ai dit, une implémentation standard ne peut ignorer ces problèmes et doit donc payer pour leur gestion.

Je pense que les deux SUT ne sont pas des comparaisons directes. Je ne serais pas surpris d'une différence comparable si vous considérez toutes les variables: fabrication de mémoire, architecture de la carte mère, version du compilateur (qui a compilé malloc), à quoi ressemble l'application d'espace mémoire sur le SUT à l'époque, etc etc etc ... .... Essayez d'utiliser votre faisceau de test pour être plus représentatif de la façon dont vous utiliseriez la mémoire dans un projet réel - avec des allocations grandes/petites, et certaines applications maintenues pendant longtemps et certaines libérées peu de temps après avoir été prises.

2
Wayne Booth

Si vous comparez une implémentation réelle de malloc avec un projet d'école, considérez qu'un vrai malloc doit gérer les allocations, les réallocations et la libération de mémoire de tailles extrêmement différentes, fonctionnant correctement si les allocations se produisent simultanément sur différents threads, et la réallocation et la libération de mémoire se produisent sur différents threads . Vous voulez également être sûr que toute tentative de libérer de la mémoire qui n'a pas été allouée avec malloc sera interceptée. Et enfin, assurez-vous de ne pas comparer avec une implémentation de débogage malloc.

2
gnasher729