web-dev-qa-db-fra.com

Comment et quand aligner sur la taille de la ligne de cache?

Dans l'excellente file d'attente mpmc bornée de Dmitry Vyukov écrite en C++ Voir: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

Il ajoute quelques variables de remplissage. Je suppose que c'est pour l'aligner sur une ligne de cache pour les performances.

J'ai quelques questions.

  1. Pourquoi est-ce fait de cette façon?
  2. Est-ce une méthode portable qui fonctionnera toujours
  3. Dans quels cas serait-il préférable d'utiliser à la place __attribute__ ((aligned (64))).
  4. pourquoi le remplissage avant un pointeur de tampon aiderait-il aux performances? le pointeur n'est-il pas seulement chargé dans le cache, donc ce n'est vraiment que la taille d'un pointeur?

    static size_t const     cacheline_size = 64;
    typedef char            cacheline_pad_t [cacheline_size];
    
    cacheline_pad_t         pad0_;
    cell_t* const           buffer_;
    size_t const            buffer_mask_;
    cacheline_pad_t         pad1_;
    std::atomic<size_t>     enqueue_pos_;
    cacheline_pad_t         pad2_;
    std::atomic<size_t>     dequeue_pos_;
    cacheline_pad_t         pad3_;
    

Ce concept fonctionnerait-il sous gcc pour le code c?

57
Matt

C'est fait de cette façon pour que les différents cœurs modifiant différents champs n'aient pas à faire rebondir la ligne de cache contenant les deux entre leurs caches. En général, pour qu'un processeur accède à certaines données en mémoire, la ligne de cache entière qui les contient doit se trouver dans le cache local de ce processeur. S'il modifie ces données, cette entrée de cache doit généralement être la seule copie dans n'importe quel cache du système (mode exclusif dans le style MESI/MOESI protocoles de cohérence de cache). Lorsque des cœurs séparés tentent de modifier différentes données qui se trouvent sur la même ligne de cache et perdent ainsi du temps à déplacer cette ligne entière d'avant en arrière, cela s'appelle faux partage.

Dans l'exemple particulier que vous donnez, un noyau peut être la mise en file d'attente d'une entrée (lecture (partagée) buffer_ et écriture (exclusive) uniquement enqueue_pos_) tandis qu'une autre file d'attente (partagée buffer_ et exclusif dequeue_pos_) sans que l'un des cœurs ne bloque sur une ligne de cache appartenant à l'autre.

Le rembourrage au début signifie que buffer_ et buffer_mask_ finissent sur la même ligne de cache, plutôt que de se répartir sur deux lignes et nécessitent donc le double du trafic mémoire pour y accéder.

Je ne sais pas si la technique est entièrement portable. L'hypothèse est que chaque cacheline_pad_t sera lui-même aligné sur une limite de ligne de cache de 64 octets (sa taille), et donc tout ce qui suit, il sera sur la ligne de cache suivante. Pour autant que je sache, les normes de langage C et C++ ne nécessitent que cela de structures entières, afin qu'elles puissent vivre correctement dans des tableaux, sans violer les exigences d'alignement d'aucun de leurs membres. (voir les commentaires)

L'approche attribute serait plus spécifique au compilateur, mais pourrait réduire de moitié la taille de cette structure, car le remplissage se limiterait à arrondir chaque élément à une ligne de cache complète. Cela pourrait être très bénéfique si l'on en avait beaucoup.

Le même concept s'applique aussi bien en C qu'en C++.

41
Phil Miller

Vous devrez peut-être vous aligner sur une limite de ligne de cache, qui est généralement de 64 octets par ligne de cache, lorsque vous travaillez avec des interruptions ou des lectures de données hautes performances, et elles sont obligatoires à utiliser lorsque vous travaillez avec des sockets interprocessus. Avec les sockets Interprocess, il existe des variables de contrôle qui ne peuvent pas être réparties sur plusieurs lignes de cache ou DDR RAM mots autrement, cela provoquera les L1, L2, etc. ou les caches ou DDR RAM pour fonctionner comme un filtre passe-bas et filtrer vos données d'interruption! QUE IS BAD !!! Cela signifie que vous obtenez des erreurs bizarres lorsque votre algorithme est bon et qu'il a le potentiel pour vous rendre fou!

Le DDR RAM va presque toujours lire en mots de 128 bits (DDR RAM Words), qui est de 16 octets, donc les variables du tampon en anneau ne doivent pas être réparti sur plusieurs DDR RAM mots. certains systèmes utilisent DDR 64 bits RAM mots et techniquement, vous pourriez obtenir un DDR 32 bits RAM Word sur un processeur 16 bits mais on utiliserait la SDRAM dans la situation.

On peut également être intéressé à minimiser le nombre de lignes de cache utilisées lors de la lecture de données dans un algorithme hautes performances. Dans mon cas, j'ai développé l'algorithme entier-chaîne le plus rapide au monde (40% plus rapide que l'algorithme le plus rapide précédent) et je travaille à l'optimisation de l'algorithme Grisu, qui est l'algorithme à virgule flottante le plus rapide du monde. Pour imprimer le nombre à virgule flottante, vous devez imprimer l'entier, afin d'optimiser l'optimisation de Grisu que j'ai implémentée, j'ai aligné les lignes de recherche (LUT) pour Grisu en exactement 15 lignes de cache, ce qui est plutôt étrange qu'il s'aligne comme ça. Cela prend les LUT de la section .bss (c'est-à-dire la mémoire statique) et les place sur la pile (ou tas mais la pile est plus appropriée). Je n'ai pas évalué cela, mais il est bon de le mentionner, et j'ai beaucoup appris à ce sujet.Le moyen le plus rapide de charger des valeurs est de les charger à partir de l'i-cache et non du d-cache. La différence est que l'i-cache est en lecture seule et a des lignes de cache beaucoup plus grandes car il est en lecture seule (2 Ko était ce qu'un professeur m'a cité une fois.). Donc, vous allez en fait dégrader vos performances de l'indexation des tableaux plutôt que de charger une variable comme celle-ci:

int faster_way = 12345678;

par opposition à la manière plus lente:

int variables[2] = { 12345678, 123456789};
int slower_way = variables[0];

La différence est que le int variable = 12345678 sera chargé à partir des lignes i-cache en se décalant vers la variable dans i-cache depuis le début de la fonction, tandis que slower_way = int[0] sera chargé à partir des plus petites lignes de cache en utilisant une indexation de tableau beaucoup plus lente. Cette subtilité particulière, comme je viens de le découvrir, ralentit en fait mon algorithme d'entier en chaîne et bien d'autres. Je dis cela parce que vous pensez peut-être que vous optimisez en alignant le cache sur les données en lecture seule lorsque vous ne l'êtes pas.

En général, en C++, vous utiliserez le std::align une fonction. Je conseillerais de ne pas utiliser cette fonction car il n'est pas garanti de fonctionner de manière optimale . Voici le moyen le plus rapide de s'aligner sur une ligne de cache, pour être franc, je suis l'auteur et c'est une prise sans vergogne:

Algorithme d'alignement de mémoire Kabuki Toolkit

namespace _ {
/* Aligns the given pointer to a power of two boundaries with a premade mask.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number of bits in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param mask The mask for the Least Significant bits to align. */
template <typename T = char>
inline T* AlignUp(void* pointer, intptr_t mask) {
  intptr_t value = reinterpret_cast<intptr_t>(pointer);
  value += (-value ) & mask;
  return reinterpret_cast<T*>(value);
}
} //< namespace _

// Example calls using the faster mask technique.

enum { kSize = 256 };
char buffer[kSize + 64];

char* aligned_to_64_byte_cache_line = AlignUp<> (buffer, 63);

char16_t* aligned_to_64_byte_cache_line2 = AlignUp<char16_t> (buffer, 63);

et voici le remplacement std :: align plus rapide:

inline void* align_kabuki(size_t align, size_t size, void*& ptr,
                          size_t& space) noexcept {
  // Begin Kabuki Toolkit Implementation
  intptr_t int_ptr = reinterpret_cast<intptr_t>(ptr),
           offset = (-int_ptr) & (align - 1);
  if ((space -= offset) < size) {
    space += offset;
    return nullptr;
  }
  return reinterpret_cast<void*>(int_ptr + offset);
  // End Kabuki Toolkit Implementation
}
3
user2356685