67 votes

Quelle est la méthode recommandée pour aligner la mémoire en C++11 ?

Je travaille sur la mise en œuvre d'un tampon en anneau à producteur unique et à consommateur unique, et j'ai deux exigences :

  1. Aligner une instance unique allouée au tas d'un tampon circulaire sur une ligne de cache.
  2. Aligner un champ dans un tampon circulaire sur une ligne de cache (pour éviter un faux partage).

Ma classe ressemble à quelque chose comme :

#define CACHE_LINE_SIZE 64  // To be used later.

template<typename T, uint64_t num_events>
class RingBuffer {  // This needs to be aligned to a cache line.
public:
  ....

private:
  std::atomic<int64_t> publisher_sequence_ ;
  int64_t cached_consumer_sequence_;
  T* events_;
  std::atomic<int64_t> consumer_sequence_;  // This needs to be aligned to a cache line.

};

J'aborderai tout d'abord le point 1, à savoir alignement d'une instance unique allouée au tas de la classe. Il y a plusieurs façons de procéder :

  1. Utilisez le c++ 11 alignas(..) spécificateur :

    template<typename T, uint64_t num_events>
    class alignas(CACHE_LINE_SIZE) RingBuffer {
    public:
      ....
    
    private:
      // All the private fields.
    
    };
  2. Utilisez posix_memalign(..) + placement new(..) sans modifier la définition de la classe. Le problème est que cette méthode n'est pas indépendante de la plate-forme :

    void* buffer;
    if (posix_memalign(&buffer, 64, sizeof(processor::RingBuffer<int, kRingBufferSize>)) != 0) {
        perror("posix_memalign did not work!");
        abort();
    }
    // Use placement new on a cache aligned buffer.
    auto ring_buffer = new(buffer) processor::RingBuffer<int, kRingBufferSize>();
  3. Utiliser l'extension GCC/Clang __attribute__ ((aligned(#)))

    template<typename T, uint64_t num_events>
    class RingBuffer {
    public:
      ....
    
    private:
      // All the private fields.
    
    } __attribute__ ((aligned(CACHE_LINE_SIZE)));
  4. J'ai essayé d'utiliser le C++ 11 normalisé. aligned_alloc(..) au lieu de la fonction posix_memalign(..) mais GCC 4.8.1 sur Ubuntu 12.04 n'a pas pu trouver la définition dans stdlib.h

Sont-ils tous garantis de faire la même chose ? Mon objectif est l'alignement de la ligne de cache, donc toute méthode qui a des limites sur l'alignement (disons double mot) ne fera pas l'affaire. L'indépendance de la plateforme, qui indique l'utilisation de la norme alignas(..) est un objectif secondaire.

Je ne sais pas si alignas(..) et __attribute__((aligned(#))) ont une certaine limite qui pourrait être inférieure à la ligne de cache de la machine. Je ne peux plus le reproduire, mais en imprimant des adresses, je pense que je n'ai pas toujours obtenu des adresses alignées sur 64 octets avec la fonction alignas(..) . Au contraire posix_memalign(..) semble toujours fonctionner. Une fois encore, je ne peux plus reproduire ce phénomène, alors peut-être ai-je fait une erreur.

Le deuxième objectif est de aligner un champ dans une classe/structure à une ligne de cache. Je fais cela pour éviter les faux partages. J'ai essayé les méthodes suivantes :

  1. Utilisez le C++ 11 alignas(..) spécificateur :

    template<typename T, uint64_t num_events>
    class RingBuffer {  // This needs to be aligned to a cache line.
      public:
      ...
      private:
        std::atomic<int64_t> publisher_sequence_ ;
        int64_t cached_consumer_sequence_;
        T* events_;
        std::atomic<int64_t> consumer_sequence_ alignas(CACHE_LINE_SIZE);
    };
  2. Utiliser l'extension GCC/Clang __attribute__ ((aligned(#)))

    template<typename T, uint64_t num_events>
    class RingBuffer {  // This needs to be aligned to a cache line.
      public:
      ...
      private:
        std::atomic<int64_t> publisher_sequence_ ;
        int64_t cached_consumer_sequence_;
        T* events_;
        std::atomic<int64_t> consumer_sequence_ __attribute__ ((aligned (CACHE_LINE_SIZE)));
    };

Ces deux méthodes semblent s'aligner consumer_sequence à une adresse située 64 octets après le début de l'objet, afin de savoir si consumer_sequence est aligné sur le cache dépend du fait que l'objet lui-même est aligné sur le cache. Ma question est la suivante : existe-t-il de meilleures façons de procéder ?

EDIT :

La raison aligned_alloc ne fonctionnait pas sur ma machine était que j'étais sur eglibc 2.15 (Ubuntu 12.04). Il a fonctionné sur une version plus récente d'eglibc.

De la page de manuel : _La fonction aligned_alloc() a été ajouté à la glibc dans la version 2.16_ .

Cela le rend plutôt inutile pour moi puisque je ne peux pas avoir besoin d'une version aussi récente d'eglibc/glibc.

5 votes

Excellente question, voir l'article de Michael Spencer Conférence BoostCon 2013 . Je ne pense pas qu'il soit possible de s'aligner de manière portable sur plus de 16 octets (donc une ligne de cache de 64 octets et un alignement encore plus grand sur les pages de mémoire virtuelle ne sont pas supportés par la norme).

0 votes

@TemplateRex Merci pour le lien. L'exposé semble pertinent + 1.

31voto

Glenn Teitelbaum Points 3564

Malheureusement, la meilleure solution que j'ai trouvée consiste à allouer de l'espace supplémentaire, puis à utiliser la partie "alignée". Ainsi, le RingBuffer new peut demander 64 octets supplémentaires et retourner la première partie alignée de 64 octets. Cela gaspille de l'espace mais donne l'alignement dont vous avez besoin. Vous devrez probablement définir la mémoire avant ce qui est renvoyé à l'adresse d'allocation réelle pour la désallouer.

[Memory returned][ptr to start of memory][aligned memory][extra memory]

(en supposant qu'il n'y a pas d'héritage de RingBuffer) quelque chose comme :

void * RingBuffer::operator new(size_t request)
{
     static const size_t ptr_alloc = sizeof(void *);
     static const size_t align_size = 64;
     static const size_t request_size = sizeof(RingBuffer)+align_size;
     static const size_t needed = ptr_alloc+request_size;

     void * alloc = ::operator new(needed);
     void *ptr = std::align(align_size, sizeof(RingBuffer),
                          alloc+ptr_alloc, request_size);

     ((void **)ptr)[-1] = alloc; // save for delete calls to use
     return ptr;  
}

void RingBuffer::operator delete(void * ptr)
{
    if (ptr) // 0 is valid, but a noop, so prevent passing negative memory
    {
           void * alloc = ((void **)ptr)[-1];
           ::operator delete (alloc);
    }
}

Pour la deuxième exigence, qui consiste à avoir un membre de données de la catégorie RingBuffer sont également alignés sur 64 octets, ce qui signifie que si vous savez que le début de l'élément this est aligné, vous pouvez tamponner pour forcer l'alignement des membres de données.

0 votes

Cela semble être une façon plus standard de le faire, avec la réserve que toute demande d'alignement de plus de 16 octets n'est pas requise par la norme. Je l'accepte car elle semble plus portable que ma solution posix_memalign(..).

1 votes

Votre épargne de alloc à utiliser avec delete devrait utiliser void* Non ?

1 votes

"((void **)ptr)[-1] = alloc ;" - cela ne dépend-il pas du compilateur ?

9voto

rubenvb Points 27271

La réponse à votre problème est std::stockage aligné . Il peut être utilisé au niveau supérieur et pour les membres individuels d'une classe.

3 votes

Mais il a les mêmes limitations que les alignas (jusqu'à c++17 jusqu'à 16 octets/limite dépendant de la plateforme).

4voto

Rajiv Points 695

Après quelques recherches supplémentaires, je pense que.. :

  1. Comme @TemplateRex l'a souligné, il ne semble pas y avoir de moyen standard d'aligner plus de 16 octets. Ainsi, même si nous utilisons la méthode normalisée alignas(..) il n'y a aucune garantie, sauf si la limite d'alignement est inférieure ou égale à 16 octets. Je devrai vérifier que cela fonctionne comme prévu sur une plateforme cible.

  2. __attribute ((aligned(#))) ou alignas(..) ne peut pas être utilisé pour aligner un objet alloué au tas comme je le soupçonnais, c'est-à-dire que new() ne fait rien avec ces annotations. Elles semblent fonctionner pour les objets statiques ou les allocations de pile avec les réserves de (1).

    Soit posix_memalign(..) (non standard) ou aligned_alloc(..) (standardisé mais n'a pas pu fonctionner avec GCC 4.8.1) + placement new(..) semble être la solution. Ma solution lorsque j'ai besoin d'un code indépendant de la plate-forme est de recourir à des macros spécifiques au compilateur :)

  3. L'alignement des champs des structures/classes semble fonctionner avec les deux types d'alignements. __attribute ((aligned(#))) et alignas() comme indiqué dans la réponse. Encore une fois, je pense que les mises en garde du point (1) concernant les garanties sur l'alignement sont valables.

Ma solution actuelle consiste donc à utiliser posix_memalign(..) + placement new(..) pour aligner une instance de ma classe allouée au tas, car ma plateforme cible n'est pour l'instant que Linux. J'utilise également alignas(..) pour l'alignement des champs car il est standardisé et fonctionne au moins sur Clang et GCC. Je serai heureux de le changer si une meilleure réponse se présente.

0 votes

En pratique alignas(64) ou même plus, fonctionne.

2voto

Hugo Points 88

Je ne sais pas si c'est la meilleure façon d'aligner la mémoire allouée avec un nouvel opérateur, mais c'est certainement très simple !

C'est la façon dont cela est fait dans la passe de l'assainisseur de fil dans GCC 6.1.0.

#define ALIGNED(x) __attribute__((aligned(x)))

static char myarray[sizeof(myClass)] ALIGNED(64) ;
var = new(myarray) myClass;

Eh bien, dans sanitizer_common/sanitizer_internal_defs.h, il est également écrit

// Please only use the ALIGNED macro before the type.
// Using ALIGNED after the variable declaration is not portable!        

Je ne sais donc pas pourquoi l'ALIGNED est utilisé ici après la déclaration de la variable. Mais c'est une autre histoire.

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