Неблокирующий кольцевой буфер Lockfree множественного производителя множественного потребителя с записью переменной длины

Я хочу передавать сообщения переменной длины от нескольких производителей нескольким потребителям с очередью с малой задержкой в ​​многосокетных системах Xeon E5. (Например, 400 байт с задержкой 300 нс было бы неплохо.)

Я искал существующие реализации незаблокированных очередей с несколькими производителями и несколькими потребителями (MPMC) с использованием неблокирующего кольцевого буфера. Но большинство реализаций/алгоритмов в Интернете основаны на узле (т. е. узел имеет фиксированную длину), например boost::lockfree::queue, midishare и т. д.

Конечно, можно возразить, что тип узла может быть установлен на uint8_t или что-то подобное, но тогда запись будет корявой, а производительность будет ужасной.

Я также хотел бы, чтобы алгоритм предлагал обнаружение перезаписи на стороне читателей, чтобы читатели обнаруживали перезаписываемые данные.

Как я могу реализовать очередь (или что-то еще), что делает это?


person HCSF    schedule 17.08.2018    source источник
comment
@close избиратели: я перефразировал этот вопрос, чтобы он не был запросом библиотеки. Ранее он был написан таким образом, но интересен лежащий в основе вопрос разработки алгоритма.   -  person Peter Cordes    schedule 17.08.2018


Ответы (2)


Извините за поздний ответ, но взгляните на библиотеку Ring DPDK. Он бесплатный (лицензия BSD), молниеносно быстр (сомневаюсь, что вы найдете более быстрое решение бесплатно) и поддерживает все основные архитектуры. Так же есть масса примеров.

для передачи сообщений переменной длины

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

Таким образом, общее решение будет таким:

  1. Создайте мемпул(ы) для совместного использования буферов между потоками/процессами. Каждый мемпул поддерживает только буфер фиксированного размера, поэтому вы можете захотеть создать несколько мемпулов в соответствии с вашими потребностями.

  2. Создайте одно кольцо MPMC или набор пар колец SPSC между потоками/процессами. Решение SPSC может быть быстрее, но оно может не соответствовать вашему проекту.

  3. Производитель выделяет буфер, заполняет его и передает указатель на этот буфер через кольцо.

  4. Потребитель получает указатель, читает сообщение и освобождает буфер.

Звучит много работы, но в мемпулах и кольцах DPDK есть много оптимизаций. Но подойдет ли он для 300 нс?

Ознакомьтесь с официальными отчетами о производительности DPDK. Хотя официального отчета о производительности кольца нет, есть результаты тестирования vhost/vistio. В основном пакеты перемещаются следующим образом:

Traffic gen. -- Host -- Virtual Machine -- Host -- Traffic gen.

Хост работает как один процесс, виртуальная машина как другой.

Результат теста составляет ~4 млн пакетов в секунду для пакетов размером 512 байт. Это не вписывается в ваш бюджет, но вам нужно сделать намного, гораздо меньше работы...

person Andriy Berestovskyy    schedule 27.09.2018

Вы, вероятно, захотите поместить указатели в свою очередь, а не копировать данные в/из самого общего кольца. то есть полезная нагрузка кольцевого буфера - это просто указатель.

Семантика освобождения/получения заботится о том, чтобы данные были там, когда вы разыменовываете указатель, полученный из очереди. Но тогда у вас возникает проблема с освобождением памяти: как производитель узнает, когда потребитель закончил использование буфера, чтобы он мог использовать его повторно?

Если можно передать право собственности на буфер, то все в порядке. Может быть, потребитель может использовать буфер для чего-то другого, например, добавить его в локальный свободный список или, возможно, использовать его для чего-то, что он производит.


Чтобы узнать следующее, см. анализ очереди MPMC без блокировки на основе кольцевого буфера в Гарантии выполнения без блокировки< /а>. Я представляю себе модификации, которые сделают его пригодным для ваших целей.

Он имеет индекс чтения и индекс записи, а каждый узел кольцевого буфера имеет счетчик последовательности, который позволяет ему определять, что пишущие догоняют читающих (очередь заполнена) по сравнению с читающими, догоняющими писателей (очередь пуста), не создавая конкуренции. между читателями и писателями. (IIRC, читатели читают индекс записи или наоборот, но нет общих данных, которые изменяются как читателями, так и писателями.)


Если существует разумная верхняя граница размера буфера, вы могли бы иметь общие буферы фиксированного размера, связанные с каждым узлом в кольцевом буфере. Например, 1 КБ или 4 КБ. Тогда вам не понадобится полезная нагрузка в кольцевом буфере; индекс был бы интересной вещью.

Если выделение памяти не имеет большого значения (только кеш-память), даже буферы размером 64 КБ или 1 МБ будут в основном хороши, даже если вы обычно используете только младшие 400 байтов каждого. Части буфера, которые не используются, просто останутся холодными в кеше. Если вы используете огромные страницы размером 2 МБ, буферы меньшего размера являются хорошей идеей, чтобы уменьшить нагрузку на TLB: вы хотите, чтобы несколько буферов покрывались одной и той же записью TLB.

Но вам нужно будет запросить буфер перед записью в него и закончить запись в него до завершения второго шага добавления записи в очередь. Вы, вероятно, не хотите делать больше, чем просто memcpy, потому что частично завершенная запись блокирует чтение, если она становится самой старой записью в очереди до ее завершения. Возможно, вы могли бы выполнить предварительную запись буфера ( с prefetchw в Broadwell или новее), прежде чем пытаться его заявить, чтобы сократить время между тем, как вы (потенциально) блокируете очередь. Но если за писателей мало разногласий, это может не иметь значения. И если существует высокая конкуренция, поэтому вам (почти) не всегда удается заявить права на первый пробуемый вами буфер, предварительная выборка записи в неправильном буфере замедлит работу программы чтения или записи, которой он принадлежит. Может быть, обычная предварительная выборка была бы хороша.

Если буферы привязаны непосредственно к записям очереди, возможно, вам следует просто поместить их в очередь в очередь, если библиотека MPMC позволяет использовать пользовательский код чтения, который считывает длину и копирует столько байтов, вместо того, чтобы всегда копировать весь гигантский массив.

Тогда каждая запись управления очередью, которую просматривают производители/потребители, будет находиться в отдельной строке кэша, поэтому между двумя производителями, требующими смежных записей, не будет конкуренции.

Если вам нужны действительно большие буферы, потому что ваша верхняя граница составляет около 1 МБ или что-то в этом роде, повторные попытки из-за конкуренции приведут к касанию большего количества записей TLB, поэтому более компактный кольцевой буфер с отдельными большими буферами может быть лучшей идеей.


Читатель, затребовавший буфер на полпути, не блокирует другие читатели. Он блокирует очередь только в том случае, если она циклически повторяется, а производитель застревает в ожидании. Таким образом, вы определенно можете заставить своих читателей использовать данные на месте в очереди, если она достаточно велика и читатели быстры. Но чем больше вы делаете во время частично завершенного чтения, тем больше шансов, что вы заснете и в конечном итоге заблокируете очередь.

Это гораздо более серьезное дело для производителей, особенно если очередь обычно (почти) пуста: потребители подходят к вновь написанным записям почти сразу после их создания. Вот почему вы можете захотеть выполнить предварительную выборку данных, которые вы собираетесь копировать, и/или самого общего буфера перед запуском производителя.


400 байт — это всего 12,5 циклов фиксации 32 байтов за такт в кэше L1d (например, Intel Haswell/Skylake), так что это действительно мало по сравнению с межъядерными задержками или временем вам нужно дождаться RFO в кэш-памяти. Таким образом, минимальное время между тем, когда производитель делает заявку на узел видимой во всем мире, и моментом, когда вы завершаете эту заявку, чтобы читатели могли ее прочитать (и более поздние записи), по-прежнему очень мало. Блокировка очереди на долгое время надеюсь избежать.

Такое количество данных помещается даже в регистры YMM 13, так что теоретически компилятор может фактически загрузить данные в регистры до того, как запросить запись в буфер, и просто сохранить их. Возможно, вы могли бы сделать это вручную с помощью встроенных функций с полностью развернутым циклом. (Вы не можете проиндексировать регистровый файл, поэтому он должен быть полностью развернут или всегда хранить 408 байт или что-то еще.)

Или 7 регистров ZMM с AVX512, но вы, вероятно, не хотите использовать 512-битные загрузки/сохранения, если вы не используете другие 512-битные инструкции, из-за влияния на тактовую частоту max-turbo и отключения порта 1 для вектор АЛУ упс. (Я предполагаю, что это все еще происходит с векторной загрузкой/сохранением, но, если нам повезет, некоторые из этих эффектов происходят только с 512-битными операциями ALU...)

person Peter Cordes    schedule 17.08.2018
comment
Спасибо за ответ. Много информации для переваривания. Итак, я сначала отвечаю на ваш последний раздел - да, передача 400 байт менее чем за 300 нано очень короткая / быстрая. Следовательно, я думаю, что блокировка очереди даже не должна происходить (при условии, что префикс блокировки для инструкции CAS в x86 не считается блокированием, хотя он действительно блокирует). У моего клиента есть работающий код, достигающий такой задержки, но из-за IP я не могу опубликовать здесь. Но в коде есть ошибка, из-за которой читатели не могут обнаружить перезапись буфера. Вот почему я поднял этот вопрос. Позвольте мне прочитать остальную часть вашего ответа. Спасибо! - person HCSF; 17.08.2018
comment
Просто пояснение к заявлению о задержке для кода моего клиента - когда есть больше читателей, общая задержка записи-чтения увеличивается (например, увеличивается на 100%, когда есть около 6 читателей, но 1 писатель над SHM в linux ; что до сих пор остается для меня загадкой, учитывая, что читатели не использовали инструкции с префиксом забора или блокировки, основанные на objdump). Просто пытаюсь быть справедливым. - person HCSF; 17.08.2018
comment
@HCSF: 300 нс - это 900 тактовых циклов на процессоре с тактовой частотой 3 ГГц. Это довольно быстро для автономной работы, но в пределах одного ядра это возраст. Dr. Bandwidth измерил задержку между сокетами в 240 нс на старой системе Sandybridge-Xeon, просто проверив одну строку кэша. (software.intel.com/en -нас/форумы/). Задержка может быть аналогичной для ваших небольших буферов фиксированного размера, потому что несколько строк кэша могут быть запущены одновременно. Использование буферов, привязанных к узлам, вместо размещения указателей в узлах означает, что этим нагрузкам не нужно ждать адреса. - person Peter Cordes; 17.08.2018
comment
Внутри одного чипа межъядерная задержка может составлять всего 50 нс для четырехъядерного рабочего стола или 16 нс между гиперпотоками одного физического ядра. Выше для многоядерных процессоров Xeon, потому что кольцевая шина имеет больше переходов. SiSoft показывает некоторые тесты задержки между ядрами (sisoftware.co.uk/2017/06/24/), включая 10-ядерный Skylake i9-7900X со скоростью 80 нс. (Однако это не часть системы с несколькими сокетами, что может иметь значение, потому что, вероятно, он должен отслеживать другой сокет.) - person Peter Cordes; 17.08.2018
comment
Есть ли у вас пример кода для иллюстрации Использование буферов, привязанных к узлам, вместо размещения указателей в узлах означает, что этим нагрузкам не нужно ждать адреса? Может быть, я неправильно понимаю - я предполагаю, что каждый узел будет иметь что-то вроде uint8_t data[NODE_SIZE], а затем, когда загрузится считыватель, ему все равно нужно будет сделать что-то вроде data[0], что по существу отменяет и получает адрес? Я должен неправильно понять. - person HCSF; 17.08.2018
comment
@HCSF: я думал что-то вроде struct node_buf { size_t size; alignas(32) char data[4096-32]; };. Тогда queue<node_buf> работает, но скопирует все 4 КБ, даже если вы используете только первые несколько. Вот почему вам нужны специальные функции чтения/записи, которые знают, что им нужно скопировать только до size байтов из data. Да, вы должны разыменовать размер перед чтением буфера, но он находится в той же строке кеша, что и данные. (и предсказание ветвлений в любом случае скрывает эту задержку.) Всего лишь с указателем, это вероятно. не будет в кеше, но этот запрос не может начаться, пока не появится указатель. - person Peter Cordes; 17.08.2018
comment
О, я вижу. Тогда на самом деле то же самое можно сделать без фиксированного размера узла. Это может быть что-то вроде [uint32_t][данные переменной длины с выравниванием 4].... пока не знаю, какое преимущество дает фиксированная длина... кроме более простой реализации... дайте мне подумать об этом подробнее. - person HCSF; 17.08.2018
comment
ваша ссылка на у кольцевой шины больше переходов отлично. Это объясняет, почему я вижу более высокую задержку при обмене данными SHM между ядрами в одном и том же сокете в Skylake! Вздох - person HCSF; 17.08.2018
comment
огромная страница - это хорошо. Насколько я знаю, Linux предлагает только 2 размера огромных страниц — 2 МБ или 1 ГБ. Если у меня есть несколько сегментов общей памяти размером 1 МБ, 4 МБ и 8 МБ в качестве кольцевых буферов в этом вопросе, не будет ли лучше поместить их все на одну огромную страницу размером 1 ГБ (при условии, что это возможно), чтобы мне нужна была только одна запись TLB в каждое ядро? И, вероятно, он останется горячим, поскольку производители и потребители будут продолжать получать один из сегментов общей памяти? - person HCSF; 17.08.2018
comment
@HCSF Вам нужно, чтобы он был фиксированного размера, чтобы ваш кольцевой буфер мог быть массивом! Индексация массива работает, только если элементы имеют фиксированный размер. И если они выделяются только один раз в начале, они не могут расти позже, поэтому все они должны быть достаточно большими. - person Peter Cordes; 17.08.2018
comment
@HCSF: Linux прозрачно использует 2M огромных страниц для анонимной памяти. Я думаю, вы можете явно использовать огромные страницы размером 1 ГБ, но для этого требуется, чтобы ОС нашла 1 ГБ непрерывной физической памяти. Выполнение этого распределения может потребовать много дефрагментации. Я думаю, что возможно, но неудобно использовать огромные страницы 1G в пользовательском пространстве в Linux. Skylake имеет только 4 записи L1dTLB для страниц 1G по сравнению с 32 для страниц 2M (7-cpu .com/cpu/Skylake.html), а старые были хуже, поэтому это могло снизить производительность. pvk.ca/Blog/2014 /02/18/how-bad-can-1gb-pages-be выглядит интересно и, возможно, правильно, но я только бегло просмотрел - person Peter Cordes; 17.08.2018
comment
Правильно, я согласен. Я думаю, мы говорим о 2 разных вещах. Я, вероятно, неправильно понимаю ваш узел фиксированной длины. На мой взгляд (по крайней мере, boost::lockfree::queue), я думал о кольцевом буфере (фиксированной длины) с несколькими узлами, каждый узел также имеет фиксированную длину. - person HCSF; 17.08.2018
comment
да, некоторые люди предлагают установить параметр загрузки ядра, чтобы выделить 1 ГБ огромных страниц во время загрузки, чтобы увеличить вероятность успеха. В идеале я должен использовать меньшие сегменты общей памяти. Просто я еще не понял, как справиться с тем, что читатели слишком медленные. Следовательно, планирую использовать более крупные (в качестве хака), пока не найду хороший кольцевой буфер MPMC с обнаружением перезаписи. Также желателен меньший общий размер общей памяти, поскольку все может поместиться в кеш L3 (надеюсь). - person HCSF; 17.08.2018
comment
Вопросы и ответы по MPMC без блокировки, на которые я ссылался, касаются реализации в liblfds, которая обнаруживает и блокирует заполненную очередь вместо того, чтобы молча перезаписывать непрочитанные записи. См. Раздел в моем ответе для очень краткого описания того, как это работает, и этот раздел вопросов и ответов (или сам код) для получения более подробной информации. - person Peter Cordes; 17.08.2018