Подтвердить что ты не робот

Использование очереди Boost.Lockfree выполняется медленнее, чем использование мьютексов

До сих пор я использовал std::queue в своем проекте. Я измерил среднее время, которое требуется конкретной операции в этой очереди.

Время было измерено на 2 машинах: Моя локальная Ubuntu VM и удаленный сервер. Используя std::queue, среднее значение было почти одинаковым на обеих машинах: ~ 750 микросекунд.

Затем я "обновил" std::queue до boost::lockfree::spsc_queue, чтобы я мог избавиться от мьютексов, защищающих очередь. На моей локальной виртуальной машине я видел огромный прирост производительности, а средний показатель составляет 200 микросекунд. Однако на удаленной машине среднее значение достигало 800 микросекунд, что было медленнее, чем раньше.

Сначала я подумал, что это может быть связано с тем, что удаленная машина может не поддерживать реализацию без блокировки:

Из Страница Boost.Lockfree:

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

Чтобы узнать, поддерживаются ли эти инструкции, boost::lockfree::queue имеет метод bool is_lock_free(void) const;. Тем не менее, boost::lockfree::spsc_queue не имеет такой функции, что для меня подразумевает, что он не полагается на аппаратное обеспечение и всегда свободен - на любой машине.

Что может быть причиной потери производительности?


Код Exmple (Производитель/Потребитель)

// c++11 compiler and boost library required

#include <iostream>
#include <cstdlib>
#include <chrono>
#include <async>
#include <thread>
/* Using blocking queue:
 * #include <mutex>
 * #include <queue>
 */
#include <boost/lockfree/spsc_queue.hpp>


boost::lockfree::spsc_queue<int, boost::lockfree::capacity<1024>> queue;

/* Using blocking queue:
 * std::queue<int> queue;
 * std::mutex mutex;
 */

int main()
{
    auto producer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Producing data in a random interval
        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            // Push random int (0-9999)
            queue.push(std::rand() % 10000);

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for random duration (0-999 microseconds)
            std::this_thread::sleep_for(std::chrono::microseconds(rand() % 1000));
        }
    }

    auto consumer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Example operation on the queue.
        // Checks if 1234 was generated by the producer, returns if found.

        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            int value;
            while(queue.pop(value)
            {
                if(value == 1234)
                    return;
            }

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for 100 microseconds
            std::this_thread::sleep_for(std::chrono::microseconds(100));
        }
    }

    consumer.get();
    std::cout << "1234 was generated!" << std::endl;
    return 0;
}
4b9b3361

Ответ 1

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

Проблема с алгоритмами блокировки - это то, что они максимизируют конфликт, позволяя конкурирующим потокам продолжать бороться. Замки избегают конфликтов, деформируя конкурирующие потоки. Блокировка свободных алгоритмов, в первом приближении, должна использоваться только тогда, когда невозможно отменить запланированные потоки. Это редко применяется к коду уровня приложения.

Позвольте мне дать вам очень экстремальный гипотетический характер. Представьте, что четыре потока работают на типичном, современном двухъядерном процессоре. Нити A1 и A2 управляют коллекцией A. Нити B1 и B2 управляют коллекцией B.

Во-первых, предположим, что коллекция использует блокировки. Это будет означать, что если потоки A1 и A2 (или B1 и B2) будут работать одновременно, один из них будет заблокирован блокировкой. Итак, очень быстро, один поток A и один поток B будут работать. Эти потоки будут работать очень быстро и не будут бороться. Каждый раз, когда нить пытается бороться, конфликтный поток будет отменен. Yay.

Теперь представьте, что коллекция не использует блокировки. Теперь потоки A1 и A2 могут запускаться одновременно. Это вызовет постоянную конкуренцию. Кэш-линии для коллекции будут пинг-понг между двумя ядрами. Межъядерные шины могут насыщаться. Производительность будет ужасной.

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

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

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

Ответ 2

Я не могу сказать, что буст-очередь без блокировки медленнее во всех возможных случаях. По моему опыту, push (const T & item) пытается сделать копию. Если вы создаете объекты tmp и помещаете их в очередь, то вы сталкиваетесь с перетаскиванием производительности. Я думаю, что библиотека просто нуждается в перегруженной версии push (элемент T &&), чтобы сделать подвижный объект более эффективным. Перед добавлением новой функции вам, возможно, придется использовать указатели, обычный тип или умные, предложенные после С++ 11. Это довольно ограниченный аспект очереди, и я использую только очередь без блокировки, редко меняются.