Фан факт: если не трогать окулус квест слишком долго, он магически превращается в кусок пластика
А ещё тот факт, что приложение, нужное для коннекта шлема с компом, требует фейсбук логина, вымораживает
А ещё тот факт, что приложение, нужное для коннекта шлема с компом, требует фейсбук логина, вымораживает
😁1
Страшные люди пишут на сях лок-фри алгоритмы
Вот пишет он в статье, мол
Вот пишет он в статье, мол
Cache_Remap
нужна, чтобы пореже трогать одну кеш-линию. А смотришь в реализацию — там какая-то мрачная битовая магия, с полчаса медитируешь, чтоб понять, что это он младшие битики наверх утащил. Ща ещё столько же, походу, буду искать, куда этот кудесник флажок запрятал. А потом гадать, что за волшебные условия он расставил по коду, но не по псевдокоду в статье. Ъуъ👌2
У меня курс про лок-фри алгоритмы был на жаве/котле, потому для меня от имплементации пейперов на сях несёт какой-то хтонью
Автор в статье: так как мы хотим хранить значения в диапазоне [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