7
votes

2 fils plus lents que 1?

Je jouais avec std :: thread code> et quelque chose bizarre surgivé: xxx pré>

Lors de la compilation du code ci-dessus sans optimisations à l'aide de Clang ++, j'ai eu le Suivre les points de repère: p> xxx pré>

J'ai ensuite changé mon code ci-dessous: (Utilisation de seulement 1 thread) p>

real 0m2.304s
user 0m2.298s
sys  0m0.003s


5 commentaires

Tous les threads font sont des têtes de bashing essayant de lire et d'écrire à k. Et quand la première finition, il doit encore attendre la seconde.


@chris: k n'est pas volatile , de sorte que les threads ne sont pas en compétition, car ils ne partagent pas efficacement k .


@MOOINGDUCK: volatile assure les écritures à la mémoire, mais pas d'avoir volatile ne les empêche pas. La question indique spécifiquement "... sans optimisations ..." et il est typique des constructions non optimisées de suivre les instructions du programme très littéralement et non de "cache" des valeurs dans des registres. Sur le matériel Intel / AMD récent, il y a un rinçage automatique entre les caches de noyau accédant à la même adresse mémoire qui ralentiserait ceci.


@Tontyd: C'est un bon point sur les bâtiments de débogage ...


Le compilateur ne serait probablement pas en cache une variable globale même si vous avez laissé de côté volatile. Ce qui se passe consiste à écrire de vieilles valeurs qui déplace le compteur de 1 à 100000 surtout si un fil a été préempté après avoir lu K mais avant d'écrire K + 1.


4 Réponses :


16
votes

Vous avez deux threads qui se battent sur la même variable, k . Donc, vous passez du temps où les processeurs disent «Processeur 1: Hé, savez-vous quelle valeur k a? Processeur 2: Bien sûr, vous y allez! . Étant donné que k n'est pas atomique, il n'y a pas non plus de garantie que Thread2 n'écrire pas une "vieille" valeur de k de sorte que le prochain thread thread 1 lit la valeur, elle saute Retour 1, 2, 10 ou 100 étapes, et doit le refaire - en théorie qui ne pouvait conduire à aucune des boucles toutes les finitions, mais cela nécessiterait un peu de malchance.


7 commentaires

C'est logique. Merci.


@ Magtheridon96: En plus de ce que dit les tapis, votre programme a une course de données et ainsi un comportement formellement indéfini. n'importe quoi pourrait arriver parce que vous accédez à une variable non atomique partagée de deux threads sans synchronisation. Le modèle de mémoire des processeurs X86 est quelque peu indicatif mais vous ne devriez vraiment pas compter sur cela.


@Johannesd Au début, je l'essais avec un std :: atomic , et j'ai reçu des résultats similaires, alors j'ai essayé avec un int . Les deux tests étaient plus rapides, mais les ratios entre les temps étaient à peu près les mêmes. Merci les gars. Vous m'avez montré l'importance d'avoir des unités de stockage atomique.


@ Magtheridon96: Il suffit de faire k un std :: atomic empêcherait la course de données, mais cela ne rendrait pas le problème de ping-pong disparaître car l'expression K = k + 1 ne serait toujours pas atomique. Cependant, k ++ ou k + = 1 serait. Mais la synchronisation entre les processeurs appliqués par les opérations atomiques, même si le verrouillage entraînerait toujours une surcharge - ce n'est tout simplement pas un cas d'utilisation pouvant être accéléré à l'aide de threads. Pensez-y comme essayant d'écrire une histoire avec votre ami - ce ne serait pas plus rapide que de le faire seul parce que vous devriez faire demi-tour.


@ Magtheridon96: std :: atomique ne permettra pas que les données soient partagées entre les deux threads - en fait, cela en fait un peu pisponible dans le sens où chaque écrit sur une force atomique Le processeur qui n'est pas à l'intérieur de la partie "atomique" pour "abandonner" sa copie de cette valeur et attendre que "Atomic" se termine avant de pouvoir chercher la nouvelle valeur. C'est pourquoi le partage de nombreuses données entre les threads est généralement plus lent que de fonctionner dans un seul fil.


@Johannesd explique également pourquoi j'ai eu une belle vitesse de boost lorsque j'ai changé k = k + 1 sur k ++ . (D'environ 1 minute 11 secondes à 35 secondes après avoir changé le code pour utiliser un std :: atomic )


@Johannesd +1 pour l'analogie de l'histoire.



4
votes

Cela devrait vraiment être un commentaire en réponse à la réponse de Matts Peterson, mais je voulais fournir des exemples de code.

Le problème est la conflit d'une ressource spécifique, ainsi qu'une Cacheline. P>

Alternative 1: P>

#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>

static const uint64_t ITERATIONS = 10000000000ULL;
#define CACHE_LINE_SIZE 128

int main(int argc, const char** argv)
{
    size_t numThreads = 1;
    if (argc > 1) {
        numThreads = strtoul(argv[1], NULL, 10);
        if (numThreads == 0)
            return -1;
    }

    std::vector<std::thread> threads;
    std::mutex kMutex;
    uint64_t k = 0;

    for (size_t t = 0; t < numThreads; ++t) {
       threads.emplace_back([=, &k]() {
           alignas(CACHE_LINE_SIZE) uint64_t myK = 0;
           // Imperfect division of labor, we'll fall short in some cases.
           for (uint64_t i = 0; i < ITERATIONS / numThreads; ++i) {
               myK++;
           }
           kMutex.lock();
           k += myK;
           kMutex.unlock();
       });
    }

    for (size_t t = 0; t < numThreads; ++t) {
        threads[t].join();
    }
    return 0;
}


3 commentaires

Merci d'avoir pris le temps de faire tout ça :). J'aime la 3ème variation.


Essayez le 3ème avec un objet qui est Alignas (128) (ou quelle que soit la taille de votre ligne de cache) - L'objectif étant de déplacer l'espace de travail du fil à un emplacement qui n'a pas aucun chevauchement avec quoi D'autres processeurs travaillent sur.


Enfin, sur un ordinateur au lieu d'un téléphone, corrigez les erreurs du compilateur.



2
votes

me semble être la question plus importante que "Pourquoi ce travail n'a-t-il pas fait?" est "Comment puis-je obtenir cela pour travailler?" Pour la tâche à la main, je pense que std :: async code> (malgré Les lacunes significatives ) sont vraiment un meilleur outil que d'utiliser std :: thread code> directement.

#include <future>
#include <iostream>

int k = 0;
unsigned tasks = std::thread::hardware_concurrency();
unsigned reps = 1000000000 / tasks;

int main() {
    std::vector<std::future<int>> f;

    for (int i=0; i<tasks; i++)
        f.emplace_back(std::async(std::launch::async, 
                                  [](){int j; for (j=0; j<reps; j++); return j;})
                      );

    for (int i=0; i<tasks; i++) {
        f[i].wait();
        k += f[i].get();
    }

    std::cout << k << "\n";
    return 0;
}


0 commentaires

0
votes

Je rencontre ce problème. Mon opinion est que, pour certains types de travail, le coût de la gestion du thread peut être plus que le bénéfice que vous obtenez de l'exécution de threads. Voici mon exemple de code, faisant du vrai travail dans une boucle, un grand nombre d'itérations, j'ai donc eu un numéro très cohérent avec la commande horaire.

       Thead    No_thread
=========================
real   4m24        2m44s
usr    0m54s       2m44s
sys    0m6.2s      0m0.016s


0 commentaires