Za Pomocą Boost.Kolejka Lockfree jest wolniejsza niż korzystanie z mutexów

Do tej pory używałem std::queue w swoim projekcie. Zmierzyłem średni czas, jakiego wymaga konkretna operacja w tej kolejce.

Czasy były mierzone na 2 maszynach: mojej lokalnej maszynie wirtualnej Ubuntu i zdalnym serwerze. Używając std::queue, średnia była prawie taka sama na obu maszynach: ~750 mikrosekund.

Potem "ulepszyłem" std::queue do boost::lockfree::spsc_queue, żeby pozbyć się muteksów chroniących kolejkę. Na moim lokalnym VM mogłem zobaczyć ogromny wzrost wydajności, średnia jest teraz na 200 mikrosekundy. Jednak na zdalnej maszynie średnia wzrosła do 800 mikrosekund, co jest wolniejsze niż wcześniej.

Najpierw pomyślałem, że może to być spowodowane tym, że zdalna maszyna może nie obsługiwać implementacji bez blokady:

Z doładowania.Strona Lockfree:

Nie wszystkie urządzenia obsługują ten sam zestaw instrukcji atomowych. Jeśli nie jest dostępny w sprzęcie, można go emulować w oprogramowaniu za pomocą strażników. Ma to jednak oczywistą wadę utrata własności bez blokady.

Aby dowiedzieć się, czy te instrukcje są obsługiwane, boost::lockfree::queue ma metodę o nazwie bool is_lock_free(void) const;. Jednak boost::lockfree::spsc_queue nie ma takiej funkcji, co dla mnie oznacza, że nie opiera się na sprzęcie, a to jest zawsze lockfree - na każdej maszynie.

Co może być przyczyną utraty wydajności?


Kod Exmple (Producent/konsument)

// 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;
}
Author: thesys, 2017-04-21

1 answers

Algorytmy bez blokady na ogół działają gorzej niż algorytmy oparte na blokadach. To kluczowy powód, dla którego nie są używane tak często.

Problem z algorytmami lock free polega na tym, że maksymalizują one argumentację, pozwalając wątkom kontestującym kontynuować spór. Blokada zapobiega konfliktom poprzez usuwanie harmonogramu wątków. Algorytmy Lock free, do pierwszego przybliżenia, powinny być używane tylko wtedy, gdy nie jest możliwe usunięcie harmonogramu wątków. Rzadko dotyczy to kod na poziomie aplikacji.

Pozwól, że przedstawię ci bardzo ekstremalną hipotezę. Wyobraź sobie, że cztery wątki działają na typowym, nowoczesnym dwurdzeniowym procesorze. Wątki A1 i A2 manipulują zbiorem A. wątki B1 i B2 manipulują zbiorem B.

Po pierwsze, wyobraźmy sobie, że kolekcja używa zamków. Oznacza to, że jeśli wątki A1 i A2 (lub B1 i B2) spróbują działać w tym samym czasie, jeden z nich zostanie zablokowany przez blokadę. Tak więc, bardzo szybko, jeden wątek A i jeden wątek B będzie uruchomiony. Wątki te będą działać bardzo szybko i nie będą walczyć. Za każdym razem, gdy wątki spróbują się przeciwstawić, wątek kolidujący zostanie anulowany. Yay.

Wyobraź sobie, że kolekcja nie używa żadnych zamków. Teraz wątki A1 i A2 mogą działać w tym samym czasie. Spowoduje to ciągłe spory. Linie pamięci podręcznej dla kolekcji będą znajdować się pomiędzy dwoma rdzeniami. Autobusy między-rdzeniowe mogą być nasycone. Wydajność będzie okropna.

Ponownie, to jest bardzo przesadzone. Ale masz pomysł. Chcesz unikaj sporów, nie cierp przez to jak najwięcej.

Jednak Teraz uruchom ten eksperyment myślowy ponownie, gdzie A1 i A2 są jedynymi wątkami w całym systemie. Teraz kolekcja lock free jest prawdopodobnie lepsza (choć może się okazać, że lepiej mieć tylko jeden wątek w tym przypadku!).

Prawie każdy programista przechodzi przez fazę, w której myśli, że blokady są złe, a unikanie blokad powoduje, że kod idzie szybciej. W końcu zdają sobie sprawę, że to jest spór to sprawia, że rzeczy powoli i blokuje, używane prawidłowo, zminimalizować niezgodność.

 76
Author: David Schwartz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-21 11:08:31