std::thread instanziieren ist langsam
-
theta schrieb:
dove schrieb:
warum verwendest du nicht standard-threads?
Tut der Fragesteller doch; die darunterliegende Implementation basiert einfach auf Posix-Threads.
ich dachte, der OP verwendet die POSIX-funktionen direkt (wie
pthread_create
).Die Threads nicht jedes Mal neu zu erstellen, hilft bestimmt. Ein Thread-Pool bietet sich an, wie von dove erwähnt. Oder gleich
std::async(..)
.MSVC bietet hier wohl einen thread-pool an, allerdings ist das implementationsabhängig. manche leute behaupten, ein
std::async
dürfe standardkonform gar keine thread-pools verwenden; unabhängig davon ist hier eine eigene implementation (soll heißen: eine der vielen, die es schon gibt) sicher besser, vor allem, wenn man die anzahl der threads auch kontrollieren will.
-
Fytch schrieb:
...sind die ersten paar Threads pro 12er-Batch immer im Bereich von 0.01-0.05ms. Die letzteren Threads hingegen brauchen hin und wieder deutlich mehr Zeit, wobei sich der exakte Wert zwischen 3ms und 32ms bewegt, mit Häufungen insbesondere bei 31ms & 32ms.
Mein Profiler sagt mir außerdem, dass das davon kommt, dasspthread_create
angeblichSleepEx
aufruft. Das macht für mich durchaus Sinn, da z.B.Sleep(1)
auf Windows tatsächlich oft etwa 31-32ms schläft.Guck mal in die
pthread_create
Implementierung rein. DasSleep/SleepEx
kann ja nicht so schwer zu finden sein. Dann wirst du - vermutlich - sehen, dass daSleep(0)
steht. Stünde da nämlichSleep(1)
, dann sollten eigentlich alle Aufrufe > 1 msec dauern. (Die MSDN ist was das Thema Aufrunden vs. Abrunden angeht etwas unklar mit Hang zu "Sleep
wartet nie länger als angegeben", nach meiner Erfahrung ist es aber eher "Sleep
wartet nie kürzer als angegeben" - was IMO auch viel mehr Sinn macht.)Und wenn das so ist, dann ist die Frage einfach: Wie viele Hardware-Threads hat deine CPU. Wenn die Antwort 4~8 ist, dann würde mich das beobachtete Verhalten nicht wundern. Dann würde das einfach heissen: pthread_create ruft - vermutlich nach dem Erzeugen des Threads - gleich mal
Sleep(0)
auf, um dem neu erzeugten Thread sofort ne Chance zu geben zu laufen. So lange noch nicht genug Threads "ready" sind kommtSleep(0)
sofort zurück und es gibt keine nennenswerte Verzögerung. Sobald aber mehr "ready" Threads als hardware Threads da sind, gibtSleep(0)
wirklich den Rest der Zeitscheibe ab. 32 klingt da auch plausibel, da ein Quantum IIRC 2 Ticks lang ist, und ein Tick üblicherweise ca. 16 ms.Eine Möglichkeit das zu lösen wäre wie dove und theta schon erwähnt haben vermutlich einen Thread-Pool zu verwenden. Ich bin nicht 100% sicher, aber ich meine mich zu erinnern dass das "normale" Aufwachen eines Threads durch einen Event/Condition-Variable/... nicht zu einem dynamischen Boost führt. Und wenn das korrekt ist, dann sollten die aufgeweckten Worker-Threads dem "aufweckenden" Thread auch nicht sofort die Rechenzeit stehlen.
Eine andere Möglichkeit wäre den "aufweckenden" Thread mit höherer Priorität laufen zu lassen als die Worker. Also entweder den "aufweckenden" Thread mit +1 oder die Worker-Threads mit -1. Doof dabei ist bloss dass es noch keinen Weg gibt die Priorität eines
std::thread
mit ausschliesslich Standard C++ Mitteln zu ändern.
-
Hallo nochmals
@dove: Ich verwende, wie theta angemerkt hat,
std::thread
. Jedoch verwendet mein MinGW darunter eine Bibliothek, die POSIX implementiert. MSVC's Dinkumware hingegen nutzt direkt die WinAPI.@hustbaer: Ich habe mal nachgesehen und es wird wahrscheinlich auf diese Zeile (und nicht auf diese) zurückzuführen sein.
Meine CPU hat auch 12 Hardware-Threads, oder logische Kerne, wie man das auch immer nennen will. Deine Erläuterung leuchtet aber durchaus ein und erklärt auch, wieso es immer die letzteren Threads sind, die so eine Verzögerung nach sich ziehen.Weil es von allen vorgeschlagen wurde, habe ich den Ansatz mit dem Thread-Pool implementiert. Dabei habe ich eine
std::condition_variable
verwendet, mit der ich die 12 Threads aus ihrem wartenden Zustand wecke, sobald neue Arbeit vorhanden ist. Mein Problem hierbei ist nur, dass diestd::condition_variable
selbst noch langsamer ist, als einfach neue Threads zu machen. Ich habe ein minimales Beispiel zusammengeschnippelt, das mein Problem reproduziert. Ich wäre dankbar, wenn ihr den vielleicht kompilieren und laufen lassen könntet und dann die Resultate postet. Bei mir (MinGW GCC 6.2.0 mit POSIX-Threads) gibt das z.B. aus:0ms 0ms 0ms 0ms 0ms 0ms 25.0014ms 25.0014ms 57.0033ms 64.0037ms 64.0037ms 64.0037ms
Das ist leider noch schlechter, als einfach immer neue Threads zu machen. Habe ich im Code etwas grundsätzlich falsch gemacht, dass er so miserable Performance aufweist?
Grüße
-
32 Bit 1. Test 0.019358ms 0.025283ms 0.032ms 0.041481ms 0.041876ms 0.06558ms 0.070716ms 3.86212ms 2. Test 0.035556ms 0.039901ms 0.043062ms 0.044642ms 0.048593ms 0.056889ms 0.084543ms 0.122469ms 3. Test 0.071111ms 0.078618ms 0.085729ms 0.087309ms 4.09363ms 4.10232ms 5.5277ms 6.99338ms
64 Bit 1. Test 0.060839ms 0.06479ms 0.06795ms 0.090469ms 0.112197ms 0.114568ms 0.120889ms 31.8309ms 2. Test 0.019358ms 0.027655ms 0.034766ms 0.043852ms 0.044247ms 0.052148ms 0.056889ms 0.06084ms 3. Test 0.018568ms 0.019753ms 0.020938ms 0.028049ms 0.045827ms 0.063605ms 0.070716ms 0.079012ms
Visual C++ 2015 - Intel i7-6700HQ @ 2.6GHz
Meep Meep
-
@Meep Meep: Und was verbirgt sich hinter Test1/2/3?
Denn ohne dieses Wissen sind die zahlen nutzlos
-
sein minimales beispiel hab ich 3 mal als 32 bit und 3 mal als 64 bit laufen lassen
-
Fytch schrieb:
@hustbaer: Ich habe mal nachgesehen und es wird wahrscheinlich auf diese Zeile (und nicht auf diese) zurückzuführen sein.
Denke ich auch, denn dass
CreateEvent
fehlschlägt ist zwar theoretisch möglich, aber mir in der Praxis noch nicht untergekommen. Dazu müsste das System so arg am Sand sein (Resourcenmangel), dass es mMn. auch keinen grossen Sinn hat noch ein paar Retries zu machen.Fytch schrieb:
Meine CPU hat auch 12 Hardware-Threads, oder logische Kerne, wie man das auch immer nennen will. Deine Erläuterung leuchtet aber durchaus ein und erklärt auch, wieso es immer die letzteren Threads sind, die so eine Verzögerung nach sich ziehen.
Ja. Wobei es eigentlich nur ein bzw. maximal zwei Threads sein sollten, wenn man davon ausgeht dass sonst nix läuft was nennenswert Rechenzeit verbrät. Hast du nebenbei noch Dinge laufen während du diese Tests machst?
Fytch schrieb:
Weil es von allen vorgeschlagen wurde, habe ich den Ansatz mit dem Thread-Pool implementiert. Dabei habe ich eine
std::condition_variable
verwendet, mit der ich die 12 Threads aus ihrem wartenden Zustand wecke, sobald neue Arbeit vorhanden ist. Mein Problem hierbei ist nur, dass diestd::condition_variable
selbst noch langsamer ist, als einfach neue Threads zu machen.Naja... du misst da ja jetzt 'was anderes, nämlich die Zeit bis die Worker-Threads anfangen zu laufen. Und das kann schonmal ein bisschen dauern, du hast ja nicht unendlich Hardware-Threads zur Verfügung.
Das "Anwerfen" des Thread-Pools sollte so aber recht schnell gehen, und so wie ich es verstanden habe ist es das worum es dir hauptsächlich ging ...?Mal abgesehen davon dass du nicht korrekt synchronisierst (Zugriff auf Started/Kill während du nicht die Mutex gelockt hast etc.). Ich würde vorschlagen mal ne Proof-Of-Concept Implementierung zu machen mit der du das in deiner Anwendung testen kannst - also so dass die Worker auch wirklich Arbeit verrichten.
-
Meep Meep schrieb:
[...]
Dein System scheint deutlich weniger Verzögerung aufzuweisen. Merkwürdig...
hustbaer schrieb:
Wobei es eigentlich nur ein bzw. maximal zwei Threads sein sollten, wenn man davon ausgeht dass sonst nix läuft was nennenswert Rechenzeit verbrät. Hast du nebenbei noch Dinge laufen während du diese Tests machst?
Ein paar Programme schon, die CPU-Auslastung ist jedoch stets unter 1%. Ich habe eben das selbe Programm nach einem frischen Reboot getestet und die Ergebnisse sind identisch.
hustbaer schrieb:
Naja... du misst da ja jetzt 'was anderes, nämlich die Zeit bis die Worker-Threads anfangen zu laufen. Und das kann schonmal ein bisschen dauern, du hast ja nicht unendlich Hardware-Threads zur Verfügung.
Das "Anwerfen" des Thread-Pools sollte so aber recht schnell gehen, und so wie ich es verstanden habe ist es das worum es dir hauptsächlich ging ...?Ich verstehe dich nicht ganz. In meinem geposteten Beispiel beginnen die Threads mit dem Konstruktor von
ThreadPool
zu laufen. Weil das FlagStarted
jedoch nicht gesetzt ist, werden die Threads sofort in einen Wartezustand übergehen. Um mich dessen zu vergewissern, habe ich nach dem Konstruktoraufruf ein kurzes Sleep eingebaut. Danach setze ich das Flag und benachrichtige die Threads mittels derconditional_variable
. Von genau da an messe ich, bis hin zum Zeitpunkt, wo der letzte Thread die Arbeit begonnen hat. Und diese Spanne ist 64ms.
Wenn es nicht das ist, was du mit dem Anwerfen des Pools gemeint hast, was dann?
Was ich möchte, ist, die Zeit, die es dauert, um die Arbeiter-Threads aus dem Warte-Zustand zu wecken, zu verringern. Wenn ich jedes Mal neue Threads erstelle, dauert es ein bisschen, bis diese konstruiert sind. Und hier dauert es jedes Mal, wenn ich die Threads wecken möchte.hustbaer schrieb:
Mal abgesehen davon dass du nicht korrekt synchronisierst (Zugriff auf Started/Kill während du nicht die Mutex gelockt hast etc.)
Ich habe das nun gelöst, indem ich die Flags zu
std::atomic
s gemacht habe. Zu meinem Erstaunen hat es wirklich einen Unterschied gemacht (davor hat das Programm nicht immer korrekt funktioniert, aber ich konnte mir nicht erklären wieso). Kannst du das vielleicht ein wenig erläutern? Ich dachte immer, es sei ok, wenn genau ein Thread schreibt und mehrere lesen. Im schlimmsten Fall bekommt einer der lesenden Threads die Mitteilung eben erst später zu sehen, mehr aber nicht. Wird da tatsächlich auch beim Lesezugriff etwas zurückgeschrieben?hustbaer schrieb:
Ich würde vorschlagen mal ne Proof-Of-Concept Implementierung zu machen mit der du das in deiner Anwendung testen kannst - also so dass die Worker auch wirklich Arbeit verrichten.
Das habe ich schon gemacht, bevor ich gepostet habe. Mein minimales Beispiel, das ich gezeigt habe, ist in Wirklichkeit eine abgespeckte Version meiner eigentlichen
ThreadPool
-Implementierung. Auch in meinem echten Programm habe ich Messungen eingebaut, und zwar wie viel Zeit vergeht zwischen Nutzereingabe (also dort wo mein Programm den Event bekommt) und dort wo der letzte Thread aufgewacht und mit der Arbeit begonnen hat (ähnlich wie in meinem Beispiel). Hier ein paar Werte der Messung:78.9585 42.6059 40.3486 71.2987 0.0590301 1.44654 0.0578254 32.9084 0.0168657 0.352373 0.0596324 71.0566
Die teilweise tiefen Werte kommen daher, wenn die Threads schon Arbeit verrichten und einfach mehr dazubekommen.
-
Fytch schrieb:
hustbaer schrieb:
Naja... du misst da ja jetzt 'was anderes, nämlich die Zeit bis die Worker-Threads anfangen zu laufen. Und das kann schonmal ein bisschen dauern, du hast ja nicht unendlich Hardware-Threads zur Verfügung.
Das "Anwerfen" des Thread-Pools sollte so aber recht schnell gehen, und so wie ich es verstanden habe ist es das worum es dir hauptsächlich ging ...?Ich verstehe dich nicht ganz. In meinem geposteten Beispiel beginnen die Threads mit dem Konstruktor von
ThreadPool
zu laufen. Weil das FlagStarted
jedoch nicht gesetzt ist, werden die Threads sofort in einen Wartezustand übergehen. Um mich dessen zu vergewissern, habe ich nach dem Konstruktoraufruf ein kurzes Sleep eingebaut. Danach setze ich das Flag und benachrichtige die Threads mittels derconditional_variable
. Von genau da an messe ich, bis hin zum Zeitpunkt, wo der letzte Thread die Arbeit begonnen hat. Und diese Spanne ist 64ms.
Wenn es nicht das ist, was du mit dem Anwerfen des Pools gemeint hast, was dann?
Was ich möchte, ist, die Zeit, die es dauert, um die Arbeiter-Threads aus dem Warte-Zustand zu wecken, zu verringern.Naja du hast da zwei Teile. Einmal die Dauer der
StartTheThing()
Funktion. Diese kannst du definitiv verringern indem du einen Pool verwendest. Dann mussStartTheThing()
nämlich bloss ne Mutex locken, ein paar Sachen updaten, ne Condition-Variable feuern und dann die Mutex wieder freigeben. Sofern die Mutex nicht "contended" ist geht das alles relativ zügig. Also zumindest wenn wir in Millisekunden zählen.Der zweite Teil ist dann die Zeit die zwischen dem Aufruf von
StartTheThing()
und dem Zeitpunkt vergeht wo alle Worker-Threads angefangen haben auch wirklich "zu worken". Und da kannst du nicht unbedingt besonders viel tun. Das OS wird die Threads inetwa dann wecken sobald keine anderen Threads mit höherer oder gleicher Priorität mehr da sind. Normalerweise gibt's da aber auch kein grosses Problem. Verzögerungen dürfen da eigentlich nur entstehen wenn kein Core frei ist auf dem der jeweils aufgeweckte Thread sofort laufen könnte.Fytch schrieb:
Ich dachte immer, es sei ok, wenn genau ein Thread schreibt und mehrere lesen. Im schlimmsten Fall bekommt einer der lesenden Threads die Mitteilung eben erst später zu sehen, mehr aber nicht. Wird da tatsächlich auch beim Lesezugriff etwas zurückgeschrieben?
Es ist OK wenn einer schreibt.
Es ist OK wenn mehrere gleichzeitig lesen.
Es ist aber natürlich nicht OK wenn einer schreibt UND gleichzeitig noch jemand liest.
Und vonwegen Atomics: verwende lieber die Mutex die du sowieso brauchen wirst.
-
Danke dir, hustbaer, für deine ausführlichen Erklärungen. Ich konnte deine Anregungen in meinem Code umsetzen.