У меня есть «серверная» программа, которая обновляет многие связанные списки в общей памяти в ответ на внешние события. Я хочу, чтобы клиентские программы как можно быстрее заметили обновление в любом из списков (самая низкая задержка). Сервер помечает узел связанного списка state_
как FILLED
, как только его данные будут заполнены, а его следующий указатель будет установлен в допустимое местоположение. До этого момента его state_
составляет NOT_FILLED_YET
. Я использую барьеры памяти, чтобы убедиться, что клиенты не видят state_
как FILLED
до того, как данные внутри действительно готовы (и, похоже, они работают, я никогда не вижу поврежденных данных). Кроме того, state_
является volatile, чтобы убедиться, что компилятор не поднимает проверку клиента из циклов.Из этих 3 методов чтения связанных списков из разделяемой памяти, почему 3-й самый быстрый?
Сохраняя код сервера точно так же, я придумал 3 разных метода для проверки клиентом связанных списков для изменений. Вопрос в следующем: почему 3-й метод наиболее быстрый?
Метод 1: Круговой над всеми связанными списками (так называемые «каналы») непрерывно, глядя, чтобы увидеть, если какие-либо узлы заменить на «FILLED»:
void method_one()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
Data* current_item = channel_cursors[i];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[i] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
Метод 1 дал очень низкую латентность, когда то количество каналов было небольшим. Но когда число каналов выросло (250K +), оно стало очень медленным из-за циклизации по всем каналам. Поэтому я пробовал ...
Способ 2. Дайте каждому связанному списку ID. Сохраните отдельный «список обновлений» сбоку. Каждый раз, когда обновляется один из связанных списков, введите его идентификатор в список обновлений. Теперь нам просто нужно следить за единственным списком обновлений и проверять идентификаторы, которые мы получаем от него.
void method_two()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ == NOT_FILLED_YET) {
continue;
}
::uint32_t update_id = update_cursor->list_id_;
Data* current_item = channel_cursors[update_id];
if(current_item->state_ == NOT_FILLED_YET) {
std::cerr << "This should never print." << std::endl; // it doesn't
continue;
}
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[update_id] = static_cast<Data*>(current_item->next_.get(segment));
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
}
Метод 2 дал TERRIBLE латентность. Принимая во внимание, что метод 1 может давать задержка 10us, метод 2 необъяснимо часто дает 8 мс латентности! Используя gettimeofday, кажется, что изменение в update_cursor-> state_ было очень медленным, чтобы протаиваться с представления сервера на клиентский (я нахожусь в многоядерном поле, поэтому я предполагаю, что задержка связана с кешем). Поэтому я попробовал гибридный подход ...
Способ 3: Сохраните список обновлений. Но циклически переходите по всем каналам непрерывно, и в каждой итерации проверьте, обновлен ли список обновлений. Если это так, пойдите с номером, нажатым на него. Если это не так, проверьте канал, в который мы сейчас итерализовались.
void method_three()
{
std::vector<Data*> channel_cursors;
for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
{
Data* current_item = static_cast<Data*>(i->get(segment)->tail_.get(segment));
channel_cursors.push_back(current_item);
}
UpdateID* update_cursor = static_cast<UpdateID*>(update_channel.tail_.get(segment));
while(true)
{
for(std::size_t i = 0; i < channel_list.size(); ++i)
{
std::size_t idx = i;
ACQUIRE_MEMORY_BARRIER;
if(update_cursor->state_ != NOT_FILLED_YET) {
//std::cerr << "Found via update" << std::endl;
i--;
idx = update_cursor->list_id_;
update_cursor = static_cast<UpdateID*>(update_cursor->next_.get(segment));
}
Data* current_item = channel_cursors[idx];
ACQUIRE_MEMORY_BARRIER;
if(current_item->state_ == NOT_FILLED_YET) {
continue;
}
found_an_update = true;
log_latency(current_item->tv_sec_, current_item->tv_usec_);
channel_cursors[idx] = static_cast<Data*>(current_item->next_.get(segment));
}
}
}
Задержка этого метода не хуже метода 1, но масштабируется до большого количества каналов. Проблема в том, что я не знаю, почему. Просто бросить гаечный ключ в вещах: если я раскомментирую часть «найденный через обновление», он печатает между КАЖДОЙ СООБЩЕНИЕМ LATENCY LOG. Это означает, что вещи всегда встречаются только в списке обновлений! Так что я не понимаю, как этот метод может быть быстрее, чем метод 2.
Полный, компилируемый код (требуется GCC и повышения-1,41), который генерирует случайные строки в качестве тестовых данных находится по адресу: http://pastebin.com/0kuzm3Uf
Update: Все 3 метода эффективно спин-блокировки до тех пор, пока не произойдет обновление. Разница заключается в том, сколько времени им требуется, чтобы заметить, что обновление произошло. Они все непрерывно уплачивают процессор, так что не объясняют разницу в скорости. Я тестирую на 4-ядерном компьютере, где ничего больше не работает, поэтому серверу и клиенту нечего конкурировать. Я даже сделал версию кода, в которой обновления сигнализируют о состоянии и ждут клиентов при условии - это не помогло латентности любого из методов.
Update2: Несмотря на то, что существует 3 метода, я пробовал только 1 за раз, поэтому только 1 сервер и 1 клиент конкурируют за элемент state_.
Да, это для того, чтобы избежать условий гонки. Он также должен был быть в методе 2, я отредактировал свой пост, чтобы исправить это (тайминги одинаковы). Современные процессоры (а не только компилятор) могут свободно переупорядочивать магазины и загружать потоки, поэтому один поток может установить A тогда B, но другой поток может «видеть», что B был установлен до того, как он увидит, что A был установлен. Сервер заполняет данные узла, делает RELEASE_MEMORY_BARRIER, а затем устанавливает state_ в FILLED. В сочетании с клиентом, выполняющим ACQUIRE_MEMORY_BARRIER, это гарантирует, что клиент никогда не воспринимает state_ как ЗАПОЛНЕН, прежде чем данные узла будут заполнены. –
Я должен добавить, что это мой первый раз, используя барьеры памяти, и вот как я * думаю * они предполагаются работать;) Если вы следуете ссылке pastebin, первый файл, list.H определяет RELEASE_MEMORY_BARRIER и ACQUIRE_MEMORY_BARRIER для примитивов, задокументированных здесь: http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins .html –
Извините, спам; p Я отредактировал свое сообщение (см. внизу), чтобы обратиться к удару процессора. –