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

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

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

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

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

С импульс.Страница Lockfree:

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

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

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


код 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;
}

1 ответов


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

проблема с алгоритмами без блокировки заключается в том, что они максимизируют конкуренцию, позволяя соперничающим потокам продолжать бороться. Блокировки избегают конфликтов, отменяя планирование конфликтующих потоков. Алгоритмы Lock free, в первом приближении, должны использоваться только тогда, когда невозможно отменить расписание конкурирующих потоков. Что только редко применяется к коду уровня приложения.

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

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

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

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

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

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