5
votes

Pourquoi memset est-il appelé après calloc?

J'ai recherché le code d'une bibliothèque et j'ai remarqué que les appels à calloc sont suivis de memset pour le bloc alloué par calloc . J'ai trouvé cette question avec une réponse assez complète sur les différences entre calloc et malloc + memset et en appelant memset pour juste avant le stockage alloué:

Pourquoi malloc + memset est plus lent que calloc? a >

Ce que je ne comprends toujours pas, c'est pourquoi on voudrait le faire. Quels sont les avantages de ces opérations?

L'exemple de code de la bibliothèque mentionnée ci-dessus:

if (p != NULL && && __builtin_expect(perturb_byte, 0))
   alloc_perturb (p, bytes);
return p;

Le code de la structure allouée (chaque tableau contient 32 éléments):

  /* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
     mmap.  */
  if (__glibc_unlikely (av == NULL))
    {
      void *p = sYSMALLOc (nb, av);
      if (p != NULL)
       alloc_perturb (p, bytes);
      return p;
    }

En plus de la réponse acceptée, j'aimerais fournir des informations vers lesquelles mon collègue m'a indiqué. Il y avait un bogue dans la glibc qui, parfois, empêchait calloc de remettre à zéro la mémoire. Voici le lien: https://bugzilla.redhat.com/show_bug.cgi?id=1293976

Texte réel du rapport de bogue au cas où le lien serait déplacé:

glibc: calloc () renvoie une mémoire non nulle

Description du problème:

Chez Facebook, nous avions une application qui commençait à se bloquer et à planter bizarrement en passant de glibc-2.12-1.149.el6.x86_64 à glibc-2.12-1.163.el6.x86_64. Il s'avère que ce patch

glibc-rh1066724.patch

A introduit le problème.

Vous avez ajouté le bit suivant à _int_malloc ()

typedef struct _light_pcapng_file_info {
    uint16_t major_version;
    uint16_t minor_version;
    char *file_comment;
    size_t file_comment_size;
    char *hardware_desc;
    size_t hardware_desc_size;
    char *os_desc;
    size_t os_desc_size;
    char *user_app_desc;
    size_t user_app_desc_size;
    size_t interface_block_count;
    uint16_t link_types[MAX_SUPPORTED_INTERFACE_BLOCKS];
    double timestamp_resolution[MAX_SUPPORTED_INTERFACE_BLOCKS];

} light_pcapng_file_info;

Mais ce n'est pas correct, alloc_perturb memset inconditionnellement l'octet avant à 0xf, contrairement à l'amont où il vérifie si perturb_byte est défini. Cela doit être changé en

light_pcapng_file_info *light_create_default_file_info()
{
    light_pcapng_file_info *default_file_info = calloc(1, sizeof(light_pcapng_file_info));
    memset(default_file_info, 0, sizeof(light_pcapng_file_info));
    default_file_info->major_version = 1;
    return default_file_info;
}

Le correctif que j'ai joint résout le problème pour moi.

Ce problème est exacerbé par le fait que toute sorte de la contention de verrouillage sur l'arène nous fait retomber sur mmap () 'ing un nouveau morceau. C'est parce que nous vérifions si l'arène incontrôlée que nous vérifions est corrompue, et si c'est le cas, nous bouclons, et si nous bouclons jusqu'au début, nous savons que nous n'avons rien trouvé. Sauf si notre arène initiale n'est pas réellement corrompue, nous retournerons toujours NULL, donc nous retombons plus souvent sur cette chose mmap (), ce qui rend vraiment les choses instables.

Veuillez corriger cela dès que possible possible, j'irais même jusqu'à appeler cela un problème de sécurité possible.


4 commentaires

calloc déjà 0-initialise la mémoire, donc utiliser memset pour le refaire est inutile (sauf si vous avez affaire à une implémentation boguée de calloc ).


Peut-être que l'auteur de la bibliothèque n'a pas utilisé calloc au début, mais malloc ? Peut-être que l'auteur de la bibliothèque ne sait pas vraiment ce que calloc est censé faire? Peut-être y a-t-il une myriade d'autres raisons? Le seul qui peut répondre est l'auteur original. Tout ce que nous pouvons faire, c'est deviner .


Le code a été écrit par une recrue. Et c'est tout ... rien à méditer.


Lire un autre code, en tirer des leçons, est important, mais cela présente cet inconvénient horrible: il y a beaucoup de code incroyablement mal écrit, et c'est un petit exemple. Il est absolument inutile d'appeler memset après calloc.


3 Réponses :


1
votes

J'appellerais cela un bogue, car il fait un travail inutile, calloc () est spécifié pour renvoyer la mémoire déjà effacée, alors pourquoi l'effacer à nouveau? Peut-être un refactoring a échoué, quand quelqu'un est passé de malloc () .

Si le code est open source et / ou dans un référentiel auquel vous avez accès, je vérifierais l'historique des commit pour ces lignes et verrais ce qui se passe. Avec un peu de chance, il y a un message de validation qui raconte la motivation ...


1 commentaires

Ce n'est pas complètement inutile - appeler memset () garantit que les pages de mémoire virtuelle sont réellement créées. Bien que si c'est la raison du code, malloc () alors memset () serait un meilleur choix.



7
votes

L'appel de memset () garantit que le système d'exploitation effectue réellement le mappage de la mémoire virtuelle. Comme indiqué dans les réponses à la question que vous avez liée, calloc () peut être optimisé afin que le mappage de mémoire réel soit différé.

L'application peut avoir des raisons de ne pas différer la création réelle du mappage de mémoire virtuelle - par exemple en utilisant le tampon pour lire à partir d'un périphérique très rapide, bien que dans le cas de l'utilisation de memset () pour remettre à zéro la mémoire, en utilisant calloc () au lieu de malloc () semble redondant.


7 commentaires

Non, cela n'explique pas pourquoi. Vous pouvez simplement toucher une seule cellule de mémoire pour y parvenir - vous n'avez pas à remettre à zéro toute la mémoire allouée juste pour forcer l'allocation. Ce serait inutilement inefficace.


@Lundin Ne devrait-il pas y avoir une cellule mémoire par page qui doit être touchée? Dans l'exemple du post du PO, il s'agit probablement d'une seule page (ou même d'une partie d'une page), mais si la mémoire s'étend sur plusieurs pages et que la taille de la page est inconnue, un appel à memset pourrait être plus simple.


@Lundin vous n'avez pas à remettre à zéro toute la mémoire allouée juste pour forcer l'allocation. ce serait inutilement inefficace. Et vous pouvez également incliner votre décalage pour obtenir de meilleures performances en raison de l'escalade du cache. Et si c'est une énorme quantité de mémoire et que vous voulez l'obtenir plus rapidement, vous pouvez toucher chaque page avec de nombreux fils. Mais memset () fait tout cela en une seule ligne de code. (Ouais, j'ai dû faire tout ça ...)


@Someprogrammerdude Cela voudrait dire qu'il s'agissait d'un port spécifique au système. Mais il est vrai que nous ne savons pas quelle est la taille de la structure.


Quoi qu'il en soit, si le but était de forcer l'allocation, je me serais certainement attendu à un commentaire expliquant que c'était le but.


Notez que gcc 8.2.1 optimise volontiers memset () à zéro après calloc () , donc cette tentative de pré-faute des adresses sera rompre avec un compilateur sensé de toute façon .


@Lundin mais "Le code a été écrit par une recrue." ;-)



2
votes

Personne n'est parfait, c'est tout. Oui, memset à zéro suite à un calloc est extravagant. Si vous voulez un memset explicite afin de garantir que vous êtes en possession de la mémoire que vous avez demandée, alors il devrait suivre un malloc à la place.

Le nouveau code contient environ 20 bogues pour 1 000 lignes et les lois de probabilité impliquent que tous ne sont pas éliminés. De plus, ce n'est pas vraiment un bogue, car il n'y a pas de mauvais comportement à observer.


0 commentaires