Из этих 3 методов для чтения связанных списков от общей памяти, почему 3-е является самым быстрым?

У меня есть программа 'сервера', которая обновляет много связанных списков в общей памяти в ответ на внешние события. Я хочу, чтобы клиентские программы заметили обновление в любом из списков как можно быстрее (самая низкая задержка). Сервер отмечает узел связанного списка state_ как FILLED после того как его данные заполнены в, и его прямой указатель был установлен на допустимое местоположение. До тех пор, state_ NOT_FILLED_YET. Я использую барьеры памяти, чтобы удостовериться, что клиенты не видят state_ как FILLED прежде чем данные в на самом деле готовы (и это, кажется, работает, я никогда не вижу поврежденные данные). Кроме того, state_ энергозависимо, чтобы быть уверенным, что компилятор не снимает проверку клиента его из циклов.

Сохраняя серверный код точно тем же, я придумал 3 различных метода для клиента для сканирования связанных списков для изменений. Вопрос: Почему 3-й метод является самым быстрым?

Метод 1: Циклический алгоритм по всем связанным спискам (названный 'каналами') непрерывно, надеясь видеть, изменились ли какие-либо узлы на 'ЗАПОЛНЕННЫЙ':

void method_one()
{
    std::vector channel_cursors;
    for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
    {
        Data* current_item = static_cast(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(current_item->next_.get(segment));
        }
    }
}

Метод 1 дал очень низкую задержку, когда затем количество каналов было небольшим. Но когда количество каналов выросло (250K +), это стало очень медленным из-за цикличного выполнения по всем каналам. Таким образом, я попробовал...

Метод 2: Дайте каждому связанному списку идентификатор. Сохраните отдельный 'список обновления' стороне. Каждый раз один из связанных списков обновляется, продвиньте его идентификатор в списке обновления. Теперь мы просто должны контролировать единственный список обновления и проверить идентификаторы, которые мы получаем от него.

void method_two()
{
    std::vector channel_cursors;
    for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
    {
        Data* current_item = static_cast(i->get(segment)->tail_.get(segment));
        channel_cursors.push_back(current_item);
    }

    UpdateID* update_cursor = static_cast(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(current_item->next_.get(segment));
        update_cursor = static_cast(update_cursor->next_.get(segment));
    }   
}

Метод 2 дал УЖАСНУЮ задержку. Принимая во внимание, что Метод 1 мог бы дать под 10us задержка, Метод 2 будет необъяснимо часто даваемая задержка на 8 мс! Используя gettimeofday кажется, что изменение в update_cursor-> state_ очень не спешило propogate от представления сервера до клиента (я нахожусь на многоядерном поле, таким образом, я предполагаю, что задержка происходит из-за кэша). Таким образом, я попробовал гибридный подход...

Метод 3: Сохраните список обновления. Но цикл по всем каналам непрерывно, и в рамках каждого повторения проверяет, обновил ли список обновления. Если это имеет, пойдите с числом, продвинутым на него. Если это не имеет, проверьте канал, к которому мы в настоящее время выполняли итерации.

void method_three()
{
    std::vector channel_cursors;
    for(ChannelList::iterator i = channel_list.begin(); i != channel_list.end(); ++i)
    {
        Data* current_item = static_cast(i->get(segment)->tail_.get(segment));
        channel_cursors.push_back(current_item);
    }

    UpdateID* update_cursor = static_cast(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(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(current_item->next_.get(segment));
        }
    }
}

Задержка этого метода была так же хороша как Метод 1, но масштабировалась к большим количествам каналов. Проблема, у меня нет подсказки почему. Только бросить ключ в вещи: если я некомментирую 'найденный через обновление' часть, оно печатает между КАЖДЫМ СООБЩЕНИЕМ ЖУРНАЛА ЗАДЕРЖКИ. Что означает, что вещи только когда-либо находятся в списке обновления! Таким образом, я не понимаю, как этот метод может быть быстрее, чем метод 2.

Полный, компилируемый код (требует GCC и повышения 1.41), который генерирует случайные строки как данные тестирования, в: http://pastebin.com/0kuzm3Uf

Обновление: Все 3 метода эффективно spinlocking, пока обновление не происходит. Различие находится в том, сколько времени оно берет их, чтобы заметить, что обновление произошло. Они все непрерывно облагают налогом процессор, так, чтобы не объяснял различие в скорости. Я тестирую на машине с 4 ядрами ни с чем иным выполнение, таким образом, у сервера и клиента нет ничего для конкуренции с. Я даже сделал версию кода, где обновления сигнализируют об условии и сделали, чтобы клиенты ожидали на условии - это не помогло задержке ни одного из методов.

Update2: Несмотря на то, чтобы там быть 3 методами, я только попробовал 1 за один раз, таким образом, только 1 сервер и 1 клиент конкурируют за state_ участника.

8
задан Joseph Garvin 28 March 2010 в 05:16
поделиться

4 ответа

Ответ было сложно понять, и, честно говоря, было бы сложно с информацией, которую я представил, хотя, если бы кто-то действительно скомпилировал исходный код, который я предоставил им ' У меня есть шанс на борьбу;) Я сказал, что «найдено через список обновлений» печатается после каждого сообщения журнала задержки, но на самом деле это было неправдой - это было правдой только до тех пор, пока я мог прокручивать назад в моем терминале. В самом начале было обнаружено множество обновлений без использования списка обновлений.

Проблема в том, что между моментом, когда я устанавливаю свою начальную точку в списке обновлений и моей начальной точкой в ​​каждом из списков данных, будет некоторое отставание, потому что эти операции требуют времени. Помните, списки постоянно растут. Рассмотрим простейший случай, когда у меня есть 2 списка данных, A и B. Когда я устанавливаю начальную точку в списке обновлений, в нем оказывается 60 элементов из-за 30 обновлений в списке A и 30 обновлений в списке B. чередовались:

A
B
A
B
A // and I start looking at the list here
B

Но затем, после того как я установил список обновлений там, есть множество обновлений для B и нет обновлений для A. Затем я устанавливаю свои стартовые позиции в каждом из списков данных. Моими отправными точками для списков данных будут после этого всплеска обновлений, но моя отправная точка в списке обновлений будет до этого всплеска, поэтому теперь я собираюсь проверить наличие нескольких обновлений без найти их.Вышеупомянутый смешанный подход работает лучше всего, потому что, перебирая все элементы, когда он не может найти обновления, он быстро закрывает временной промежуток между тем, где находится список обновлений, и где находятся списки данных.

0
ответ дан 6 December 2019 в 01:39
поделиться

Гипотеза: Метод 2 каким-то образом блокирует запись обновления сервером.

Помимо самих процессорных ядер, вы можете столкнуться с проблемой связного кэша. Когда вы читаете значение в данном ядре, кэш L1 на этом ядре должен получить доступ на чтение к этой строке кэша, что означает, что ему необходимо аннулировать доступ на запись к этой строке, который есть у любого другого кеша. И наоборот, чтобы написать значение. Это означает, что вы постоянно перемещаете строку кэша вперед и назад между состоянием «записи» (в кэше ядра сервера) и состоянием «чтение» (в кешах всех клиентских ядер).

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

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

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

1
ответ дан 6 December 2019 в 01:39
поделиться

Я заметил, что как в методе 1, так и в методе 3 у вас есть строка ACQUIRE_MEMORY_BARRIER , которая, как я полагаю, имеет какое-то отношение к условия многопоточности / гонки?

В любом случае, метод 2 не имеет никаких засыпаний, что означает, что следующий код ...

while(true)
{   
    if(update_cursor->state_ == NOT_FILLED_YET) {
        continue;
    }

забьет процессор. Типичный способ выполнить такого рода задачу производителя / потребителя - использовать какой-то семафор, чтобы сигнализировать читателю, что список обновлений изменился. Поиск многопоточности производителя / потребителя должен дать вам большое количество примеров. Основная идея здесь в том, что это позволяет потоку перейти в спящий режим, пока он ожидает изменения состояния update_cursor->. Это предотвращает кражу этим потоком всех циклов процессора.

0
ответ дан 6 December 2019 в 01:39
поделиться

Я не знаю, читали ли вы когда-нибудь столбцы «Параллелизм» от Херба Саттера. Они довольно интересны, особенно когда речь идет о проблемах с кешем.

Действительно, Method2 здесь кажется лучше, потому что значение id меньше, чем данные в целом, означало бы, что вам не нужно слишком часто обращаться к основной памяти (что обременительно).

Однако на самом деле может случиться так, что у вас есть такая строка кеша:

Line of cache = [ID1, ID2, ID3, ID4, ...]
                  ^         ^
            client          server

, которая затем создает конкуренцию.

Вот статья Херба Саттера: Устранение ложного обмена . Основная идея состоит в том, чтобы просто искусственно увеличить ваш идентификатор в списке, чтобы он полностью занимал одну строку кеша.

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

1
ответ дан 6 December 2019 в 01:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: