Как ускорить кольцевой буфер в 20 раз, просто запомнив чужой индекс
Днём я писал про кольцевой буфер в Еве и нашёл в нём проблему. Вот статья, которая объясняет, как сделать такой буфер по-настоящему быстрым.
Базовая очередь
Очередь «один пишет, один читает» (SPSC) на кольцевом буфере выглядит так:
- писатель кладёт элемент в
buffer[write_idx]и публикует новыйwrite_idx; - читатель читает
buffer[read_idx]и публикует новыйread_idx; - чтобы понять, есть ли место, писатель читает
read_idx; чтобы понять, есть ли данные, читатель читаетwrite_idx.
Публикация идёт через атомарную запись с семантикой release, чтение — с acquire. Блокировок нет. Индексы, как положено, разнесены по разным кеш-линиям, чтобы не было false sharing.
Где всё равно медленно
Проблема в пункте 3: на каждой операции каждая сторона читает индекс другой стороны. Писатель только что изменил write_idx, кеш-линия у него в эксклюзивном состоянии. Читатель хочет её прочитать, и линия переходит в разделяемое состояние. Писатель снова пишет, и линию снова надо забирать. В среднем около трёх промахов кеша на операцию.
Решение: кешировать чужой индекс
Каждая сторона хранит у себя локальную копию индекса другой стороны и обновляет её, только когда это действительно нужно:
- писатель смотрит на свою копию
read_idx. Пока по ней есть место, он пишет, не трогая настоящийread_idx. Только если копия говорит «полно», он перечитывает настоящий индекс; - читатель точно так же держит копию
write_idx.
Если читатель увидел, что доступно N элементов, следующие N−1 чтений ему вообще не нужно трогать индекс писателя.
Результаты
На тесте Ригторпа:
- до: около 5,5 млн операций в секунду и ~350 млн промахов кеша;
- после: около 112 млн операций в секунду и ~15 млн промахов.
Примерно в 20 раз быстрее, и это быстрее, чем очереди из Boost и Folly. Дальше можно выиграть ещё на пакетных операциях и huge pages, но это уже тонкая настройка под конкретное приложение.
Мой комментарий
Для Евы план исправления теперь в двух шагах:
- Развести
headиtailпо разным кеш-линиям. Сейчас они соседи, и это прямой false sharing. - Добавить кешированные копии чужих индексов. Сделать это просто: копия живёт в локальной памяти процесса, а не в общем сегменте, потому что она нужна только своей стороне.
Красота этого трюка в том, что он не меняет протокол. Раскладка общей памяти остаётся той же, меняется только то, как часто каждая сторона в неё заглядывает.