Автор в статье: так как мы хотим хранить значения в диапазоне [0, n-1], в качестве null'а возьмём 2n-1
Я: отлично, логично, всё понятненько
Автор в коде: очевидно, в качестве null можно взять использовать n-1
Я: *второй вечер бью голову об вопрос* почему...
Upd. ненулевой шанс, что имплементация подогнана под бенчи, а бенчи могут этот корнер-кейс и не задеть 🙃
Upd 2. попробовать что ли автору написать...
Upd 3. похоже, действительно алгоритм ломается, если брать n-1 вместо 2n-1, но бенчи это не покрывают и не особо проверяют. Да и бенчи не слишком-то честные...
Upd 4. или бенч всё-таки честный... но тогда и алгоритм работает? ну что за качели
ЪУЪ
Я: отлично, логично, всё понятненько
Автор в коде: очевидно, в качестве null можно взять использовать n-1
Я: *второй вечер бью голову об вопрос* почему...
Upd. ненулевой шанс, что имплементация подогнана под бенчи, а бенчи могут этот корнер-кейс и не задеть 🙃
Upd 2. попробовать что ли автору написать...
Upd 3. похоже, действительно алгоритм ломается, если брать n-1 вместо 2n-1, но бенчи это не покрывают и не особо проверяют. Да и бенчи не слишком-то честные...
Upd 4. или бенч всё-таки честный... но тогда и алгоритм работает? ну что за качели
ЪУЪ
🔥2
Ларчик просто открывался: я дурачок, всё работает и написано прям по статье
Ура! Справился-таки написать реализацию очереди
Сводки из приключений в процессе:
* кучу времени дебажил проблему, что часть записей в очередь теряется
* не мог понять, накосячил в реализации или в тесте
* в какой-то момент понял, что нашёл кейс, не покрытый алгоритмом в статье
* "ну точно где-то я нашизил"
* совершил немыслимое: залез в секцию "доказательство корректности" (обычно при прочтении статей у меня на ней мозг перемешивается в однородную кашицу, потому она скипается)
* осознал, что в тесте не учёл инвариант: в очереди не должно оказаться больше n элементов. Обеспечивается он тем, что это служебная очередь
В процессе думаю над тем, что можно обкромсать для MPSC кейса (который рискует оказаться для меня более интересным). Статья строит очередь произвольных элементов на двух очередях индексов: занятых и свободных (поскольку все n индексов, циркулирующих по очередям, прописаны на инициализации, инвариант, на который я наткнулся выше, всегда выполняется). То бишь, идейно выглядит так (вся магия спрятана внутри
Чтобы ослабить алгоритм до MPSC, понадобится MPSC очередь занятых индексов (туда пишут producer'ы, читает единственный consumer) и SPMC очередь для свободных индексов (из неё берут элементы producer'ы, туда пишет единственный consumer). Хотя погодите-ка. Реально очередь формируется из занятых индексов. Но свободные нам брать в порядке очереди вообще не обязательно (если не вредно). В статье про ослабленные очереди я натыкался на структуру данных bag: у неё есть операции
Но прежде, замеры и тюнинг оригинала. Изначально получал 10'000-кратный разброс в результатах, пока сократил до гордых 100 раз (методом распихивания по раздельным кеш-линиям всего, что распихивается). В SPSC режиме лучшая задержка, которую видел, в районе 2 микросекунд (что вообще-то дофига, но пока код не тюненный, да и железо третьей свежести), но чаще получаю несколько десятков. Что поделать, вынимаем напильник, дорабатываем
P.S. Ну и да, в процессе замеров обнаружил, что некий
Сводки из приключений в процессе:
* кучу времени дебажил проблему, что часть записей в очередь теряется
* не мог понять, накосячил в реализации или в тесте
* в какой-то момент понял, что нашёл кейс, не покрытый алгоритмом в статье
* "ну точно где-то я нашизил"
* совершил немыслимое: залез в секцию "доказательство корректности" (обычно при прочтении статей у меня на ней мозг перемешивается в однородную кашицу, потому она скипается)
* осознал, что в тесте не учёл инвариант: в очереди не должно оказаться больше n элементов. Обеспечивается он тем, что это служебная очередь
В процессе думаю над тем, что можно обкромсать для MPSC кейса (который рискует оказаться для меня более интересным). Статья строит очередь произвольных элементов на двух очередях индексов: занятых и свободных (поскольку все n индексов, циркулирующих по очередям, прописаны на инициализации, инвариант, на который я наткнулся выше, всегда выполняется). То бишь, идейно выглядит так (вся магия спрятана внутри
SCQ
очередей)class Queue {
bool try_enqueue(T elem) {
optional<size_t> idx = free.dequeue();
bool success = idx.has_value();
if (success) {
data[*idx] = elem;
acquired.enqueue(*idx);
}
return success;
}
optional<T> dequeue() {
optional<T> result;
optional<size_t> idx = acquired.dequeue();
if (idx) {
result = data[*idx];
free.enqueue(*idx);
}
return result;
}
SCQ free;
SCQ acquired;
T[] data;
};
Чтобы ослабить алгоритм до MPSC, понадобится MPSC очередь занятых индексов (туда пишут producer'ы, читает единственный consumer) и SPMC очередь для свободных индексов (из неё берут элементы producer'ы, туда пишет единственный consumer). Хотя погодите-ка. Реально очередь формируется из занятых индексов. Но свободные нам брать в порядке очереди вообще не обязательно (если не вредно). В статье про ослабленные очереди я натыкался на структуру данных bag: у неё есть операции
push(T)
и pop_any()
: она не обещает вообще никакого порядка, в котором из неё вынимаются элементы. Можно покопать в этом направлении (пока нашёл единственную статью 11-го года — буду вдохновляться, возможно, что-то ещё на эту тему найдётся)Но прежде, замеры и тюнинг оригинала. Изначально получал 10'000-кратный разброс в результатах, пока сократил до гордых 100 раз (методом распихивания по раздельным кеш-линиям всего, что распихивается). В SPSC режиме лучшая задержка, которую видел, в районе 2 микросекунд (что вообще-то дофига, но пока код не тюненный, да и железо третьей свежести), но чаще получаю несколько десятков. Что поделать, вынимаем напильник, дорабатываем
P.S. Ну и да, в процессе замеров обнаружил, что некий
kswapd0
жрёт у меня один из двух процов целиком, паскуда. Даже после ребута, когда использование свопа 0, использование RAM <10%. Неладное заподозрил, когда увидел, что он запущен из-под юзера, на котором у меня всякие докер-шняги крутятся, плюс запущен как ./kswapd0
. Судя по тому, что в интернете нашёл, это какой-то майнер. Сходу влезть в бинари не получилось, так что прикопал в архивчик и снёс. Где поймал — хер его поймиResearchGate
A lock-free algorithm for concurrent bags | Request PDF
Request PDF | A lock-free algorithm for concurrent bags | A lock-free bag data structure supporting unordered buffering is presented in this paper. The algorithm supports multiple producers and multiple... | Find, read and cite all the research you need on…
❤3
Приключения вчерашнего вечера:
* осознал, что долбить очередь producer'ами безостановочно — не очень осмысленно и так себе отражает ожидаемую нагрузку, так что воткнул nop'ов между enqueue'ями
* начал собирать таблички, данных, но из них понятно пока мало
* задержки грустные, средние 20микро на никогда не полной очереди это жесть (пик 1)
* жоско портят малину скачки в задержках неизвестного происхождения (пик 3). Пока только нащупываю, с чем скоррелировано
* Подозреваю заполненность очереди. Увеличение её размера с 32K до 1M улучшает все квантили, но задирает максимумы: видимо, когда очередь таки заполняется, мы в жопе
Продолжаем копать
* осознал, что долбить очередь producer'ами безостановочно — не очень осмысленно и так себе отражает ожидаемую нагрузку, так что воткнул nop'ов между enqueue'ями
* начал собирать таблички, данных, но из них понятно пока мало
* задержки грустные, средние 20микро на никогда не полной очереди это жесть (пик 1)
* жоско портят малину скачки в задержках неизвестного происхождения (пик 3). Пока только нащупываю, с чем скоррелировано
* Подозреваю заполненность очереди. Увеличение её размера с 32K до 1M улучшает все квантили, но задирает максимумы: видимо, когда очередь таки заполняется, мы в жопе
Продолжаем копать
Воюю с очередями каждый день, часть N
В какой-то момент умудрился увидеть задержку в 1 микросек, но уже не помню, при каких условиях и как вообще так. Больше я её не видел (спойлер: ещё увижу)
В целом, замеры очень шумные (медиана задержек может скакать в 3 раза) — долго с этим боролся
* Научился изолировать потоки под свои нечистые дела
* Мне раскрыли глаза на особенности устройства моих xeon'ов: на одном кристалле находятся два модуля по 8 ядер, а я пинился так, что потоки оказывались в разных модулях. Кеш-миссы свалились с 37% до 12%, пропускная способность подросла где-то на 50-60%
* Уложил все данные в один шмат памяти (до этого было 3: по штуке на вспомогательные очереди, 1 непосредственно с данными) — шиш мне это дало
* Вернулся к гипнотизированию flamegraph'ов. Поймал случай с хорошими и плохими задержками. (Сейчас стоит вспомнить, что я рассматриваю MPSC применение) Увидел, что у consumer'а ухудшается часть
Вывод. Если consumer не справляется, всё начинает идтипи... плохо. Я пытался от этого отгородиться, напихав producer'ам nop'ов, но их оказалось недостаточно, иногда — вообще недостаточно. 2000 nop'ов против 1000 nop'ов — пик 1 и 2 соответственно
Что делать. Раз страдать начинает очередь
Что делать ещё. До сих пор это MPMC очередь. Когда дожму с неё всё, что смогу, пойду исследовать, что мне может дать релаксация до MPSC случая
Сайд-квест 1. Насколько я знаю, на x64 размер кеш-линии это 64 байта. Но в референсной реализации выравнивание штуков было по 128 байт. На практике выравнивание действительно надо делать по 128, потому что на 64 перформанс проседает. Интересно, почему так
Сайд-квест 2. Во вспомогательных очередях индексы самой очереди перемешиваются так, чтобы как можно дольше не заходить в одну кеш-линию. Интересно, что будет, если сменить условие на "как можно дольше не заходить на одну кеш-линию, оставаясь на одной странице"
В какой-то момент умудрился увидеть задержку в 1 микросек, но уже не помню, при каких условиях и как вообще так. Больше я её не видел (спойлер: ещё увижу)
В целом, замеры очень шумные (медиана задержек может скакать в 3 раза) — долго с этим боролся
* Научился изолировать потоки под свои нечистые дела
* Мне раскрыли глаза на особенности устройства моих xeon'ов: на одном кристалле находятся два модуля по 8 ядер, а я пинился так, что потоки оказывались в разных модулях. Кеш-миссы свалились с 37% до 12%, пропускная способность подросла где-то на 50-60%
* Уложил все данные в один шмат памяти (до этого было 3: по штуке на вспомогательные очереди, 1 непосредственно с данными) — шиш мне это дало
* Вернулся к гипнотизированию flamegraph'ов. Поймал случай с хорошими и плохими задержками. (Сейчас стоит вспомнить, что я рассматриваю MPSC применение) Увидел, что у consumer'а ухудшается часть
free.enqueue()
, а у producer'а — free.dequeue()
(см. выше). Более того, во вспомогательных очередях есть функция catchup
, которая вызывается, если dequeue
убежал за enqueue
(случается с FAA-очередями). В хорошем прогоне эта функция не светится, в плохом — становится очень заметнойВывод. Если consumer не справляется, всё начинает идти
Что делать. Раз страдать начинает очередь
free
, разгонять надо её. Ещё до этого у меня была мысль, что именно очередь свободных индексов — оверкилл. У нас тут множество элементов от 0 до n-1. По-любому можно сделать прощеЧто делать ещё. До сих пор это MPMC очередь. Когда дожму с неё всё, что смогу, пойду исследовать, что мне может дать релаксация до MPSC случая
Сайд-квест 1. Насколько я знаю, на x64 размер кеш-линии это 64 байта. Но в референсной реализации выравнивание штуков было по 128 байт. На практике выравнивание действительно надо делать по 128, потому что на 64 перформанс проседает. Интересно, почему так
Сайд-квест 2. Во вспомогательных очередях индексы самой очереди перемешиваются так, чтобы как можно дольше не заходить в одну кеш-линию. Интересно, что будет, если сменить условие на "как можно дольше не заходить на одну кеш-линию, оставаясь на одной странице"
🔥8
Люблю программирование
Иногда ты отлавливаешь всратый рейс в сетевом протоколе в качестве приключения на 20 минут
А иногда ты 2-3 часа не можешь понять, что у тебя в системе не льются данные, потому что их источник не запущен
Иногда это происходит в один день (сегодня, например)
Иногда ты отлавливаешь всратый рейс в сетевом протоколе в качестве приключения на 20 минут
А иногда ты 2-3 часа не можешь понять, что у тебя в системе не льются данные, потому что их источник не запущен
Иногда это происходит в один день (сегодня, например)
👏4❤1
Кресты на моей кукухе
Воюю с очередями каждый день, часть N В какой-то момент умудрился увидеть задержку в 1 микросек, но уже не помню, при каких условиях и как вообще так. Больше я её не видел (спойлер: ещё увижу) В целом, замеры очень шумные (медиана задержек может скакать в…
Воюю с очередями ваще не каждый день, часть N + 1
Пошёл выполнять сайд-квест 2(потому что на основной квест-линии надо думать)
Итак, у нас есть две вспомогательные очереди, которые держат индексы от 0 до n. Внутри циклический массив, собственно, индексов. Решаем вопрос, как выбрать, куда писать очередной элемент.
Тривиальное решение:
Плюс: просто
Минус: false-sharing (несколько потоков будут писать в одну кеш-линию -> инвалидировать её друг другу -> страдать)
Решение из референсной реализации очереди:
1. Возьмём
2. Битово сдвинем
3. Старшие 4 бита справа от
Так мы не вернёмся к элементу в первой кеш-линии, пока не дойдём до упора в
Плюс: мы не упрёмся в false-sharing
Минус: вымываем кеши на турбированной скорости, на каждом
Решение, которое я решил опробовать:
То же, что в предыдущем, но
3. Старшие 4 бита справа от `log2(PAGE_SIZZE / sizeof(uint64_t))`перенесём в начало
Теперь мы вернёмся к элементу в первой кеш-линии уперевшись не в
Плюсы:
* Всё ещё не так страдаем от false-sharing'а, как в тривиальном случае
* Плотнее сидим в кешах
Минусы:
* Напороться на false-sharing стало легче: раз в 32 вставки (4096 / 128)
* В частности, пока непонятно, насколько хорошо будет работать для >32 одновременных producer'ов
Перф говорит, что действительно в кеши стали попадать почаще (было: пик1, стало: пик2). Но есть ли смысл?
To be continued...
Пошёл выполнять сайд-квест 2
Итак, у нас есть две вспомогательные очереди, которые держат индексы от 0 до n. Внутри циклический массив, собственно, индексов. Решаем вопрос, как выбрать, куда писать очередной элемент.
Тривиальное решение:
uint64_t idx = tail_++ % capacity_;
Плюс: просто
Минус: false-sharing (несколько потоков будут писать в одну кеш-линию -> инвалидировать её друг другу -> страдать)
Решение из референсной реализации очереди:
1. Возьмём
uint64_t local_tail = tail_++;
2. Битово сдвинем
local_tail
влево на log2(CACHE_LINE_SIZE / sizeof(uint64_t))
(в цифрах: log2(128 / 8) = 4
)3. Старшие 4 бита справа от
log2(capacity_)
перенесём в началоТак мы не вернёмся к элементу в первой кеш-линии, пока не дойдём до упора в
capacity_
Плюс: мы не упрёмся в false-sharing
Минус: вымываем кеши на турбированной скорости, на каждом
enqueue
перепрыгивая через 128 байтРешение, которое я решил опробовать:
То же, что в предыдущем, но
3. Старшие 4 бита справа от `log2(PAGE_SIZZE / sizeof(uint64_t))`перенесём в начало
Теперь мы вернёмся к элементу в первой кеш-линии уперевшись не в
capacity_
, а в размер страницыПлюсы:
* Всё ещё не так страдаем от false-sharing'а, как в тривиальном случае
* Плотнее сидим в кешах
Минусы:
* Напороться на false-sharing стало легче: раз в 32 вставки (4096 / 128)
* В частности, пока непонятно, насколько хорошо будет работать для >32 одновременных producer'ов
Перф говорит, что действительно в кеши стали попадать почаще (было: пик1, стало: пик2). Но есть ли смысл?
To be continued...
🤯2🔥1
Воюю с очередями ваще не каждый день, часть N + 1 (измерительная)
Что нам дало сидение на одной странице (на моём потенциально однобоком бенче)
1. Консьюмер стал дышать легче. Как выяснил раньше, консьюмер захлёбывается, если продьюсер крутит 1000 nop'ов между вставками, и страдает куда меньше при 3000 nop'ов. Решил посмотреть, как справляемся теперь с паузой в 1000 nop'ов. Медианная задержка упала с 16мкс до 2мкс. Приятно (пик1 -> пик2)
2. По задержкам интересно. Сказал бы, что медианы в районе погрешности, но пусть мой способ действительно похуже. Однако, у моего способа хвост задержек потоньше (пик3). Лучше это видно на табличках: пик4 для варианта автора, пик5 для моего. Авторский вариант (пик4) показывает себя лучше на низких квантилях, мой (пик5) — на высоких (и, внезапно, в среднем)
3. Раньше меня преследовало число 128. Теперь ещё и 512. Рисую задержки для каждого элемента (пик6). Ясно видно, что у выбросов есть периодичность. Выбросы поредее происходят раз в 128 элементов. Выбросы погуще — раз в 512.Я ВСЁ ЕЩЁ НЕ ПОНИМАЮ, ПОЧЕМУ, ЭТИ ЧИСЛА ПРЕСЛЕДУЮТ МЕНЯ, ПОМОГИТЕ . Оказывается, видимая разница в выбросах на 128 и на 512 появилась на моменте введения 3000 nop'ов (пик7), явно видно, что они плотнее. То есть, как-то это с походами в память связано. Но пока я не смог придумать, в какую формулу подставить числа, чтоб всё сложилось
4. Попытка херачить в MPSC режиме без nop'ов всё ещё гробит всё настолько, что с nop'ами быстрее 🙂
Итого, стоит ли оно того? Спорно. Высокие квантили спиленные настолько, что делают среднее лучше, меня прельщают, так что для меня — да
Что ж. Дальше придётся-таки воспользоваться так называемым умом, и написать параллельный битсет, чтобы попробовать выбросить очередь свободных индексов
Что нам дало сидение на одной странице (на моём потенциально однобоком бенче)
1. Консьюмер стал дышать легче. Как выяснил раньше, консьюмер захлёбывается, если продьюсер крутит 1000 nop'ов между вставками, и страдает куда меньше при 3000 nop'ов. Решил посмотреть, как справляемся теперь с паузой в 1000 nop'ов. Медианная задержка упала с 16мкс до 2мкс. Приятно (пик1 -> пик2)
2. По задержкам интересно. Сказал бы, что медианы в районе погрешности, но пусть мой способ действительно похуже. Однако, у моего способа хвост задержек потоньше (пик3). Лучше это видно на табличках: пик4 для варианта автора, пик5 для моего. Авторский вариант (пик4) показывает себя лучше на низких квантилях, мой (пик5) — на высоких (и, внезапно, в среднем)
3. Раньше меня преследовало число 128. Теперь ещё и 512. Рисую задержки для каждого элемента (пик6). Ясно видно, что у выбросов есть периодичность. Выбросы поредее происходят раз в 128 элементов. Выбросы погуще — раз в 512.
4. Попытка херачить в MPSC режиме без nop'ов всё ещё гробит всё настолько, что с nop'ами быстрее 🙂
Итого, стоит ли оно того? Спорно. Высокие квантили спиленные настолько, что делают среднее лучше, меня прельщают, так что для меня — да
Что ж. Дальше придётся-таки воспользоваться так называемым умом, и написать параллельный битсет, чтобы попробовать выбросить очередь свободных индексов