2016-08-26 2 views
4

Je suis en train d'exécuter un simple programme de test sur un ordinateur Windows (compilé avec MSVS2015) et un serveur exécutant Solaris 10 (compilé avec GCC 4.9.3). Sous Windows, j'obtiens des augmentations de performances significatives en augmentant les threads de 1 à la quantité de cœurs disponibles; cependant, le même code ne voit aucun gain de performances sur Solaris 10.std :: performances asynchrones sous Windows et Solaris 10

La machine Windows dispose de 4 cœurs (8 logiques) et la machine Unix de 8 cœurs (16 logiques).

Quelle est la cause de ceci? Je compile avec -pthread, et est créant des discussions car il imprime tous les "S" es avant le premier "F". Je n'ai pas d'accès root sur la machine Solaris, et d'après ce que je peux voir il n'y a aucun outil installé que je peux utiliser pour voir l'affinité d'un processus.

code Exemple:

#include <iostream> 
#include <vector> 
#include <future> 
#include <random> 
#include <chrono> 

std::default_random_engine gen(std::chrono::system_clock::now().time_since_epoch().count()); 
std::normal_distribution<double> randn(0.0, 1.0); 

double generate_randn(uint64_t iterations) 
{ 
    // Print "S" when a thread starts 
    std::cout << "S"; 
    std::cout.flush(); 

    double rvalue = 0; 
    for (int i = 0; i < iterations; i++) 
    { 
     rvalue += randn(gen); 
    } 
    // Print "F" when a thread finishes 
    std::cout << "F"; 
    std::cout.flush(); 

    return rvalue/iterations; 
} 

int main(int argc, char *argv[]) 
{ 
    if (argc < 2) 
     return 0; 

    uint64_t count = 100000000; 
    uint32_t threads = std::atoi(argv[1]); 

    double total = 0; 

    std::vector<std::future<double>> futures; 
    std::chrono::high_resolution_clock::time_point t1; 
    std::chrono::high_resolution_clock::time_point t2; 

    // Start timing 
    t1 = std::chrono::high_resolution_clock::now(); 
    for (int i = 0; i < threads; i++) 
    { 
     // Start async tasks 
     futures.push_back(std::async(std::launch::async, generate_randn, count/threads)); 
    } 
    for (auto &future : futures) 
    { 
     // Wait for tasks to finish 
     future.wait(); 
     total += future.get(); 
    } 
    // End timing 
    t2 = std::chrono::high_resolution_clock::now(); 

    // Take the average of the threads' results 
    total /= threads; 

    std::cout << std::endl; 
    std::cout << total << std::endl; 
    std::cout << "Finished in " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " ms" << std::endl; 
} 
+0

Tout juste en haut de ma tête, essayez de déplacer les déclarations de 'gen' et' randn' dans 'generate_randn', de sorte qu'il y ait une instance RNG par thread au lieu d'un seul RNG partagé. Je serais également intéressé de voir la distribution complète des temps d'exécution par thread. – zwol

+0

Vous aviez raison, ça a bien fonctionné après avoir déplacé gen et randn à l'intérieur de generate_randn! Ajoutez-le comme une réponse et je le marquerai. :) –

Répondre

3

En règle générale, les classes définies par la bibliothèque standard C de ne pas ont tout blocage interne. La modification d'une instance d'une classe de bibliothèque standard à partir de plusieurs threads, ou la lecture d'une thread lors de l'écriture d'une autre thread, est un comportement indéfini, sauf si "les objets de ce type sont explicitement spécifiés comme partageables sans données". (N3337, sections 17.6.4.10 et 17.6.5.9.) Les classes RNG ne sont pas «explicitement spécifiées comme étant partageables sans séquences de données». (cout est un exemple d'un objet stdlib que est « partageable avec des courses de données. » - aussi longtemps que vous ne l'avez pas fait ios::sync_with_stdio(false))

En tant que tel, votre programme est incorrect car il accède à une RNG mondiale objet de plus d'un fil simultanément; chaque fois que vous demandez un autre nombre aléatoire, l'état interne du générateur est modifié. Sur Solaris, cela semble aboutir à la sérialisation des accès, alors que sous Windows, il est probable que vous n'obtiendrez pas des nombres "aléatoires".

Le traitement consiste à créer des GNA distincts pour chaque fil. Ensuite, chaque fil fonctionnera indépendamment, et ils ne se ralentiront pas et ne marcheront pas les uns sur les autres. C'est un cas particulier d'un principe très général: le multithreading fonctionne toujours mieux moins il y a de données partagées.

Il y a une ride plus à se soucier: chaque thread appellera system_clock::now à peu près en même temps, de sorte que vous pouvez finir avec quelques-uns des RNG par thread ensemencés avec la même valeur. Il serait préférable de les semer tous à partir d'un objet random_device. random_device demande des nombres aléatoires à partir du système d'exploitation, et n'a pas besoin d'être ensemencée; mais ça peut être très lent. Le random_device doit être créé et utilisé à l'intérieur de main, et les graines passées à chaque fonction de travail, car un random_device global accédé à partir de plusieurs threads (comme dans l'édition précédente de cette réponse) est tout aussi indéfini qu'un default_random_engine global.

Au total, votre programme devrait ressembler à ceci: (. Ce n'est pas vraiment une réponse, mais il ne rentre pas dans un commentaire, en particulier avec la commande mise en forme un lien)

#include <iostream> 
#include <vector> 
#include <future> 
#include <random> 
#include <chrono> 

static double generate_randn(uint64_t iterations, unsigned int seed) 
{ 
    // Print "S" when a thread starts 
    std::cout << "S"; 
    std::cout.flush(); 

    std::default_random_engine gen(seed); 
    std::normal_distribution<double> randn(0.0, 1.0); 

    double rvalue = 0; 
    for (int i = 0; i < iterations; i++) 
    { 
     rvalue += randn(gen); 
    } 
    // Print "F" when a thread finishes 
    std::cout << "F"; 
    std::cout.flush(); 

    return rvalue/iterations; 
} 

int main(int argc, char *argv[]) 
{ 
    if (argc < 2) 
     return 0; 

    uint64_t count = 100000000; 
    uint32_t threads = std::atoi(argv[1]); 

    double total = 0; 

    std::vector<std::future<double>> futures; 
    std::chrono::high_resolution_clock::time_point t1; 
    std::chrono::high_resolution_clock::time_point t2; 

    std::random_device make_seed; 

    // Start timing 
    t1 = std::chrono::high_resolution_clock::now(); 
    for (int i = 0; i < threads; i++) 
    { 
     // Start async tasks 
     futures.push_back(std::async(std::launch::async, 
            generate_randn, 
            count/threads, 
            make_seed())); 
    } 
    for (auto &future : futures) 
    { 
     // Wait for tasks to finish 
     future.wait(); 
     total += future.get(); 
    } 
    // End timing 
    t2 = std::chrono::high_resolution_clock::now(); 

    // Take the average of the threads' results 
    total /= threads; 

    std::cout << '\n' << total 
       << "\nFinished in " 
       << std::chrono::duration_cast< 
        std::chrono::milliseconds>(t2 - t1).count() 
       << " ms\n"; 
} 
+0

_ "L'implémentation Solaris de a un verrouillage interne" _, FWIW la question indique que c'est GCC 4.9, qui n'a pas de verrouillage dans "" sur Solaris ou toute autre plate-forme. Je ne sais pas pourquoi un moteur partagé + distribution serait si lent, mais c'est UB de toute façon, je ne suis pas très motivé pour le savoir. En utiliser un par thread est nécessaire pour l'exactitude, donc si cela aide aussi le problème de performance c'est bien :-) –

+0

@JonathanWakely Avez-vous une autre explication pour les phénomènes? Je ne peux pas expérimenter commodément sur Solaris ou Windows. – zwol

+0

Ma meilleure estimation serait que les différents threads sont programmés sur des processeurs différents et ils obtiennent constamment des échecs de cache pour les variables partagées, de sorte que la contention du cache les fait fonctionner en série. –

2

Vous pouvez profiler votre exécutable sous Solaris en utilisant Solaris Studio's collect utility. Sur Solaris, ce sera capable de vous montrer où vos discussions sont en conflit.

collect -d /tmp -p high -s all app [app args] 

afficher ensuite les résultats en utilisant the analyzer utility:

analyzer /tmp/test.1.er & 

Remplacer /tmp/test.1.er avec le chemin vers la sortie générée par une course de profil collect.

Si vos threads s'affrontent sur certaines ressources comme @zwol dans sa réponse, vous le verrez.

brief marketing Oracle pour l'ensemble d'outils est disponible ici: http://www.oracle.com/technetwork/server-storage/solarisstudio/documentation/o11-151-perf-analyzer-brief-1405338.pdf

Vous pouvez également essayer de compiler votre code avec Solaris Studio plus de données.

+0

Bien que le problème soit déjà résolu, merci pour la réponse! L'utilisation de Solaris Studio pour l'application réelle pour laquelle j'avais des problèmes était quelque chose qui n'était pas possible, c'est pourquoi j'ai testé dans GCC. Je vais certainement utiliser l'analyseur si je rencontre des problèmes similaires plus tard cependant! –