Forum: PC-Programmierung Performance std::thread vs std::future


von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo,

ich schreibe im Moment verschiedene kleine Progrämmchen um mich im 
Umgang mit modernem C++ zu üben. Da gibt es wirklich sehr sehr viel zu 
lernen und verstehen.

Bei ersten Übungen zum Thema Concurrency bin ich auf einen sehr 
interessanten Effekt gestoßen.

Ich hätte erwartet, dass std::thread sehr viel performanter ist, als 
std::async, weil letzteres ja im Endeffekt nur eine aufwendigere 
Schnittstelle zu ersterem ist. Der Unterschied ist groß genug, dass ich 
gern nachfragen möchte, ob jemand weiß, warum?

1. Spalte Timing in ns, 2. Spalte Ergebnis der Berechnung

std::thread
generic 131282  9995155
atomic  132604  10000000
mutex   423830  10000000

std::async
generic 101916  9999564
atomic  103509  10000000
mutex   123793  10000000

3 Dinge finde ich besonders interessant:

1. async + mutex ist schneller als thread/nicht safe, das ist doch 
verrückt?

2. Bei thread ist atomic drei mal schneller als mutex, bei async ist 
atomic aber bloß 20 % schneller.

3. Vor allem aber wird die mutex Variante im Vergleich zur atomic 
Variante überproportional schneller, was doch sogar unerwartet ist, wenn 
einfach nur der Verwaltungsaufwand sinkt?

Das kompilierbare Programm hängt unten dran, ist ein klein wenig länger.

Herzliche Grüße

 Timm

Edit: Beim mutex ist nur inc() gemutext, dec() wird ja nicht benutzt.
Edit2: Ich habe mir schon die Finger wund gesucht, aber ich habe den 
Eindruck, dass wenn es um die eher neuen Features geht aktuelle Infos 
wirklich sehr rar sind.
1
#include <iostream>
2
#include <iomanip>
3
#include <chrono>
4
#include <vector>
5
#include <cmath>
6
#include <thread>
7
#include <future>
8
#include <atomic>
9
#include <mutex>
10
11
using namespace std::chrono_literals;
12
13
template<typename TimeT = std::chrono::milliseconds>
14
struct measure
15
{
16
    template<typename F, typename ...Args>
17
    static typename TimeT::rep execution(F&& func, Args&&... args)
18
    {
19
        auto start = std::chrono::steady_clock::now();
20
        std::forward<decltype(func)>(func)(std::forward<Args>(args)...);
21
        auto duration = std::chrono::duration_cast< TimeT>
22
        (std::chrono::steady_clock::now() - start);
23
        return duration.count();
24
    }
25
};
26
27
struct genericCounter {
28
    int count;
29
    genericCounter() :count(0) {};
30
    int inc() {return ++count;}
31
    int dec() {return --count;}
32
    int get() const {return count;}
33
    int set(const int in) {return count=in;}
34
};
35
36
struct atomicCounter {
37
    std::atomic<int> count;
38
    atomicCounter() :count(0) {};
39
    int inc() {return ++count;}
40
    int dec() {return --count;}
41
    int get() const {return count;}
42
    int set(const int in) {return count=in;}
43
};
44
45
struct mutexCounter {
46
    int count;
47
    std::mutex m;
48
    mutexCounter() :count(0) {};
49
    int inc() {m.lock(); ++count; m.unlock(); return count;}
50
    int dec() {return --count;}
51
    int get() {return count;}
52
    int set(const int in) {return count=in;}
53
};
54
55
void threadExplosion(std::function<void()> f) {
56
    std::vector<std::thread> threads;
57
    
58
    for(int i = 0; i < 10; ++i){
59
        threads.push_back(std::thread(f));
60
    }
61
    
62
    for(auto& thread : threads){
63
        thread.join();
64
    }
65
}
66
67
void futureExplosion(std::function<void()> f) {
68
    std::vector<std::future<void>> threads;
69
    
70
    for(int i = 0; i < 10; ++i){
71
        threads.push_back(std::async(std::launch::async, f));
72
    }
73
    
74
    for(auto& thread : threads){
75
        thread.get();
76
    }
77
}
78
79
template <typename T>
80
void tf(T f) {
81
}
82
    
83
int main(int argc, const char * argv[]) {
84
    genericCounter g1;
85
    auto test1 = [&g1](){ for(int n=0; n<100; ++n) g1.inc(); };
86
87
    atomicCounter g2;
88
    auto test2 = [&g2](){ for(int n=0; n<100; ++n) g2.inc(); };
89
    
90
    mutexCounter g3;
91
    auto test3 = [&g3](){ for(int n=0; n<100; ++n) g3.inc(); };
92
    
93
    unsigned long int time1=0,
94
          time2=0,
95
          time3=0;
96
    
97
    int loops = 10000;
98
    
99
    for(int n=0; n<loops; n++) {
100
        time1+=measure<std::chrono::nanoseconds>::execution(threadExplosion,test1);
101
        time2+=measure<std::chrono::nanoseconds>::execution(threadExplosion,test2);
102
        time3+=measure<std::chrono::nanoseconds>::execution(threadExplosion,test3);
103
        
104
    }
105
    
106
    std::cout << time1/loops << "  " << g1.get() << std::endl
107
              << time2/loops << "  " << g2.get() << std::endl
108
              << time3/loops << "  " << g3.get() << std::endl;
109
    
110
    time1=time2=time3=0;
111
    g1.set(0); g2.set(0); g3.set(0);
112
    
113
    for(int n=0; n<loops; n++) {
114
        time1+=measure<std::chrono::nanoseconds>::execution(futureExplosion,test1);
115
        time2+=measure<std::chrono::nanoseconds>::execution(futureExplosion,test2);
116
        time3+=measure<std::chrono::nanoseconds>::execution(futureExplosion,test3);
117
        
118
    }
119
    
120
    std::cout << time1/loops << "  " << g1.get() << std::endl
121
              << time2/loops << "  " << g2.get() << std::endl
122
              << time3/loops << "  " << g3.get() << std::endl;
123
    
124
    return 0;
125
}

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Der Unterschied zwischen den Varianten ist, dass der ctor von 
std::thread immer(!) einen neuen Thread des OS erzeugt, während 
std::async ein internes Threadpooling macht. Es wird also ggf. darauf 
verzichtet, einen neuen OS Thread zu erzeugen, wenn noch einer im Pool 
existiert.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Timm R. schrieb:
> Ich hätte erwartet, dass std::thread sehr viel performanter ist, als
> std::async, weil letzteres ja im Endeffekt nur eine aufwendigere
> Schnittstelle zu ersterem ist. Der Unterschied ist groß genug, dass ich
> gern nachfragen möchte, ob jemand weiß, warum?

Einen thread zu starten könnte teuer sein. Bei std::async sollten die 
threads eines pools wieder verwendet werden. Das Verhalten würde ich so 
etwarten. (und vor allem hätte dieser Pool, wahrscheinlich nicht mehr 
threads, als CPUs.)

> 2. Bei thread ist atomic drei mal schneller als mutex, bei async ist
> atomic aber bloß 20 % schneller.

Das könnte an zu hoher contention liegen. Wenn Du mehr threads am laufen 
hast, als CPUs dann kann es sein, dass der scheduler evtl. auch mal 
einen thread mit gelocketem mutex schlafen legt. Das ist natürlich Gift 
für die Performance, soetwas würde man so nie designen und deswegen 
kannst Du mit Deinen Messergebnissen auch herzlich wenig anfangen.

mfg Torsten

von mh (Gast)


Lesenswert?

Ich habe das Program nur kurz überflogen und ich habe mich bis jetzt 
noch nicht wirklich mit c++ threads und co beschäftigt. Meine erste 
Vermutung ist, dass dein Program nicht das machst was du möchtest.

Du möchtest für jeden deiner Testfälle:
1. Zeit nehmen
2. "Threads starten"
3. auf Ergebnis warten
4. Zeit stoppen

du machst aber:
1. Zeit nehmen
2. "Threads starten"
3. Zeit stoppen
4. auf Ergebnis warten

Du misst also wie lange das erstellen und starten der Threads dauert.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo Du Lexikon,

Wilhelm M. schrieb:
> Der Unterschied zwischen den Varianten ist, dass der ctor von
> std::thread immer(!) einen neuen Thread des OS erzeugt, während
> std::async ein internes Threadpooling macht. Es wird also ggf. darauf
> verzichtet, einen neuen OS Thread zu erzeugen, wenn noch einer im Pool
> existiert.

bist du zufällig mit Stroustrup oder Sutter verwandt oder verschwägert 
:-)


Das Wiederverwenden würde einiges erklären. Gerade in meinem Beispiel 
würde das natürlich monstermäßig zuschlagen, weil ich so viele Threads 
erzeuge und beerdige.

Zwei Fragen hätte ich: 1. Steht das irgendwo? (Nicht weil ich zweifle, 
sondern weil in dem Text bestimmt noch andere relevante Dinge stehen)

2. Praktisch alle Tutorials verwenden std::thread, warum nur? std::async 
scheint viel besser zu sein? Auch diese Eigentschaft spricht doch 
absolut für std::async?

Besten Dank!

vlg

 Timm

von mh (Gast)


Lesenswert?

Ok überfliegen reicht nicht. Hab das get in den explosions übersehen.

von Wilhelm M. (wimalopaan)


Lesenswert?

Timm R. schrieb:

>
> Zwei Fragen hätte ich: 1. Steht das irgendwo? (Nicht weil ich zweifle,
> sondern weil in dem Text bestimmt noch andere relevante Dinge stehen)

Nicht so direkt. Das muss ja auch nicht so sein, denn es ist eine 
Implementierungsfreiheit. Das schreibt keiner vor. Nur das Verhalten von 
std::thread ist so definiert. Also: eine gute Implementierung wird das 
so machen, eine schlechte muss es aber nicht.

In der Doku von std::async steht:
1
Behaves as if (2) is called with policy being std::launch::async | std::launch::deferred. In other words, f may be executed in another thread or it may be run synchronously when the resulting std::future is queried for a value.

Dieser Satz gibt m.E. alle Freiheiten ...

> 2. Praktisch alle Tutorials verwenden std::thread, warum nur? std::async
> scheint viel besser zu sein? Auch diese Eigentschaft spricht doch
> absolut für std::async?

(Vielleicht weil viel abgeschrieben wird ...)

: Bearbeitet durch User
von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo,

vielen Dank schonmal an alle!

@Torsten
Der Effekt tritt ganz ähnlich auch bei 2 Threads auf:

34611  1998242
34005  2000000
41736  2000000

26220  1999971
26778  2000000
28366  2000000

Was ich auch noch interessant finde, ist dass es bei der ungeschützten 
futures-Variante sytematisch weniger Kollisionen gibt (siehe zweite 
Spalte: 1998242 vs 1999971, also 1800 vs. 30 Kollisionen)

vlg
 Timm

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Timm R. schrieb:

> 2. Praktisch alle Tutorials verwenden std::thread, warum nur? std::async
> scheint viel besser zu sein? Auch diese Eigentschaft spricht doch
> absolut für std::async?

Weil die Tutorials die Verwendung von threads erklären wollen? 
std::thread und std::async sind doch total unterschiedliche Dinge.

std::thread verwendest Du typischerweise dort, wo durchgängig einen 
eigenen CPU kontext benötigst (z.B: als member eines pools). std::async 
verwendest Du da, wo Du mal eben einen pool verwenden möchtest.

Mir hat "Programming with POSIX Threads" von David R. Butenhof sehr gut 
gefallen. Die C++ thread API lehnt sich direkt an Posix an.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Timm R. schrieb:
> @Torsten
> Der Effekt tritt ganz ähnlich auch bei 2 Threads auf:>

Ja, es kann sein, dass Dein std::async überhaupt keinen thread startet 
und die übergebene Funktion direkt ausführt. Das wäre in Deinem Fall ja 
auch das effektivste :-)

von Wilhelm M. (wimalopaan)


Lesenswert?

Mit std::thread und std::async vergleicht man unterschiedliche Dinge: 
ich meine jetzt nicht, dass std::async auch threads (oder tasks oder 
sonst was) verwenden kann, sondern es handelt sich hierbei um 
unterschiedliche Abstraktionsebenen.

std::thread ist low-level: ich will hier definitiv einen Thread 
erzeugen.

std::async ist high(er)-level: ich will eine Aufgabe asynchron erledigen 
lassen (wie, ist mir vollkommen egal) und das Ergebnis als Future später 
irgendwann (oder auch nicht) abfragen.

Daran sieht man, dass low-level nicht immer geschickter ist, denn bei 
vielen high-level Ansätzen konnten sich schon eine ganze Menge 
intelligenter Leute bei der Realisierung der Abstraktion austoben ...

: Bearbeitet durch User
von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo,

Torsten R. schrieb:
> Timm R. schrieb:
>> @Torsten
>> Der Effekt tritt ganz ähnlich auch bei 2 Threads auf:>
>
> Ja, es kann sein, dass Dein std::async überhaupt keinen thread startet
> und die übergebene Funktion direkt ausführt.

nee. Erstens würde ich das ja im Debugger sehen, zweitens würde dann ja 
auch mein generic counter, ohne Schutzmaßnahmen, das richtige Ergebnis 
liefern und drittens garantiert der Standard die asynchrone Ausführung.

§30.8.6 Nr.6
Throws: system_error if policy == launch::async and the implementation 
is unable to start a new thread.

vlg

Timm

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Timm R. schrieb:
>> Ja, es kann sein, dass Dein std::async überhaupt keinen thread startet
>> und die übergebene Funktion direkt ausführt.
>
> nee. Erstens würde ich das ja im Debugger sehen, zweitens würde dann ja
> auch mein generic counter, ohne Schutzmaßnahmen, das richtige Ergebnis
> liefern und drittens garantiert der Standard die asynchrone Ausführung.

Stimmt, dass solte dann nicht passieren.

> §30.8.6 Nr.6
> Throws: system_error if policy == launch::async and the implementation
> is unable to start a new thread.

Das widerspricht ja nicht einer Implementierung, die überhaupt keinen 
thread startet (siehe Zitat von Wilhelm). Auf einer single processor 
Maschine wäre das sogar eine sehr sinnvolle Implementierung.

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Torsten R. schrieb:
> Timm R. schrieb:
>>> Ja, es kann sein, dass Dein std::async überhaupt keinen thread startet
>>> und die übergebene Funktion direkt ausführt.
>>
>> nee. Erstens würde ich das ja im Debugger sehen, zweitens würde dann ja
>> auch mein generic counter, ohne Schutzmaßnahmen, das richtige Ergebnis
>> liefern und drittens garantiert der Standard die asynchrone Ausführung.
>
> Stimmt, dass solte dann nicht passieren.

genau, also bei meiner Implementierung definitiv Threads, habs auch 
extra noch mal überprüft.

Und egal mit wievielen Threads, immer ist der „Fehler“ im ungeschützten 
Counter mit std::thread signifikant größer.

34611  1998242
26220  1999971

Ist wohl nicht so wichtig, aber mich würde schon interessieren, ob der 
Compiler da mit irgendwelchen Tricks etwas dreht ...

vlg

 Timm

von Timm R. (Firma: privatfrickler.de) (treinisch)


Lesenswert?

Hallo,

hier noch mal zwei prominente Ergebnisse meiner Recherchen, die mich 
hatten glauben lassen std::async sei weniger cool, als es in 
Wirklichkeit ist und soweit ich die Antworten und mein Experiment 
verstehe auch wohl schlicht so nicht richtig sind:


https://stackoverflow.com/questions/25814365/when-to-use-stdasync-vs-stdthreads

Frage: When to use std::async vs std::threads?
Antwort: ... but std::async is rather limited in the current standard. 
...

Currently, std::async is probably best suited to handling either very 
long running computations or long running IO for fairly simple programs. 
It doesn't guarantee low overhead though (and in fact the way it is 
specified makes it difficult to implement with a thread pool behind the 
scenes), so it's not well suited for finer grained workloads. For that 
you either need to roll your own thread pools using std::thread ...


https://bartoszmilewski.com/2011/10/10/async-tasks-in-c11-not-quite-there-yet/

If you expected std::async to be just syntactic sugar over thread 
creation, you can stop reading right now, because that’s what it is.

Dazu im Kontrast die Feststellung Wilhelms, die sich ja absolut mit 
meiner Beobachtung deckt:

Daran sieht man, dass low-level nicht immer geschickter ist, denn bei
vielen high-level Ansätzen konnten sich schon eine ganze Menge
intelligenter Leute bei der Realisierung der Abstraktion austoben ...


Erleuchtete Grüße

 Timm

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.