Как и когда выровнять по размеру строки кэша? - PullRequest
57 голосов
/ 12 декабря 2011

В превосходной ограниченной очереди mpmc Дмитрия Вьюкова, написанной на C ++ Смотри: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

Он добавляет несколько переменных заполнения. Я предполагаю, что это для выравнивания строки кэша для повышения производительности.

У меня есть несколько вопросов.

  1. Почему это сделано таким образом?
  2. Это портативный метод, который будет всегда работать
  3. В каких случаях лучше использовать __attribute__ ((aligned (64))).
  4. почему заполнение перед указателем буфера помогает с производительностью? это не просто указатель, загруженный в кеш, так что это действительно только размер указателя?

    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_;
    

Будет ли эта концепция работать под gcc для кода c?

Ответы [ 2 ]

41 голосов
/ 12 декабря 2011

Это сделано таким образом, чтобы различным ядрам, изменяющим разные поля, не приходилось отражать строку кеша, содержащую их обоих, между их кешами.В общем случае, чтобы процессор мог обращаться к некоторым данным в памяти, вся строка кеша, содержащая их, должна находиться в локальном кеше этого процессора.Если он изменяет эти данные, эта запись в кеше обычно должна быть единственной копией в любом кеше в системе (эксклюзивный режим в стиле MESI / MOESI протоколы когерентности кэша ).Когда отдельные ядра пытаются изменить разные данные, которые находятся в одной и той же строке кэша, и, таким образом, тратить время на перемещение всей строки назад и вперед, это называется ложное совместное использование .

.В конкретном примере, который вы приводите, одно ядро ​​может ставить в очередь запись (чтение (совместно используемая) buffer_ и запись (эксклюзивно) только enqueue_pos_), в то время как другое ядро ​​выключает (совместно используемая buffer_ и эксклюзивно dequeue_pos_) без остановки ядрастрока кэша, принадлежащая другой.

Заполнение в начале означает, что buffer_ и buffer_mask_ заканчиваются на одной и той же строке кэша, а не разбиваются на две строки и, таким образом, требуют двойного трафика памяти для доступа.

Я не уверен, является ли техника полностью переносимой. Предполагается, что каждый cacheline_pad_t сам будет выровнен по границе строки кэша размером 64 байта (его размер), и, следовательно, все, что следует, будет в следующей строке кэша.Насколько я знаю, стандарты языка C и C ++ требуют этого только для целых структур, чтобы они могли красиво жить в массивах, не нарушая требований выравнивания любого из их членов. (см. Комментарии)

Подход attribute будет более специфичным для компилятора, но может сократить размер этой структуры вдвое, поскольку заполнение будет ограничено округлением каждого элемента до полной строки кэша.Это может быть очень полезно, если у вас их много.

Та же концепция применима и к C, и к C ++.

3 голосов
/ 22 августа 2018

Может потребоваться выравнивание по границе строки кэша, которая обычно составляет 64 байта на строку кэша, когда вы работаете с прерываниями или высокопроизводительным чтением данных, и их обязательно использовать при работе с межпроцессными сокетами.В случае межпроцессных сокетов существуют управляющие переменные, которые не могут быть распределены по нескольким строкам кэша или словам памяти DDR, иначе это приведет к тому, что L1, L2 и т. Д., Или кэши или память DDR, будут функционировать как фильтр нижних частот и отфильтровывать ваши данные прерываний.!ЭТО ПЛОХО!!!Это означает, что вы получаете странные ошибки, когда ваш алгоритм хорош и потенциально может заставить вас сходить с ума!

ОЗУ DDR почти всегда будет читать 128-битные слова (слова DDR RAM), что16 байт, поэтому переменные кольцевого буфера не должны быть распределены по нескольким словам DDR RAM.некоторые системы используют 64-битные слова DDR RAM, и технически вы можете получить 32-битное слово DDR RAM на 16-битном процессоре, но в этой ситуации можно использовать SDRAM.

Можно также просто заинтересоватьсяминимизация количества строк кэша, используемых при считывании данных в высокопроизводительном алгоритме.В моем случае я разработал самый быстрый в мире алгоритм целочисленных строк (на 40% быстрее, чем предыдущий самый быстрый алгоритм), и я работаю над оптимизацией алгоритма Грису, который является самым быстрым в мире алгоритмом с плавающей запятой.Чтобы напечатать число с плавающей запятой, вы должны вывести целое число, поэтому, чтобы оптимизировать Grisu, я реализовал одну оптимизацию, состоящую в том, что я выровнял таблицы поиска (LUT) для Grisu с выравниванием по кэш-строке в ровно 15 строк кэша, чтодовольно странно, что на самом деле это выровняли так.Это берет LUT из секции .bss (то есть статической памяти) и помещает их в стек (или кучу, но стек больше подходит).Я не проверял это, но это хорошо, и я многое узнал об этом. Самый быстрый способ загрузить значения - это загрузить их из i-кеша, а не из d-кеша.Разница в том, что i-кеш доступен только для чтения и имеет гораздо большие строки кеша, потому что он только для чтения (2 КБ было тем, что профессор однажды процитировал мне).Таким образом, вы на самом деле собираетесь снизить производительность при индексировании массива, а не загружать переменную следующим образом:

int faster_way = 12345678;

, а не медленнее:

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

Разница в том,что int variable = 12345678 будет загружен из строк i-кеша путем смещения переменной в i-кеше с начала функции, в то время как slower_way = int[0] будет загружен из меньших строк d-кеша с использованием намного более медленной индексации массива,Это тонкое, как я только что обнаружил, на самом деле замедляет мой и многие другие целочисленные алгоритмы.Я говорю это потому, что вы можете оптимизировать, выравнивая кеш-данные только для чтения, когда вы этого не делаете.

Как правило, в C ++ вы будете использовать функцию std::align.Я бы посоветовал не использовать эту функцию, потому что не гарантирует оптимальной работы .Вот самый быстрый способ выравнивания по строке кэша, которая, прежде всего, я автор, и это бесстыдный плагин:

Алгоритм выравнивания памяти 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);

и здесьэто более быстрая замена 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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...