c++ multithreading performance boost lock-free

c++ - El uso de la cola de Boost.Lockfree es más lento que el uso de mutexes



multithreading performance (1)

Los algoritmos de bloqueo libre generalmente tienen un rendimiento más pobre que los algoritmos basados ​​en bloqueo. Esa es una razón clave por la que no se utilizan con tanta frecuencia.

El problema con los algoritmos de bloqueo libre es que maximizan la contención al permitir que los subprocesos contendientes continúen compitiendo. Los bloqueos evitan la discordancia al eliminar los hilos contendientes. Los algoritmos de bloqueo libre, en una primera aproximación, solo deben usarse cuando no es posible desfasar los hilos contendientes. Eso solo se aplica raramente al código de nivel de aplicación.

Déjame darte una hipotética muy extrema. Imagine que cuatro subprocesos se ejecutan en una típica y moderna CPU de doble núcleo. Los subprocesos A1 y A2 están manipulando la colección A. Los subprocesos B1 y B2 están manipulando la colección B.

Primero, imaginemos que la colección usa bloqueos. Eso significará que si los hilos A1 y A2 (o B1 y B2) intentan ejecutarse al mismo tiempo, uno de ellos será bloqueado por el bloqueo. Entonces, muy rápido, se ejecutará un hilo A y un hilo B. Estos hilos se ejecutarán muy rápido y no contendrán. Cada vez que los subprocesos intentan competir, el subproceso en conflicto se desprograma. Hurra.

Ahora, imagina que la colección no usa cerraduras. Ahora, los hilos A1 y A2 pueden ejecutarse al mismo tiempo. Esto causará contienda constante. Las líneas de caché para la colección serán ping-pong entre los dos núcleos. Los buses Inter-core pueden saturarse. El rendimiento será horrible.

De nuevo, esto es muy exagerado. Pero se entiende la idea. Desea evitar la contención, no sufrir tanto como sea posible.

Sin embargo, ahora ejecute este experimento mental de nuevo donde A1 y A2 son los únicos hilos en todo el sistema. Ahora, la colección gratuita de candados probablemente sea mejor (¡aunque es posible que sea mejor tener solo un hilo en ese caso!).

Casi todos los programadores atraviesan una fase en la que piensan que los bloqueos son malos y evitar los bloqueos hace que el código vaya más rápido. Eventualmente, se dan cuenta de que es una disputa que hace que las cosas vayan más despacio y se bloqueen, se usen correctamente y minimicen la contención.

Hasta ahora estaba usando std::queue en mi proyecto. Medí el tiempo promedio que requiere una operación específica en esta cola.

Los tiempos se midieron en 2 máquinas: mi Ubuntu VM local y un servidor remoto. Usando std::queue , el promedio fue casi el mismo en ambas máquinas: ~ 750 microsegundos.

Luego "actualicé" la std::queue para boost::lockfree::spsc_queue , para poder eliminar los mutexes que protegen la cola. En mi máquina virtual local pude ver una gran ganancia de rendimiento, el promedio ahora es de 200 microsegundos. Sin embargo, en la máquina remota, el promedio aumentó a 800 microsegundos, que es más lento de lo que era antes.

Primero pensé que esto podría deberse a que la máquina remota podría no ser compatible con la implementación sin bloqueo:

Desde la página Boost.Lockfree:

No todo el hardware admite el mismo conjunto de instrucciones atómicas. Si no está disponible en hardware, puede ser emulado en software usando protecciones. Sin embargo, esto tiene el inconveniente obvio de perder la propiedad sin bloqueo.

Para saber si estas instrucciones son compatibles, boost::lockfree::queue tiene un método llamado bool is_lock_free(void) const; . Sin embargo, boost::lockfree::spsc_queue no tiene una función como esta, lo cual, para mí, implica que no depende del hardware y que siempre está libre de bloqueo, en cualquier máquina.

¿Cuál podría ser el motivo de la pérdida de rendimiento?

Código de ejemplo (Productor / Consumidor)

// 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; }