Затраты на атомарное и std :: mutex убивают мою производительность для многопоточного кодирования RLNC - PullRequest
0 голосов
/ 09 мая 2018

Я пытаюсь использовать многопоточность для кодирования с использованием случайного линейного сетевого кодирования (RLNC) для повышения производительности. Однако у меня есть проблема с производительностью, мое многопоточное решение работает медленнее, намного медленнее, чем текущая версия без многопоточности. У меня есть приостановка, что это atomic доступ на m_completed и std :: mutex для вставки элементов в m_results, которые убивают мою производительность. Однако я не знаю, как это подтвердить.

Итак, немного больше информации: функция completed() вызывается в while -цикле в главном потоке while(!encoder.completed()){}, что приводит к чертовому множеству атомарного доступа, но я не могу найти правильный способ сделать это без атомной или мьютексной блокировки. Вы можете найти код ниже.

Так что, пожалуйста, если кто-то может увидеть ошибку или направить меня к лучшему способу сделать это, я буду очень благодарен. Я потратил 1,5 недели на то, чтобы выяснить, в чем дело сейчас, и моя единственная идея - atomic или std::mutex замки

#include <cstdint>
#include <vector>
#include <mutex>

#include <memory>
#include <atomic>
...
namespace master_thesis
{
namespace encoder
{
class smart_encoder
{
...   
    void start()
    {
    ... 
    // Incase there are more an uneven amount 
    // of symbols we adjust this abov

            else
            {

                m_pool.enqueue([this, encoder](){
                        std::vector<std::vector<uint8_t>> total_payload(this->m_coefficients,
                                                                        std::vector<uint8_t>(encoder->payload_size()));

                        std::vector<uint8_t> payload(encoder->payload_size());

                        for (uint32_t j = 0; j < this->m_coefficients; ++j)
                        {
                            encoder->write_payload(payload.data());
                            total_payload[j] = payload; //.insert(total_payload.begin() + j, payload);
                        }





                        this->m_mutex.lock();

                        this->m_result.insert(std::end(this->m_result),
                                              std::begin(total_payload),
                                              std::end(total_payload));
                        ++(this->m_completed);

                        this->m_mutex.unlock();
                    });
            }

        }
    }

    bool completed()
    {
        return m_completed.load() >= (m_threads - 1);
    }

    std::vector<std::vector<uint8_t>> result()
    {
        return m_result;
    }

private:
    uint32_t m_symbols;
    uint32_t m_symbol_size;
    std::atomic<uint32_t> m_completed;
    unsigned int m_threads;
    uint32_t m_coefficients;

    std::mutex m_mutex;

    std::vector<uint8_t> m_data;
    std::vector<std::vector<uint8_t>> m_result;

    ThreadPool m_pool;

    std::vector<std::shared_ptr<rlnc_encoder>> m_encoders;
};
}
}

1 Ответ

0 голосов
/ 12 мая 2018

Узким местом, вероятно, является не вызов Completed().

На x86 чтение из выровненного по слову uint32_t автоматически является атомарной операцией, std::atomic или нет. Единственное, что std::atomic делает для uint32_t на x86, это гарантирует, что оно выровнено по словам, и что компилятор не меняет его порядок и не оптимизирует его.

Нагрузка с ограниченным контуром не является причиной конфликта шины. При первом чтении будет отсутствовать кеш, но последующие загрузки будут попадать в кеш до тех пор, пока кеш не станет недействительным из-за записи в адрес из другого потока. Есть предостережение - случайное разделение строк кэша («ложное разделение»). Одна идея о том, как вы можете исключить эту возможность, переключившись на массив с 60 байтами неиспользованного дополнения с обеих сторон вашего атома (используйте только средний). std::atomic<uint32_t> m_buffered[31]; std::atomic<uint_32t>& m_completed = m_buffered[15];

Имейте в виду, что тесная петля свяжет одно из ваших ядер, ничего не делая, кроме как просматривая его кеш. Это пустая трата денег ...;) Это вполне может быть причиной вашей проблемы. Вы должны изменить свой код так:

int m_completed = 0;  // no longer atomic
std::condition_variable cv;

// in main...(pseudocode)
lock (unique) m_mutex  // the m_mutex from the class
    while !Completed()
        cv.wait(m_mutex)

// in thread (pseudocode)
bool toSignal = false;
lock guard m_mutex
    this->m_result.insert(std::end(this->m_result),
                          std::begin(total_payload),
                          std::end(total_payload));
    ++m_completed;
    toSignal = Completed();
if toSignal
    cv.signalOne()

Также возможно, что потеря производительности связана с критической секцией мьютекса. Этот критический раздел потенциально может быть на много порядков длиннее, чем пропадание кэша. Я бы порекомендовал сравнить время для 1 потока, 2 потоков и 4 потоков в пуле потоков. если 2 потока не быстрее, чем 1 поток, то ваш код по существу работает последовательно.

Как измерить? Инструменты профилирования хороши, когда вы не знаете, что оптимизировать. У меня нет большого опыта работы с ними, но я знаю, что (по крайней мере, некоторые из старых) могут стать немного отрывочными, когда дело доходит до многопоточности. Вы также можете использовать старый добрый таймер. C ++ 11 имеет high_resolution_clock, который, вероятно, имеет разрешение в 1 микросекунды, если у вас приличное оборудование.

Наконец, я вижу много возможностей для алгоритмической / скалярной оптимизации. Предварительно выделяйте векторы вместо того, чтобы делать это каждый раз. Используйте указатели или std :: move, чтобы избежать ненужных глубоких копий. Предварительно распределите m_result и сделайте так, чтобы потоки записывали в определенные смещения индекса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...