Vous pouvez avoir besoin de 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 à haute performance, et il est obligatoire de les utiliser lorsque vous travaillez avec des sockets interprocessus. Avec les sockets interprocessus, certaines variables de contrôle ne peuvent pas être réparties sur plusieurs lignes de cache ou mots DDR RAM, sinon les caches L1, L2, etc. ou la DDR RAM fonctionneront comme un filtre passe-bas et filtreront vos données d'interruption ! C'EST MAUVAIS ! !! Cela signifie que vous obtenez des erreurs bizarres alors que votre algorithme est bon et cela a le potentiel de vous rendre fou !
La mémoire vive DDR est presque toujours lue en mots de 128 bits (mots de mémoire vive DDR), soit 16 octets, de sorte que les variables du tampon circulaire ne doivent pas être réparties sur plusieurs mots de mémoire vive DDR. Certains systèmes utilisent des mots de mémoire vive DDR de 64 bits et, techniquement, il est possible d'obtenir un mot de mémoire vive DDR de 32 bits sur un processeur de 16 bits, mais il est préférable d'utiliser de la mémoire SDRAM dans ce cas.
On peut aussi simplement être intéressé par la minimisation du nombre de lignes de cache utilisées lors de la lecture des données dans un algorithme à haute performance. Dans mon cas, j'ai développé l'algorithme integer-to-string le plus rapide du 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, il faut imprimer le nombre entier, donc pour optimiser Grisu, j'ai aligné les tables de consultation (LUT) de Grisu sur 15 lignes de cache exactement, ce qui est plutôt étrange qu'elles soient alignées comme ça. Cela prend les LUTs de la section .bss (c'est-à-dire la mémoire statique) et les place sur la pile (ou le tas, mais la pile est plus appropriée). Je n'ai pas fait d'analyse comparative mais il est bon de le souligner, et j'ai beaucoup appris à ce sujet, la façon la plus rapide de charger des valeurs est de les charger à partir du i-cache et non du d-cache. La différence est que le i-cache est en lecture seule et a des lignes de cache beaucoup plus grandes parce qu'il est en lecture seule (2KB c'est ce qu'un professeur m'a cité une fois.). Vous allez donc dégrader vos performances en indexant les tableaux plutôt qu'en chargeant une variable comme celle-ci :
int faster_way = 12345678;
par opposition à la voie la 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 du i-cache en décalant la variable dans le i-cache depuis le début de la fonction, tandis que slower_way = int[0]
sera chargé à partir des lignes plus petites du d-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 integer-to-string et celui de beaucoup d'autres. Je dis cela parce que vous pouvez penser que vous optimisez en alignant en cache des données en lecture seule alors que ce n'est pas le cas.
Typiquement, en C++, vous utiliserez la fonction std::align
fonction. Je conseille de ne pas utiliser cette fonction car il n'est pas garanti qu'il fonctionne 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 publicité éhontée :
Algorithme d'alignement de la mémoire de la boîte à outils Kabuki
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 plus rapide de std::align :
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
}
1 votes
Pourquoi utiliser le cacheline_size comme taille du tampon, plutôt que ( cacheline_size - sizeof( std::atomic<size_t> ) ) ?