std::thread instanziieren ist langsam



  • Hallo zusammen

    Dass es nicht umsonst ist, Threads zu erstellen, weiß ich. Was mir aber dennoch auffiel, war, dass die Kosten einen Thread zu erstellen stark fluktuierten. Ich erstelle zwar nicht oft Threads (1 - 2 Mal pro Sekunde je 12 Threads), aber die Verzögerungen und die Ungereimtheiten sind als Nutzer schon deutlich spürbar.
    Im Programm habe ich einen Vektor aus Threads, der über die gesamte Programmdauer erhalten bleibt. Die Kapazität ist konstant 12. Wenn ich einen Job bekomme, dann emplace ich 12 Threads auf Einmal hinein, woraufhin ich diese sogleich joine und entferne. Das "Job-Objekt" ist ein Funktor, der nur einen Zeiger speichert.
    Ich habe dann die Zeiten zur Erstellung jedes einzelnen Threads mit QueryPerformanceCounter gemessen. Hier ist ein Dump: http://pastebin.com/w6Uzw2y2 (Wert in Klammern = welcher von den 12; Wert in Millisekunden)
    Wie man sieht, 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, dass pthread_create angeblich SleepEx aufruft. Das macht für mich durchaus Sinn, da z.B. Sleep(1) auf Windows tatsächlich oft etwa 31-32ms schläft. Screenshot vom Profiling-Tool: https://i.imgur.com/LYeAsTY.png

    All diese Informationen oben beziehen sich auf ein Windows 7 SP1 64-bit System, mit MinGW GCC 6.2.0 mit Posix-Threads.

    Ich habe das selbe Programm auch mit MSVC 2015 SP3 getestet. Dort war die Verteilung mit den Zeiten pro Thread etwas anders, aber grundsätzlich bestand genau dasselbe Problem. Der Callstack des problematischen Calls nach dem Erstellen von Threads ist dieser: https://i.imgur.com/Ui9mP5R.png

    Wenn mir jemand irgendwie dazu helfen kann, wäre ich sehr dankbar.
    Grüße

    PS.: Sorry, wenn das das falsche Forum ist. Ich wusste nicht, welches am meisten zutrifft, da es ja um Posix-Threads unter Windows in einem C++-Programm geht.



  • warum verwendest du nicht standard-threads?
    jedenfalls ist dein ansatz zwar gut, aber nicht perfekt - ein thread-pool hält seine threads normalerweise jederzeit bereit, um sie nicht ständig neu starten zu müssen. das dauert nämlich (wie du ja festgestellt hast) und du kannst es auch nicht steuern. siehe z.b. https://en.wikipedia.org/wiki/Thread_pool



  • dove schrieb:

    warum verwendest du nicht standard-threads?

    Tut der Fragesteller doch; die darunterliegende Implementation basiert einfach auf Posix-Threads.

    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(..) .



  • 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, dass pthread_create angeblich SleepEx 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. Das Sleep/SleepEx kann ja nicht so schwer zu finden sein. Dann wirst du - vermutlich - sehen, dass da Sleep(0) steht. Stünde da nämlich Sleep(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 kommt Sleep(0) sofort zurück und es gibt keine nennenswerte Verzögerung. Sobald aber mehr "ready" Threads als hardware Threads da sind, gibt Sleep(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 die std::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 die std::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 Flag Started 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 der conditional_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 Flag Started 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 der conditional_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 muss StartTheThing() 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. 🙂


Anmelden zum Antworten