Использование Boost.Очередь Lockfree медленнее, чем использование мьютексов
до сих пор я был с помощью std::queue
в моем проекте. Я измерил среднее время, которое требуется для конкретной операции в этой очереди.
время было измерено на 2 машинах: моя локальная виртуальная машина Ubuntu и удаленный сервер.
Используя std::queue
, среднее значение было почти одинаковым на обеих машинах: ~750 микросекунд.
затем я "обновил"std::queue
to boost::lockfree::spsc_queue
, чтобы я мог избавиться от мьютексов, защищающих очередь. На моей локальной виртуальной машине я мог видеть огромный прирост производительности, среднем сейчас на 200 микросекунд. На удаленной машине, однако, в среднем до 800 микросекунд, что медленнее, чем раньше.
сначала я подумал, что это может быть потому, что удаленная машина может не поддерживать реализацию без блокировки:
не все аппаратные средства поддерживают один и тот же набор атомарных инструкций. Если он недоступен в аппаратном обеспечении, его можно эмулировать с помощью программного обеспечения гвардеец. Однако это имеет очевидный недостаток потери свойства без блокировки.
чтобы узнать, поддерживаются ли эти инструкции,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, вероятно, лучше (хотя вы можете обнаружить, что в этом случае лучше иметь только один поток!).
почти каждый программист проходит через фазу, когда они думают, что замки плохие и избегая блокировок делает код быстрее. В итоге, они понимают, что это утверждение это делает вещи медленными и замки, используемые правильно, минимизируют разногласия.