Forum Programmation.c++ operator new + boost::fast_pool_allocator

Posté par  . Licence CC By‑SA.
Étiquettes :
2
20
avr.
2013

Bonjour,

Dans le cadre d'un de mes projets qui alloue/désalloue beaucoup d'objets de tailles diverses mais relativement petits (< 100o par objets), j'observe une consommation mémoire très supérieur à ce que ça devrait être.
Comme je soupçonne que la mémoire ressemble à du gruyère (plus il y a de gruyère, plus il y a de trous et plus il y a de trous, moins il y a de gruyère), j'ai voulu tester le pool allocator de boost en surchargeant l'operator new.

Mais avec le cas simplifié ci-dessous, alors que le programme devrait prendre 256Mo de mémoire + éventuellement un peu d'overhead, il en consomme 2x plus soit 512Mo.
Ce facteur de 2x ne change pas selon la taille de l'objet, ni selon le nombre d'allocations.

#include <boost/pool/pool_alloc.hpp>
#include <cstdio>

class Foo
{
private:
  static boost::fast_pool_allocator<Foo> pool;
  char data[256];
public:

  static void * operator new(size_t size) { return pool.allocate();}
  static void operator delete(void * ptr) { pool.deallocate(reinterpret_cast<Foo*>(ptr)); }
};

#define NB_ALLOC 1024*1024

int main()
{
  for(int i = 0; i < NB_ALLOC; ++i) {    
    new Foo();
  }
  getchar();
  return 0;
}

Est-ce que je m'y prend mal ?
Est-ce normal ?

si c'est normal avec cet allocateur, en connaissez vous d'autres que je pourrais tester qui permettent de palier à mon problème ?

Merci d'avance

errno

  • # FAST pool allocator

    Posté par  . Évalué à 4.

    Hello,

    Effectivement, cela a l'air de venir de fast_pool_allocator. J'ai essayé avec un simple « new Foo[NB_ALLOC] » et la consommation redevient celle attendue. Je n'ai pas esssayé avec « pool_allocator » tout court car l'interface n'est pas tout-à-fait la même et que ça me faisait suer d'adapter tout ton exemple. :-) Ça ne m'étonne qu'à moitié car privilégier la vitesse se fait traditionnellement au dépit de la mémoire. Les liens entre les deux sont évidemment complexes mais on arrive généralement à une relation inversement proportionnelles, ou à peu près.

    En affichant dans la boucle les valeurs des pointeurs renvoyés par new, on s'aperçoit qu'ils sont consécutifs et croissants puis qu'ils sautent brusquement, à intervalles réguliers, d'une plage vers une autre. Il est donc raisonnable de penser que le pool alloue d'emblée une plage suffisamment grande mais de taille arbitraire au départ et que, comme tu ne fais que la remplir sans libérer tes objets (dans ton exemple), à chaque fois qu'il se retrouve au dépourvu, il décide d'allouer cette fois une plage deux fois plus grande que la précédente.

    • [^] # Re: FAST pool allocator

      Posté par  . Évalué à 5. Dernière modification le 20 avril 2013 à 15:16.

      Ça semble d'ailleurs se confirmer si tu alloues un tout petit peu moins d'objet que prévu : si tu remplaces

      NB_ALLOC 1024*1024
      
      

      par

      NB_ALLOC 1024*1024 - 1024
      
      

      Tu divises par deux ta consommation mémoire qui redevient proche de celle que tu attends. Comme 1024×1024 est une puissance de deux et que la taille des pages allouées par fast_pool_allocator<> doit l'être aussi, tu dois probablement remplir tout juste une page vers la fin de ta boucle, et l'alloueur prépare la suite en allouant autant d'espace qu'il en a utilisé jusque là. Cette dernière page restant alors majoritairement vide.

      • [^] # Re: FAST pool allocator

        Posté par  . Évalué à 2.

        Oui, du coup l'overhead de cet allocateur est quand même dans le pire des cas de x2, et en moyenne de x1.5 ce qui n'est pas rien.
        Je vais changer d'allocateur :)

        En tout cas merci pour tes réponses

    • [^] # Re: FAST pool allocator

      Posté par  . Évalué à 3.

      Merci pour ta réponse.

      En fait d'après la doc de boost, la différence entre fast_pool_allocator et pool_allocator est que le fast est sensé être à privilégier pour les allocations d'objets uniques (new Foo()) et le pool_allocator pour les allocations de plusieurs objets contiguës (new Foo[]). Ils expriment cela avec pool_allocator pour des std::vector et fast_pool_allocator pour des std::list.

      C'est une bonne idée de regarder les différences entre les adresses des pointeurs retournés.
      Je viens de tester et en effet, la taille de bloc par défaut initial étant 32.
      J'observe que le bloc créé suivant faire 64, puis le suivant 128, puis 256 …

      Ça semble donc être l'algo du fast_pool_allocator qui réagit comme ça. Je vais fouiller un peu dans les paramêtres du template pour voir si on ne peut pas modifier ce comportement.
      Sinon il ne me reste plus qu'a tester d'autres allocateurs, ou à en coder un qui réagit comme je le souhaite, c'est à dire créer un nouveau bloc quand c'est nécessaire mais toujours de la même taille.

  • # T'y va un peu fort.

    Posté par  . Évalué à 3.

    Les pools, normalement, sont utiles lorsque tu alloue et désalloue souvent, mais que le nombre d'instance reste borné à une faible valeur. Les pools vont allouer plus de place que nécessaire, c'est normal. Ils sont la pour privilégier les performances au détriment de la consommation mémoire.

    Si tu veux à la fois des performances et une faible consommation mémoire, il n'y a rien de magique, l'allocateur par défaut fait déjà un bon compromis.

    Je ne sais pas ce que fait ton programme, mais à mon avis, il faudrai profiler avant d'accuser l'allocateur mémoire. Ça, ou optimiser la consommation mémoire de ton programme (avec du copy-on-write par exemple).

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.