Wozu async deferred?



  • nwp3 schrieb:

    Ich finde es total sinnvoll. Man hat ständig irgendwelche Code-Stücken, die man parallelisieren könnte. Tut man aber nicht, denn der Overhead einen Thread zu erstellen ist größer als der Gewinn durch parallele Abarbeitung. Das muss man ständig beachten, und wahrscheinlich überblickt man nicht, dass bei der Änderung einer Funktion diese etwas langsamer geworden ist und deshalb Threads, die vorher wegen Ineffizienz ausgeschlossen wurden, plötzlich sinnvoll werden. Genau dieses Problem löst der launch-Parameter. Der Compiler sieht sich den Code an und entscheidet, ob sich der Overhead lohnt, und zwar viel besser, als der Programmierer es je könnte, insbesondere in Verbindung mit Profiling.

    Eis off schrieb:

    Okay. Und welchen Sinn macht es, dass man std::launch::async und deferred mixen kann? Dass er, wenn er Lust hat, beim retrieven des Wertes erst was rauszieht finde ich - wenn ich nicht gerade debugge - unerwünscht.

    Doch, das ist genau was du willst. Du bekommst 3 Möglichkeiten:
    launch::deferred: wegen Debugging
    launch::async: weil du zeigen willst was eine Race-Condition für Folgen hat (Meiner Meinung nach die einzige sinnvolle Anwendung von launch::async)
    launch::async|launch::deferred: Mach es Richtig. Wenn sich Threading lohnt dann tus, ansonsten lass es

    Dass der gcc das noch nicht beherrscht ist schade, wird aber sicher bald gefixt.

    Ein Programmierer sollte bei jedem noch so kleinen parallelisierbaren Codestück async + future::wait benutzen und der Compiler optimiert das zu einem normalen Funktionsaufruf, ohne Overhead. Dann, wenn das Programm wächst oder jemand Threadpools effizienter implementiert hat, kann es sofort davon profitieren. Ich erwarte durch async Performancesprünge und erhöhte Skalierbarkeit, die mit jeder Prozessorgeneration noch zunehmen wird. Soweit ich weiß ist das auch einzigartig bei Programmiersprachen, dass der Compiler über das Threading entscheidet. Die Idee ist unglaublich genial.

    Sogar jetzt würde ich empfehlen ständig async zu benutzen. Es nützt nichts, aber schaden tut es auch nicht. Und in einem Jahr dann neu kompilieren und fetten kostenlosen(!) Performaceboost einsammeln.

    Moment!
    Ich hab mich bisher nicht wirklich mit Threading, std::async und sowas auseinander gesetzt, aber was du da sagst klingt erstens interessant und zweitens etwas zu schön um wahr zu sein oO

    Könnten wir das etwas diskutieren und auch Quellen dazu angeben?
    Denn wenn das stimmt, dann haben wir ja kaum mehr Probleme was Multithreading angeht



  • Also ich seh es immer noch so, dass der nicht zwischen single- und multithreading unterscheidet. Die einzige Sache ist, dass async synchron oder asynchron ausgeführt wird, also entweder im aktuellen Thread oder in einem nebenläufigen Thread.

    Multithreading in "Hochform" nutzt ja alle Kerne aus, dementsprechend also auch mindestens so viele Threads. Async führt ja keine wirkliche Parallelisierung hinzu (das Wort impliziert für mich parallele Abarbeitung EINER Aufgabe, also mindestens zwei Threads für das, was man async als Funktion übergibt).

    Das löst also soweit ich denke nur einen Teil der Probleme. Nett wäre das trotzdem, aber nur die Spitze vom Eisberg, so wie ich das sehe.



  • Skym0sh0 schrieb:

    ... und auch Quellen dazu angeben?

    http://channel9.msdn.com/Shows/The+Knowledge+Chamber/C-A-Language-for-Modern-Times Ungeduldige können zu 22:28 springen

    Eisflamme schrieb:

    Async führt ja keine wirkliche Parallelisierung hinzu (das Wort impliziert für mich parallele Abarbeitung EINER Aufgabe, also mindestens zwei Threads für das, was man async als Funktion übergibt).

    Ah, jetzt habe ichs verstanden. So genial ist async dann auch nicht, dass es magisch deinen Code analysiert und automatisch parallelisiert. Du musst schon von Hand async für alles Parallelisierbare schreiben. Aber es ist genial genug um den Code automatisch zu synchronisieren. Und das ist fast genau so gut.



  • Also ums nochmal zusammenzufassen:
    Bei parallelisierbaren Funktionen nutzen wir ab jetzt std::async und zur laufzeit entscheidet die Laufzeitumgebung wann welche CPU (bzw. welcher Core) diese Funktion laufen lässt - Ohne dass man als Programmeirer nochwas definieren muss oder irgendwelchen Mutexe, Semaphoren oder sonstwas einbauen muss. Und gleichzeitig skaliert diese Variante relativ beliebig hoch (insoweit überhaupt soviel Rechenpower gebraucht wird, in dem Beispielvideo war es ja nicht nötig, die Flipfunktion zu parallelisieren).

    Hab ich das so richtig verstanden?!? 😕 😮



  • Natürlich nicht.



  • Zur Compilezeit entscheidet der Compiler ob std::async parallel läuft oder nicht.
    Da es parallel laufen kann, müssen wir als Programmierer dafür sorgen, dass keine Race Conditions entstehen (Mutex etc.).



  • nwp3 schrieb:

    [...]

    Wirklich interessante Erklärung, danke auch für den Link zum Video.

    Nathan schrieb:

    Zur Compilezeit entscheidet der Compiler ob std::async parallel läuft oder nicht.

    Die Entscheidung kann durchaus zur Laufzeit fallen. Darum lässt der Standard der Implementierung bei den Flags launch::async|launch::deferred so viel Freiheit. Es könnte also durchaus sein, dass intelligente Implementierungen die aktuelle CPU-Auslastung berücksichtigen. Momentan sind sie leider noch relativ dumm (wie gesagt macht gcc einfach immer launch::deferred ).



  • Ist launch::async|launch::deferred der "Fix" für das Problem, dass launch::async z.B. keine Work-Stealing Scheduler zulässt, da der Standard bekloppterweise garantiert dass ein launch::async Task in seinem eigenen Thread ausgeführt wird?

    @nwp3:
    Ich verstehe deine Euphorie nicht ganz.

    Ein Programmierer sollte bei jedem noch so kleinen parallelisierbaren Codestück async + future::wait benutzen und der Compiler optimiert das zu einem normalen Funktionsaufruf, ohne Overhead. Dann, wenn das Programm wächst oder jemand Threadpools effizienter implementiert hat, kann es sofort davon profitieren. Ich erwarte durch async Performancesprünge und erhöhte Skalierbarkeit, die mit jeder Prozessorgeneration noch zunehmen wird. Soweit ich weiß ist das auch einzigartig bei Programmiersprachen, dass der Compiler über das Threading entscheidet. Die Idee ist unglaublich genial.

    Er.
    "Wenn es nix nützt dann schades es auch nicht" ist nicht richtig. Ne Future zu initialisieren ist alles andere als gratis. Nicht wenn der Compiler es schon "wegoptimiert", nur dann kann auch nichts mehr parallelisiert werden. Und wenn der Compiler es drinnen lässt, dann braucht man schon "Payloads" von etlichen 100 CPU-Zyklen um den Overhead wiedergutzumachen.
    Das

    Der Compiler sieht sich den Code an und entscheidet, ob sich der Overhead lohnt, und zwar viel besser, als der Programmierer es je könnte, insbesondere in Verbindung mit Profiling.

    ist leider auch ein unrealistischer Wunschtraum. Das kann kein Compiler jemals können. Weil der Compiler nämlich die Parameter nicht kennen kann mit der die Funktion aufgerufen wird (*).
    Und abgesehen davon behaupte ich, dass es sehr sehr viel Aufwand darstellen würde, einem Compiler beizubringen hier überhaupt irgendwas zu analysieren, und abhängig vom Ergebnis dann einen direkten Funktionsaufruf zu generieren oder eben einen asynchronen.

    Sich einfach zu denken "ah toll, da kümmert sich der Compiler drum, jippie!" ist zwar verlockend, aber in dem Fall mMn. total unrealistisch.

    *: Überleg dir mal wie ein "normaler" (single-threaded) Sortieralgorithmus aufgebaut ist. Da hast du typischerweise ein "if (count > limit) teile_und_herrsche(); else insertion_sort();" drinnen. Dieses "if" kann der Compiler unmöglich für dich einfügen. Nicht bei single-threaded, und genau so wenig wenn man es auf mehrere Threads aufteilt.



  • ps:

    nwp3 schrieb:

    Soweit ich weiß ist das auch einzigartig bei Programmiersprachen, dass der Compiler über das Threading entscheidet. Die Idee ist unglaublich genial.

    Es wäre einzigartig und genial, wenn es existieren würde bzw. überhaupt sinnvoll möglich wäre. Tut es aber nicht bzw. ist es nicht.

    Und... die ganze std::async Sache ist, verglichen mit manchen anderen Implementierungen, eher wenig mächtig und auch nicht so toll angenehm zu verwenden.

    Cilk (aus 1994!) hat z.B. die wesentlich angenehmere Syntax, und gibt dem Compiler bzw. der Runtime auch viel mehr Freiheiten - wodurch diverse Optimierungen viel einfacher (bzw. überhaupt erst möglich) werden.

    async in C# ist wesentlich mächtiger und hat ebenso eine angenehmere Syntax.

    Die Idee von Thread-Pools (und mehr ist std::async nicht), ist auch schon sehr sehr alt. Und es gibt auch schon genügend Implementierungen in diversen Libraries in diversen Sprachen. Das einzig neue oder besonders tolle an std::async ist das std:: .



  • Ich glaube dieses std::async kann man nicht ernsthaft verwenden. Zu unzuverlässig im Verhalten das Ding. IMHO nur was für Spielzeug-Anwendungen.



  • Also ich kann die allgemeine Ablehnung von async nicht nachvollziehen. Klar gibts da noch einige Mängel. Ein Mangel, der noch nicht genannt wurde, ist, dass es kein future.then gibt.

    hustbaer schrieb:

    Ne Future zu initialisieren ist alles andere als gratis. Nicht wenn der Compiler es schon "wegoptimiert", nur dann kann auch nichts mehr parallelisiert werden. Und wenn der Compiler es drinnen lässt, dann braucht man schon "Payloads" von etlichen 100 CPU-Zyklen um den Overhead wiedergutzumachen.

    Völlig korrekt. Und ich behaupte Compiler werden 2014/15 diese Entscheidung halbwegs ordentlich umsetzen.

    hustbaer schrieb:

    Ist launch::async|launch::deferred der "Fix" für das Problem, dass launch::async z.B. keine Work-Stealing Scheduler zulässt, da der Standard bekloppterweise garantiert dass ein launch::async Task in seinem eigenen Thread ausgeführt wird?

    Würde ich nicht sagen. "Eigener Thread" wiederspricht nicht Threadpool. Aber launch::async ist ziemlich bekloppt, ja. Nur kann man ohne launch::async Leute wie

    Cilk-Fan schrieb:

    Ich glaube dieses std::async kann man nicht ernsthaft verwenden. Zu unzuverlässig im Verhalten das Ding. IMHO nur was für Spielzeug-Anwendungen.

    niemals zufrieden stellen. Mit launch::async verhält sich std::async genau so dumm und "zuverlässig" wie jede andere create_thread-Funktion. Mit der Zeit wird man erkennen wie doof launch::async ist und es einfach weglassen.

    hustbaer schrieb:

    Cilk (aus 1994!) hat z.B. die wesentlich angenehmere Syntax, und gibt dem Compiler bzw. der Runtime auch viel mehr Freiheiten - wodurch diverse Optimierungen viel einfacher (bzw. überhaupt erst möglich) werden.

    async in C# ist wesentlich mächtiger und hat ebenso eine angenehmere Syntax.

    Beispiele bitte, mit Hinweis was da optimiert werden kann, was async nicht kann.

    hustbaer schrieb:

    [async so umsetzten wie nwp3 geschrieben hat ist anstrengend/unmöglich]

    Der Compiler muss für beliebige Funktionen und Eingaben entscheiden, ob diese Funktion mehr oder weniger als 100 Zyklen (obda. der Overhead für einen Thread) brauchen wird. Die Eingabe kennt er, weil er es zur Laufzeit macht. Ohne Rekursion kann man Assemblerbefehle zählen. Syscalls kosten 100, bei Verzweigungen nimmt man den längsten Pfad, bei Schleifen... muss man die Anzahl der Schleifendurchläufe irgendwie aus dem Code und der Eingabe ableiten. Bei Zufallszahlen hat man wohl keine Chance und machts einfach asynchron. Bei Rekursion ist das schwieriger. Längsten Pfad der Funktion ohne Rekursion bestimmen, Anzahl der Rekursionen aus der Eingabe abschätzen... Ja, es ist schwierig. Aber nicht unmöglich, da ist kein Halteproblem oder so drin, das kriegt man alles durch Simulation raus. Außerdem muss man nicht immer richtig liegen.
    Wenn es jemand hinkriegt ist es jedenfalls toll, wenn nicht, dann hat man immernoch dieselbe Qualität einer "zuverlässigen" Threadfunktion auf eine plattformunabhängige Art.



  • nwp3 schrieb:

    hustbaer schrieb:

    Ne Future zu initialisieren ist alles andere als gratis. Nicht wenn der Compiler es schon "wegoptimiert", nur dann kann auch nichts mehr parallelisiert werden. Und wenn der Compiler es drinnen lässt, dann braucht man schon "Payloads" von etlichen 100 CPU-Zyklen um den Overhead wiedergutzumachen.

    Völlig korrekt. Und ich behaupte Compiler werden 2014/15 diese Entscheidung halbwegs ordentlich umsetzen.

    Ich wette dagegen.

    nwp3 schrieb:

    hustbaer schrieb:

    Ist launch::async|launch::deferred der "Fix" für das Problem, dass launch::async z.B. keine Work-Stealing Scheduler zulässt, da der Standard bekloppterweise garantiert dass ein launch::async Task in seinem eigenen Thread ausgeführt wird?

    Würde ich nicht sagen. "Eigener Thread" wiederspricht nicht Threadpool.

    Ich hab' ja auch nicht Thread-Pool geschrieben sondern Work-Stealing Scheduler. Und bei Work-Stealing kann es sein dass ein und der selbe Pool-Thread erst an Teil 1 von Task A arbeitet, dann an Task B und dann wieder mit Teil 2 von Task A weiter macht.

    Wenn der Standard nun aber "eigener Thread" sagt, dann heisst das für mich, dass man sich darauf verlassen kann, dass Task A und Task B nicht im selben Thread ausgeführt werden. Ein Programm das sich auf diese Garantie verlässt, z.B. indem es thread-local Variablen verwendet, könnte mit einem Work-Stealing Scheduler also Probleme bekommen.

    Bzw. genaugenommen heisst "eigener Thread" für mich sogar dass es ein "frischer" Thread ist. Also einer dessen thread-local Variablen bei Start des Tasks alle Default-Werte haben.

    nwp3 schrieb:

    hustbaer schrieb:

    Cilk (aus 1994!) hat z.B. die wesentlich angenehmere Syntax, und gibt dem Compiler bzw. der Runtime auch viel mehr Freiheiten - wodurch diverse Optimierungen viel einfacher (bzw. überhaupt erst möglich) werden.

    async in C# ist wesentlich mächtiger und hat ebenso eine angenehmere Syntax.

    Beispiele bitte, mit Hinweis was da optimiert werden kann, was async nicht kann.

    Cilk kennt keine Futures, und ermöglicht es einem nicht das (bei Cilk implizite) "Wait Handle" (=die Future) irgendwie zu transferieren. Soweit ich weiss muss eine Funktion auch zwingend auf alle "gespawnten" Funktionen warten bevor sie zurückkehren kann. (Bzw. wenn nicht ist trivial festzustellen ob sie es tut - wenn in allen Pfaden ein "sync" vorkommt tut sie es, und sonst eben nicht.)
    Dadurch wird es möglich bestimmte Dinge auf dem Stack der aufrufenden Funktion zu allokieren. z.B. die gespawnten Funktoren und die List-Nodes mit der diese in die Liste der wartenden Tasks eingeklinkt werden.

    Ich weiss allerdings nicht ob diese Dinge wirklich so umgesetzt sind. So lange keine anderen Gründe dagegen sprechen, würde ich es auf jeden Fall so umsetzen.

    nwp3 schrieb:

    hustbaer schrieb:

    [async so umsetzten wie nwp3 geschrieben hat ist anstrengend/unmöglich]

    Der Compiler [...blaaaaaah]

    Mir ist schon klar dass man hier diverse Heuristiken verwenden kann.
    Du hast aber behauptet dass der Compiler es besser abschätzen kann als jeder Programmierer. Und halte ich für Unsinn.
    Wenn man einfach überall alles mit async macht eine Heuristik entscheiden lässt, ist mMn. die Wahrscheinlichkeit hoch, dass das Programm das rauskommt schlechter performt, als ein Programm eines halbwegs fähigen Programmierers der keinen "Compiler-Support" für diese Unterscheidung bekommt.

    D.h. nicht dass Compiler-Support grundsätzlich abzulehnen ist. Dinge, von denen der Compiler klar beweisen kann, dass sie immer besser laufen wenn sie einfach synchron ausgeführt werden, darf er gerne synchron machen. Alles andere muss er mMn. aber in Ruhe lassen. Und das bedeutet eben dass man sich trotzdem selbst Gedanken darüber machen muss.



  • async benutzt keinen Threadpool sondern immer frische Threads? WTF?



  • omfg schrieb:

    async benutzt keinen Threadpool sondern immer frische Threads? WTF?

    Nein, obwohl der Standard AFAIK eben diese problematischen Passagen enthält, die IMO streng genommen nur frische Threads wirklich erfüllen.



  • Was ist das Problem am Flag std::launch::async per se? So wie ich dich, hustbaer, verstehe, schränkt es die Implementierung durch die erzwungene Asynchronität zu stark ein. Aber gibt es nicht Situationen, in denen man das gerade will?

    Nicht zuletzt, weil der Standard bei async|deferred auch konservative Strategien wie "immer deferred " (aktuell beim g++) zulässt. Ist zwar eine Frage von QoI, aber im Endeffekt machen solche Implementierungen async() nutzlos und zwingen einen, wieder selbst Threads zu verwalten...



  • Nexus schrieb:

    Was ist das Problem am Flag std::launch::async per se? So wie ich dich, hustbaer, verstehe, schränkt es die Implementierung durch die erzwungene Asynchronität zu stark ein. Aber gibt es nicht Situationen, in denen man das gerade will?

    Die erzwungene Asynchronität ist gut, aber die Vorgabe "eigener Thread" finde ich zu einengend. Gibt ja wie schon erwähnt viele Möglichkeiten Tasks asynchron ausführen zu lassen, wobei nicht überall jeder Task nen eigenen Thread bekommt.

    Aber wurst. Das wirs sicher noch nachgebessert. Würde mich wundern wenn das auf länger so bleibt.


Anmelden zum Antworten