55 votes

Utilisation De Boost.Lockfree file d'attente est plus lent que d'utiliser les mutex

Jusqu'à maintenant j'utilisais std::queue dans mon projet. J'ai mesuré le temps moyen de laquelle une opération spécifique sur cette file d'attente exige.

Les temps ont été mesurés sur 2 machines: Mon local de machine virtuelle Ubuntu et un serveur distant. À l'aide de std::queue, la moyenne était à peu près la même sur les deux machines: ~750 microsecondes.

Ensuite, j'ai "upgradé" l' std::queue de boost::lockfree::spsc_queue, j'ai donc pu se débarrasser de l'mutex la protection de la file d'attente. Sur mon local VM j'ai pu voir un énorme gain de performance, la moyenne est maintenant de 200 microsecondes. Sur la machine distante cependant, la moyenne est allé jusqu'à 800 microsecondes, ce qui est plus lent qu'il ne l'était avant.

J'ai d'abord pensé que cela pourrait être parce que la machine distante ne pouvait pas soutenir les sans verrouillage de la mise en œuvre:

De le Stimuler.Lockfree page:

Pas tout le matériel prend en charge le même ensemble d'instructions atomiques. Si elle n'est pas disponible dans le matériel, il peut être émulé dans le logiciel à l'aide de gardes. Cependant, cela a l'inconvénient évident de perdre le lock-bien gratuit.

Pour savoir si ces instructions sont pris en charge, boost::lockfree::queue a une méthode appelée bool is_lock_free(void) const;. Toutefois, boost::lockfree::spsc_queue n'a pas une fonction de ce genre, qui, pour moi, cela implique qu'elle ne repose pas sur le matériel et qui est toujours lockfree - sur n'importe quelle machine.

Quelle pourrait être la raison pour la perte de performance?


Exemple de code (Producteur/Consommateur)

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

128voto

David Schwartz Points 70129

Lock gratuit algorithmes généralement effectuer plus de mal que de verrouillage basé sur des algorithmes. C'est l'une des principales raisons qu'ils ne sont pas utilisés aussi fréquemment.

Le problème avec la serrure de libre algorithmes est qu'ils maximisent la contention en permettant prétendant fils de continuer à composer. Serrures d'éviter les querelles de de-la planification en soutenant des threads. Lock gratuit algorithmes, pour une première approximation, doit être utilisé uniquement lorsqu'il n'est pas possible de programmer en soutenant des threads. Que rarement s'applique à niveau de l'application du code.

Permettez-moi de vous donner un extrême hypothétique. Imaginez quatre threads en cours d'exécution sur un typique, moderne PROCESSEUR dual-core. Fils A1 et A2 de la manipulation de la collection A. les Threads B1 et B2 de la manipulation de la collection B.

Tout d'abord, imaginons la collection utilise des verrous. Cela signifie que si les threads A1 et A2 (ou B1 et B2) essayez d'exécuter en même temps, l'un d'eux sera obstrué par la serrure. Donc, très rapidement, un fil et un B thread en cours d'exécution. Ces threads d'exécuter très rapidement et ne sera pas face. De tout temps les threads tentez de résoudre le conflit fil obtiendrez de l'horaire. Yay.

Maintenant, imaginez la collection utilise pas de verrou. Maintenant, les threads A1 et A2 peuvent s'exécuter en même temps. Ce sera la cause de l'affirmation constante. Les lignes de Cache pour la collection de ping-pong entre les deux noyaux. Inter-core bus peuvent saturé. La Performance va être terrible.

Encore une fois, c'est très exagéré. Mais vous obtenez l'idée. Vous voulez éviter les problèmes de contention, de ne pas souffrir autant que possible.

Cependant, maintenant exécuter cette expérience de pensée, où A1 et A2 sont les seuls fils sur l'ensemble du système. Maintenant, le verrouillage de la collecte gratuite est probablement mieux (même si vous trouvez que c'est mieux d'avoir juste un thread dans ce cas!).

Presque chaque programmeur passe par une phase où ils pensent que les verrous sont mauvais et en évitant les verrous rend le code plus rapide. Finalement, ils se rendent compte que c'est de contention qui rend les choses lentes et les serrures, utilisé correctement, de minimiser les conflits.

0voto

Kemin Zhou Points 31

Je ne peux pas dire que le coup de pouce lockfree de la file d'attente est plus lent dans tous les cas possibles. Dans mon expérience, la poussée(const T& item) est d'essayer d'en faire une copie. Si vous êtes en train de construire tmp objets et en poussant sur la file d'attente, puis, vous êtes frappé par une performance de glisser. Je pense que la bibliothèque juste besoin de la version surchargée push(T&& item) pour faire objet mobile plus efficace. Avant l'ajout de la nouvelle fonction, vous pourriez avoir à utiliser des pointeurs, la plaine de type, ou les plus intelligents offerts après C++11. C'est plutôt l'aspect limité de la file d'attente, et je n'utilise que le lockfree file d'attente varient rarement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X