2016-08-26 6 views
4

Ich führe ein einfaches Thread-Testprogramm auf einem Windows-Computer (kompiliert mit MSVS2015) und einem Server mit Solaris 10 (kompiliert mit GCC 4.9.3). Unter Windows bekomme ich erhebliche Leistungssteigerungen, wenn ich die Threads von 1 auf die Anzahl verfügbarer Cores erhöhe; Derselbe Code zeigt unter Solaris 10 jedoch keinerlei Leistungssteigerung.Std :: Async-Leistung unter Windows und Solaris 10

Die Windows-Maschine hat 4 Kerne (8 logische) und die Unix-Maschine hat 8 Kerne (16 logische).

Was könnte die Ursache dafür sein? Ich kompiliere mit -pthread, und es ist Erstellen von Threads, da es alle "S" es vor dem ersten "F" druckt. Ich habe keinen Root-Zugriff auf dem Solaris-Computer, und von dem, was ich sehen kann, gibt es kein installiertes Tool, mit dem ich die Affinität eines Prozesses anzeigen kann.

Beispielcode:

#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

Versuchen Sie einfach, die Deklarationen von 'gen' und' randn' innerhalb von 'generate_randn' zu verschieben, so dass es pro Thread eine RNG-Instanz anstelle eines einzelnen freigegebenen RNG gibt. Ich wäre auch interessiert, die vollständige Verteilung der Laufzeiten pro Thread zu sehen. – zwol

+0

Du hattest recht, es hat gut funktioniert, nachdem ich gen und randn in generate_randn gezogen habe! Fügen Sie es als Antwort hinzu und ich werde es markieren. :) –

Antwort

3

Als allgemeine Regel gilt, durch die C++ Klassen definiert Standardbibliothek tun nicht haben keine interne Verriegelung. Das Ändern einer Instanz einer Standardbibliotheksklasse aus mehr als einem Thread oder das Lesen von einem Thread während des Schreibens aus einem anderen Thread ist ein nicht definiertes Verhalten, es sei denn, "Objekte dieses Typs werden explizit als gemeinsam nutzbare Datenrasen angegeben". (N3337, Abschnitte 17.6.4.10 und 17.6.5.9.) Die RNG-Klassen sind nicht "explizit angegeben, dass sie ohne Datenrennen gemeinsam genutzt werden können". (cout ist ein Beispiel für ein stdlib Objekt, das ist „sharable mit Daten Rennen.“ - solange man nicht ios::sync_with_stdio(false) getan haben)

Als solches Ihr Programm ist falsch, weil es eine globale RNG-Zugriffe Objekt von mehr als einem Thread gleichzeitig; Jedes Mal, wenn Sie eine andere Zufallszahl anfordern, wird der interne Status des Generators geändert. Unter Solaris scheint dies zu einer Serialisierung von Zugriffen zu führen, während es unter Windows wahrscheinlich dazu führt, dass Sie keine richtigen "zufälligen" Zahlen erhalten.

Die Heilung besteht darin, separate RNGs für jeden Thread zu erstellen. Dann wird jeder Faden unabhängig arbeiten, und sie werden sich weder gegenseitig verlangsamen noch sich gegenseitig auf die Zehen treten. Dies ist ein Spezialfall eines sehr allgemeinen Prinzips: Multithreading funktioniert immer besser, je weniger gemeinsame Daten vorhanden sind.

Es gibt eine zusätzliche Falten zu befürchten: jeder Thread system_clock::now bei fast zur gleichen Zeit nennen wird, so dass Sie können mit einigen des Pro-Thread RNGs ausgesät mit dem gleichen Wert am Ende. Es wäre besser, sie alle aus einem random_device Objekt zu säen. random_device fordert Zufallszahlen vom Betriebssystem an und muss nicht gesetzt werden; aber es kann sehr langsam sein. Die random_device sollte innerhalb main erstellt und verwendet werden, und Samen an jede Worker-Funktion übergeben, da eine globale random_device Zugriff von mehreren Threads (wie in der vorherigen Ausgabe dieser Antwort) ist genauso undefiniert wie eine globale default_random_engine.

Alle gesagt, Ihr Programm wie folgt aussehen sollte: (. Das ist nicht wirklich eine Antwort, aber es wird nicht in einen Kommentar passen, vor allem mit dem Befehl eine Link Formatierung)

#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

_ "Solaris" Implementierung von hat interne Verriegelung "_, FWIW die Frage, es ist GCC 4.9, die keine Verriegelung in' 'auf Solaris oder einer anderen Plattform hat. Ich bin nicht sicher, warum eine gemeinsame Engine + Distribution so langsam wäre, aber es ist sowieso UB, also bin ich nicht sehr motiviert, das herauszufinden. Die Verwendung von einem pro Thread ist für die Korrektheit notwendig, also wenn es auch dem Leistungsproblem hilft, das ist nett :-) –

+0

@ JonathanWakely Haben Sie eine alternative Erklärung für die Phänomene? Ich kann nicht bequem auf Solaris oder Windows experimentieren. – zwol

+0

Meine beste Vermutung wäre, dass die verschiedenen Threads auf verschiedenen Prozessoren geplant sind und sie ständig Cache-Misses für die gemeinsam genutzten Variablen erhalten, so dass die Cache-Konkurrenz sie tatsächlich seriell laufen lässt. –

2

Sie können Ihre ausführbare Datei unter Solaris mit Solaris Studio's collect utility profilieren. Unter Solaris können Sie dort anzeigen, wo Ihre Threads konkurrieren.

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

Sehen Sie dann die Ergebnisse unter Verwendung the analyzer utility:

analyzer /tmp/test.1.er & 

/tmp/test.1.er Ersetzen mit dem Pfad zu der von einem Profil collect Laufe erzeugten Ausgabe.

Wenn Ihre Threads über einige Ressourcen streiten, wie @zwol in seiner Antwort geschrieben hat, werden Sie es sehen.

Oracle Marketing kurz für das Toolset finden Sie hier: http://www.oracle.com/technetwork/server-storage/solarisstudio/documentation/o11-151-perf-analyzer-brief-1405338.pdf

Sie auch versuchen können mehr Daten Sie Ihren Code mit Solaris Studio kompilieren.

+0

Obwohl das Problem schon gelöst ist, danke für die Antwort! Die Verwendung von Solaris Studio für die eigentliche Anwendung, für die ich Probleme hatte, war etwas, das nicht möglich war. Aus diesem Grund habe ich in GCC getestet. Ich werde definitiv Analysator verwenden, wenn ich später auf ähnliche Probleme stoßen werde! –