Как стать автором
Обновить

Оптимизация кольцевого буфера для повышения пропускной способности

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров7.1K
Всего голосов 44: ↑43 и ↓1+59
Комментарии25

Комментарии 25

А если использовать readIdx_.load(std::memory_order_relaxed) вместо readIdxCached_ , это будет не то же самое? В чём разница?

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

На x86 да, а вот, например, на arm упс получится

Почему это?

Небольшая выдержка про порядок Release-Acquire.

Если atomic store в потоке A помечен как memory_order_release, atomic load в потоке B из той же переменной помечен как memory_order_acquire, а load в потоке B считывает значение, записанное store в потоке A, то store в потоке A synchronizes-with (синхронизируется с) load в ​​потоке B.

Все записи в память (включая неатомарные и атомарные с memory_order_relaxed), которые happened‑before (произошли до) atomic store с точки зрения потока A, становятся видимыми побочными эффектами в потоке B. То есть, как только atomic load завершен, поток B гарантированно увидит все, что поток A записал в память. Это обещание выполняется только в том случае, если B фактически получает значение, которое A сохранил (или значение из более поздней последовательности записи с memory_order_release).

Таким образом не достаточно установить только memory_order_release при записи в атомик, нужно еще и прочитать из него с memory_order_acquire.

Искусство писать простые вещи сложным языком... Написали бы просто: чтения после acquire гарантированно увидят все записи до release, без барьеров на каждом из потоков порядок видимости случаен. Подробную доку нашёл тут: https://lwn.net/Articles/576489/

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

При чтении из readIdx_ придется вытеснить в память значение из кеш линии ядра, в котором последний раз проводилась модификация, попутно сбросив состояние кеш линии в shared.

А при записи в readIdx_ придется инвалидировать все кеш линиию в ядре, которое последним читало из этой памяти.

В случае же с readIdxCached_ никаких дополнительных синхронизаций не потребуется (кроме первоначального чтения), так как эта память будет присутствовать только в кеше одного ядра.

С протоколом MESI можно поиграться здесь https://www.scss.tcd.ie/Jeremy.Jones/vivio/caches/MESIHelp.htm

"Разумеется, это также означает, что, если вам приходится оперировать сравнительно крупными элементами, то вы особенно не выиграете от оптимизации кольцевого буфера"

  • на этом можно было бы и закончить с оптимизацией, имхо.

Передаём через буфер не сам объект, а исключительную ссылку на него, и всё.

И зачем нужно, чтобы ссылки кешировались? Это будет влиять на производительность? Просто я видел использование кольцевых буферов в драйверах Ethernet и использовал из в драйверах АЦП, везде стат аллокация памяти и DMA. С boost не работал. Поэтому и не очень понимаю о чём речь, хотя идея и понятна

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

Эти кейсы из библиотек биржевого трейдига - цены гонять в busy loop с минимальной задержкой. Тут выигрыш может быть даже от того, что кэш-линия последовательно пишется.
А для выравнивая лучше брать двойной размер (2x64=128) префетчер все равно две за раз прочтет при запросе (текущую, и следующую за ней), и соседнюю - лучше не "марать".

Понял, спасибо, плюсы не ставятся)

Можно ли использовать кольцевые буферы для организации rate_limiter и мониторинга производительности? Есть ли быстрые реализации распределённого кольцевого буфера?

Что такое распределённый кольцевой буфер?

Единый буфер на несколько нодов/подов/узлов/машин

Это противоречит смыслу такого буфера, как места, где читатель и писатель не мешают друг другу. Но можно из нескольких таких буферов сделать коммуникацию многие ко многим. Например: https://github.com/nin-jin/go.d

Потестил, кстати, на i5-8300H CPU @ 2.30GHz, переливание и сложение 160 миллионов интов занимает как раз секунду.

Код
import std.datetime.stopwatch;
import std.range;
import std.stdio;
import std.algorithm;

import jin.go;

enum int iterations = 160_000_000;

static auto produce()
{
	return iterations.iota;
}

void main()
{
	auto timer = StopWatch(AutoStart.yes);

	int sum = go!produce.fold!q{a+b};

	timer.stop();

	writeln("Result\t\tTime");
	writeln(sum, "\t", timer.peek.total!"msecs", " ms");

}

Кольцевой буфер также называется очередью «один производитель — один потребитель» (SPSC). В ней не бывает ожидания (и, соответственно, не бывает блокировок)

Разве переполнение буфера не вызовет ожидание на запись и соответсвующую блокировку?

На практике такие ситуации не редкость, когда темп записи данных больше темпа их чтения.

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

У меня давний вопрос по поводу кольцевого буфера.

Я правильно понимаю, что записывать может один поток, а читать другой поток, при этом они могут выполняться на разных ядрах и это не создаст гонки (при условии атомарности операций с указателями чтения и записи)?

И еще, не будет ли гонки, когда мы опустошим буфер или наоборот заполним (т.е. когда указатель записи упрется в указатель чтения и наоборот)?

Да, это исключительно для ситуации с одним потоком-читателем и одним потоком-писателем. Когда поток упирается в предел, происходит возврат из функции с флагом false, так что гонке тут взяться неоткуда.

А не будет гонки по причине одновременного доступа к указателям? Или т.к. каждый поток записывает только свой указатель, а другой его исключительно только читает, то тут все нормально?

Я больше с baremetal имею дело. И мне интересно, выходит, я могу условно в обработчике прерывания писать в кольцевой буфер, а в основном цикле читать из него и критические секции тут не нужны получается?

Сами атомики безопасны, их можно читать и писать из разных потоков без каких либо гонок. Единственное, что приходится дополнительно защищать - это память, где будут лежать передаваемые данные. При этом сами указатели вектора - указатели начала данных, конца данных и конца выделенной памяти - только читаются, поэтому тут гонок нет).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий