Je travaille sur une implémentation de tampon en anneau pour un seul producteur et un seul consommateur. J'ai deux exigences:
1) Alignez une seule instance allouée en tas d'un tampon en anneau sur une ligne de cache.
2) Alignez un champ dans un tampon en anneau 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.
};
Permettez-moi d'abord d'aborder le point 1, c'est-à-dire en alignant une seule instance allouée par tas de la classe. Il y a plusieurs façons:
1) Utilisez le spécificateur c ++ 11 alignas(..)
:
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 classe. Cela souffre de ne pas être indépendant de la plateforme:
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) Utilisez 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 la fonction normalisée C++ 11 aligned_alloc(..)
au lieu de posix_memalign(..)
mais GCC 4.8.1 sur Ubuntu 12.04 n'a pas pu trouver la définition dans stdlib.h
Est-ce que tous ces éléments garantissent la même chose? Mon objectif est l'alignement de la ligne de cache, de sorte que toute méthode ayant des limites d'alignement (disons double Word) ne le fera pas. L'indépendance de la plate-forme, qui indiquerait l'utilisation du standard alignas(..)
est un objectif secondaire.
Je ne sais pas si alignas(..)
et __attribute__((aligned(#)))
ont une limite qui pourrait être inférieure à la ligne de cache sur la machine. Je ne peux plus reproduire cela mais lors de l'impression des adresses, je pense que je n'ai pas toujours obtenu des adresses alignées sur 64 octets avec alignas(..)
. Au contraire, posix_memalign(..)
semblait toujours fonctionner. Encore une fois, je ne peux plus reproduire cela, alors peut-être que je faisais une erreur.
Le deuxième objectif est d'aligner un champ au sein d'une classe/structure sur une ligne de cache. Je fais cela pour empêcher le faux partage. J'ai essayé les moyens suivants:
1) Utilisez le spécificateur C++ 11 alignas(..)
:
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) Utilisez 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 aligner consumer_sequence
Sur une adresse 64 octets après le début de l'objet, donc si consumer_sequence
Est aligné en cache dépend de si l'objet lui-même est aligné en cache. Voici ma question: existe-t-il de meilleures façons de faire de même?
EDIT: La raison pour laquelle align_alloc ne fonctionnait pas sur ma machine était que j'étais sur eglibc 2.15 (Ubuntu 12.04). Il a fonctionné sur une version ultérieure d'eglibc.
Depuis la page de manuel : The function aligned_alloc() was added to glibc in version 2.16
.
Cela me rend tout à fait inutile car je ne peux pas exiger une version aussi récente d'eglibc/glibc.
Malheureusement, le meilleur que j'ai trouvé est d'allouer de l'espace supplémentaire puis d'utiliser la partie "alignée". Ainsi, le RingBuffer new
peut demander 64 octets supplémentaires, puis renvoyer la première partie alignée de 64 octets de cela. Il gaspille de l'espace mais donnera l'alignement dont vous avez besoin. Vous devrez probablement définir la mémoire avant ce qui est retourné à l'adresse d'allocation réelle pour la désallouer.
[Memory returned][ptr to start of memory][aligned memory][extra memory]
(en supposant qu'aucun 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 d'avoir un membre de données de RingBuffer
également 64 octets alignés, pour cela si vous savez que le début de this
est aligné, vous pouvez remplir pour forcer l'alignement pour les membres de données.
La réponse à votre problème est std :: align_storage . Il peut être utilisé au niveau supérieur et pour les membres individuels d'une classe.
Après quelques recherches supplémentaires, mes pensées sont les suivantes:
1) Comme @TemplateRex l'a souligné, il ne semble pas être un moyen standard d'aligner sur plus de 16 octets. Donc, même si nous utilisons le standard alignas(..)
, il n'y a aucune garantie à moins que la limite d'alignement soit 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é en tas comme je le soupçonnais, c'est-à-dire que new()
ne fait rien avec ces annotations. Ils semblent fonctionner pour les objets statiques ou les allocations de pile avec les mises en garde de (1).
Soit posix_memalign(..)
(non standard) soit aligned_alloc(..)
(standardisé mais n'a pas pu le faire fonctionner sur GCC 4.8.1) + placement new(..)
semble être la solution. Ma solution lorsque j'ai besoin de code indépendant de la plate-forme est des macros spécifiques au compilateur :)
3) L'alignement des champs struct/class semble fonctionner à la fois avec __attribute ((aligned(#)))
et alignas()
comme indiqué dans la réponse. Encore une fois, je pense que les mises en garde de (1) sur les garanties sur le support d'alignement.
Donc, ma solution actuelle consiste à utiliser posix_memalign(..)
+ placement new(..)
pour aligner une instance allouée en tas de ma classe puisque ma plate-forme cible est actuellement Linux uniquement. J'utilise également alignas(..)
pour aligner les champs car il est normalisé et fonctionne au moins sur Clang et GCC. Je serai heureux de le changer si une meilleure réponse arrive.
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 ainsi que cela se fait dans la passe de désinfection des threads 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 le ALIGNED ici est utilisé après la déclaration de variable. Mais c'est une autre histoire.