Pourquoi malloc+memset est plus lent que calloc?

on sait que calloc est différent de malloc en ce qu'il initialise la mémoire allouée. Avec calloc , la mémoire est réglée à zéro. Avec malloc , la mémoire n'est pas effacée.

donc dans le travail quotidien, je considère calloc comme malloc + memset . Au fait, pour le plaisir, j'ai écrit le code suivant pour un benchmark.

le résultat est confus.

Code 1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

sortie du Code 1:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s  

Code 2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'"151920920"',BLOCK_SIZE);
                i++;
        }
}

sortie du Code 2:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s  

remplacer memset par bzero(buf[i],BLOCK_SIZE) dans le Code 2 produit le même résultat.

ma question Est: pourquoi malloc + memset est-il tellement plus lent que calloc ? Comment puis calloc ?

222
demandé sur Philip Conrad 2010-04-22 09:40:25

3 réponses

la version courte: toujours utiliser calloc() au lieu de malloc()+memset() . Dans la plupart des cas, ils seront les mêmes. Dans certains cas, calloc() fera moins de travail parce qu'il peut sauter memset() entièrement. Dans d'autres cas, calloc() peut même tricher et ne pas allouer de mémoire! Cependant, malloc()+memset() fera toujours la pleine quantité de travail.

comprendre cela nécessite une courte visite du système de mémoire.

visite rapide de la mémoire

il y a quatre parties principales ici: votre programme, la bibliothèque standard, le noyau, et les tables de page. Vous connaissez déjà votre programme, donc...

les allocateurs mémoire comme malloc() et calloc() sont surtout là pour prendre de petites allocations (n'importe quoi de 1 octet à 100s de KB) et les regrouper dans de plus grandes réserves de mémoire. Par exemple, si vous attribuez 16 octets, malloc() va d'abord essayer d'obtenir 16 octets de l'un de ses pools, puis demander pour plus de mémoire du noyau lorsque la piscine est vide. Cependant, puisque le programme que vous demandez affecte une grande quantité de mémoire à la fois, malloc() et calloc() vont juste demander cette mémoire directement à partir du noyau. Le seuil pour ce comportement dépend de votre système, mais j'ai vu 1 MiB utilisé comme seuil.

le noyau est responsable de l'allocation de la mémoire vive réelle à chaque processus et de s'assurer que les processus n'interfèrent pas avec mémoire d'autres processus. C'est ce qu'on appelle protection de la mémoire, il a été la saleté commune depuis les années 1990, et c'est la raison pour laquelle un programme peut se planter sans détruire le système entier. Ainsi, lorsqu'un programme a besoin de plus de mémoire, il ne peut pas simplement prendre la mémoire, mais il demande la mémoire du noyau en utilisant un appel système comme mmap() ou sbrk() . Le noyau donnera de la RAM à chaque processus en modifiant la table de la page.

Le tableau de page établit une correspondance entre les adresses mémoire et la mémoire vive réelle. Les adresses de votre processus, 0x00000000 à 0xffffff sur un système 32 bits, ne sont pas une vraie mémoire, mais sont plutôt des adresses dans la mémoire virtuelle . le processeur divise ces adresses en 4 pages KiB, et chaque page peut être assignée à un morceau différent de RAM physique en modifiant la table de page. Seul le noyau est autorisé à modifier la table de page.

Comment ça ne fonctionne pas

Voici comment allouer 256 MiB ne pas travail:

  1. votre processus appelle calloc() et demande 256 MiB.

  2. la bibliothèque standard appelle mmap() et demande 256 MiB.

  3. le noyau trouve 256 MiB de mémoire vive inutilisée et le donne à votre processus en modifiant la table de page.

  4. la bibliothèque standard zérote la mémoire vive avec memset() et retourne de calloc() .

  5. votre processus finit par disparaître, et le noyau récupère la mémoire vive pour qu'elle puisse être utilisée par un autre processus.

Comment ça marche

le processus ci-dessus fonctionnerait, mais il ne se produit pas de cette façon. Il y a trois grandes différences.

  • lorsque votre processus reçoit une nouvelle mémoire du noyau, cette mémoire a probablement été utilisée par un autre processus auparavant. C'est un risque pour la sécurité. Et si cette mémoire avait des mots de passe, des clés de cryptage, ou des recettes secrètes de salsa? Pour éviter les fuites de données sensibles, le noyau efface toujours la mémoire avant de la transmettre à un processus. Nous pourrions aussi bien frotter la mémoire en la mettant à zéro, et si la nouvelle mémoire est à zéro, nous pourrions aussi bien en faire une garantie, donc mmap() garantit que la nouvelle mémoire qu'il renvoie est toujours mise à zéro.

  • il y a beaucoup de programmes qui allouent de la mémoire mais n'utilisent pas la mémoire immédiatement. Parfois la mémoire est attribuée mais jamais utilisée. Le noyau le sait et est paresseux. Lorsque vous attribuez une nouvelle mémoire, le noyau ne touche pas du tout la table de page et ne donne aucune RAM à votre processus. Au lieu de cela, il trouve un certain espace d'adresse dans votre processus, fait une note de ce qui est supposé aller là-bas, et fait une promesse qu'il mettra RAM là si votre programme l'utilise réellement. Lorsque votre programme essaie de lire ou d'écrire à partir de ces adresses, le processeur déclenche un page fault et les étapes du noyau dans assign RAM à ces adresses et reprend votre programme. Si vous n'utilisez jamais la mémoire, la page fault n'arrive jamais et votre programme n'obtient jamais réellement la mémoire vive.

  • certains procédés allouer de la mémoire et puis lire sans le modifier. Cela signifie qu'un grand nombre de pages en mémoire à travers différents processus peuvent être remplis avec des zéros Vierges retournés de mmap() . Puisque ces pages sont toutes les mêmes, le noyau fait toutes ces adresses virtuelles pointer une seule page partagée de 4 KiB de mémoire remplie de zéros. Si vous essayez d'écrire dans cette mémoire, le processeur déclenche un autre défaut de page et le noyau intervient pour vous donner une nouvelle page de zéros qui n'est pas partagée. avec tous les autres programmes.

le processus final ressemble plus à ceci:

  1. votre processus appelle calloc() et demande 256 MiB.

  2. la bibliothèque standard appelle mmap() et demande 256 MiB.

  3. le noyau trouve 256 MiB d'espace d'adresse non utilisé , fait un note sur ce que cet espace d'adresse est maintenant utilisé pour, et retourne.

  4. la bibliothèque standard sait que le résultat de mmap() est toujours rempli de zéros (ou sera une fois qu'il obtient effectivement un peu de RAM), de sorte qu'il ne touche pas la mémoire, il n'y a donc pas de problème de page, et la RAM n'est jamais donnée à votre processus.

  5. votre processus finit par disparaître, et le noyau ne besoin de récupérer le RAM parce qu'il n'a jamais été attribué en premier lieu.

si vous utilisez memset() pour mettre la page à zéro, memset() déclenchera le défaut de la page, fera que la mémoire vive sera attribuée, puis la mettra à zéro même si elle est déjà remplie de zéros. C'est une énorme quantité de travail supplémentaire, et cela explique pourquoi calloc() est plus rapide que malloc() et memset() . Si on finit par utiliser la mémoire de toute façon, calloc() est toujours plus rapide que malloc() et memset() mais la différence n'est pas tout à fait aussi ridicule.


cela ne fonctionne pas toujours

tous les systèmes n'ont pas de mémoire virtuelle, donc tous les systèmes ne peuvent pas utiliser ces optimisations. Cela s'applique aux processeurs très anciens comme le 80286 ainsi qu'aux processeurs intégrés qui sont tout simplement trop petits pour une unité de gestion de mémoire sophistiquée.

cela aussi ne sera pas toujours travailler avec des allocations plus petites. Avec des allocations plus petites, calloc() obtient la mémoire d'un pool partagé au lieu d'aller directement dans le noyau. En général , la piscine partagée pourrait avoir des données de camelote stockées dans elle de l'ancienne mémoire qui a été utilisé et libéré avec free() , de sorte que calloc() pourrait prendre cette mémoire et appeler memset() pour le vider. Les implémentations communes traceront quelles parties du pool partagé sont vierges et toujours remplies de zéros, mais toutes les implémentations ne le font pas.

Dissiper certains des mauvaises réponses

en fonction du système d'exploitation, le noyau peut ou non avoir zéro mémoire dans son temps libre, au cas où vous auriez besoin d'avoir un peu de mémoire mise à zéro plus tard. Linux ne supprime pas la mémoire à l'avance, et Dragonfly BSD a récemment supprimé cette fonctionnalité de leur noyau . Quelques autres noyaux font zéro mémoire à l'avance, cependant. La réduction à zéro des pages durign idle n'est pas suffisante pour expliquer la grande des différences de performances, de toute façon.

la fonction calloc() n'utilise pas une version spéciale alignée sur la mémoire de memset() , et cela ne la rendrait pas beaucoup plus rapide de toute façon. La plupart des implémentations memset() pour les processeurs modernes ressemblent à ceci:

function memset(dest, c, len)
    // one byte at a time, until the dest is aligned...
    while (len > 0 && ((unsigned int)dest & 15))
        *dest++ = c
        len -= 1
    // now write big chunks at a time (processor-specific)...
    // block size might not be 16, it's just pseudocode
    while (len >= 16)
        // some optimized vector code goes here
        // glibc uses SSE2 when available
        dest += 16
        len -= 16
    // the end is not aligned, so one byte at a time
    while (len > 0)
        *dest++ = c
        len -= 1

donc vous pouvez voir, memset() est très rapide et vous n'allez pas vraiment obtenir quelque chose de mieux pour les grands blocs de mémoire.

le fait que memset() est une mémoire de mise à zéro qui est déjà mise à zéro signifie que la mémoire est mise à zéro deux fois, mais cela n'explique qu'une différence de performance de 2x. La différence de performance ici est beaucoup plus grande (j'ai mesuré plus de trois ordres de grandeur sur mon système entre malloc()+memset() et calloc() ).

tour

au lieu de boucler 10 fois, écrivez un programme qui alloue de la mémoire jusqu'à ce que malloc() ou calloc() retourne NULL.

que se passe-t-il si vous ajoutez memset() ?

390
répondu Dietrich Epp 2016-08-05 07:30:26

parce que sur de nombreux systèmes, dans le temps de traitement libre, L'OS va autour de mettre la mémoire libre à zéro sur son propre et le marquage de sécurité pour calloc() , donc quand vous appelez calloc() , il peut déjà avoir la mémoire libre, zéro pour vous donner.

10
répondu Chris Lutz 2010-04-22 05:48:20

sur certaines plateformes dans certains modes, malloc initialise la mémoire à une valeur typiquement non nulle avant de la retourner, de sorte que la seconde version pourrait bien initialiser la mémoire deux fois

0
répondu Stewart 2010-04-22 05:51:20