2017-01-31 3 views
13

Ich habe gerade gelesen:Faule Bewertung in C++ 14/17 - nur Lambdas oder auch Futures etc.?

Lazy Evaluation in C++

und bemerkte, es ist eine Art alt ist und die meisten Antworten betrachten Pre-2011 C++. In diesen Tagen wir syntaktische lambdas haben, die sogar den Rückgabetyp ableiten kann, so faul Auswertung scheint nach unten zu kochen, um sie nur um vorbei: Statt

auto x = foo(); 

Sie

auto unevaluted_x = []() { return foo(); }; 

ausführen und dann bewerten, wenn/wo Sie brauchen:

auto x = unevaluted_x(); 

Scheint so, als gäbe es nichts mehr. Einer der answers there schlägt jedoch vor, futures mit asynchronem Start zu verwenden. Kann jemand erläutern, warum/wenn Futures für Lazy-Evaluation-Arbeit, in C++ oder abstrakter sind? Es scheint, als ob Futures sehr wohl eifrig bewertet werden könnten, aber einfach, sagen wir, in einem anderen Thread und vielleicht mit weniger Priorität als alles, was sie geschaffen hat; und sowieso sollte es umsetzungsabhängig sein, oder?

Gibt es auch andere moderne C++ - Konstrukte, die im Zusammenhang mit einer faulen Auswertung sinnvoll sind?

+1

Futures sind für das Warten auf das Ergebnis eines (möglicherweise asynchronen) Prozesses. Sie sind single-use und ziemlich schwer. Wenn Sie im selben Thread nach fauler Bewertung suchen, ist es wahrscheinlich nicht das, was Sie brauchen. Es gibt eine Bibliothek namens boost.outcome, die entwickelt wird. Es ist im Wesentlichen leichte Futures (nicht für Cross-Thread-Arbeit). Wenn Sie Ihre Lazy-Funktion wiederholt aufrufen wollen, dann ist wahrscheinlich ein Funktionsobjekt oder Lambda geeignet. Vielleicht möchten Sie auch boost.hana oder ähnliches sehen. –

Antwort

12

Wenn Sie

auto unevaluted_x = []() { return foo(); }; 
... 
auto x = unevaluted_x(); 

Jedesmal, wenn Sie den Wert erhalten möchten, schreiben (wenn Sie unevaluated_x nennen) berechnet ist, verschwenden Rechenressourcen. Also, um diese übermäßige Arbeit loszuwerden, ist es eine gute Idee zu verfolgen, ob das Lambda bereits aufgerufen wurde (vielleicht in einem anderen Thread oder an einer anderen Stelle in der Codebasis). Um dies zu tun, müssen wir einige Wrapper um Lambda:

template<typename Callable, typename Return> 
class memoized_nullary { 
public: 
    memoized_nullary(Callable f) : function(f) {} 
    Return operator()() { 
     if (calculated) { 
      return result; 
     } 
     calculated = true; 
     return result = function(); 
    } 
private: 
    bool calculated = false; 
    Return result; 
    Callable function; 
}; 

Bitte beachten Sie, dass dieser Code ist nur ein Beispiel und ist nicht Thread-sicher.

Aber statt das Rad neu zu erfinden, könnte man einfach std::shared_future verwenden:

auto x = std::async(std::launch::deferred, []() { return foo(); }).share(); 

Dieser weniger Code erfordert einige andere Funktionen zu schreiben und unterstützt (wie prüfen, ob hat sich der Wert bereits berechnet worden ist, Thread-Sicherheit, etc).

Es gibt den folgenden Text in dem Standard [futures.async, (3.2)]:

Wenn launch::deferred in Politik, speichert DECAY_COPY(std::forward<F>(f)) und DECAY_COPY(std::forward<Args>(args))... im freigegebenen Zustand gesetzt. Diese Kopien von f und args bilden eine verzögerte Funktion. Der Aufruf der verzögerten Funktion wertet INVOKE(std::move(g), std::move(xyz)) aus, wobei g der gespeicherte Wert DECAY_COPY(std::forward<F>(f)) ist und xyz die gespeicherte Kopie von DECAY_COPY(std::forward<Args>(args)).... ist. Jeder Rückgabewert wird als Ergebnis im gemeinsamen Status gespeichert. Jede Ausnahme, die von der Ausführung der zurückgestellten -Funktion propagiert wird, wird als Ausnahmeergebnis im gemeinsam genutzten Zustand gespeichert. Der gemeinsame Status wird nicht bereit gemacht, bis die Funktion abgeschlossen ist.Der erste Aufruf einer nicht zeitgesteuerten Wartefunktion (30.6.4) an einem asynchronen Rückgabeobjekt, das sich auf diesen gemeinsamen Zustand bezieht, muss die zurückgestellte Funktion im Thread aufrufen, der die Wartefunktion aufgerufen hat. Sobald die Auswertung von INVOKE(std::move(g),std::move(xyz)) beginnt, wird die Funktion nicht länger als verzögert angesehen. [Hinweis: Wenn diese Richtlinie zusammen mit anderen Richtlinien angegeben wird, z. B. bei Verwendung eines Richtlinienwerts launch::async | launch::deferred, sollten Implementierungen den Aufruf oder die Auswahl der Richtlinie verzögern, wenn kein Concurrency mehr effektiv ausgenutzt werden kann. -end note]

Sie haben also eine Garantie, dass die Berechnung nicht aufgerufen wird, bevor sie benötigt wird.

+0

(1) Ihre cached_Lambda ist nicht Thread-sicher, in dem Sinne, dass Sie das Lambda möglicherweise zweimal von verschiedenen Threads aufrufen. Außerdem haben Sie vergessen, "berechnet" auf "wahr" zu setzen (wird diesen Teil zumindest bearbeiten). (2) Aber welche Garantien habe ich, wenn die Zukunft tatsächlich ausgeführt wird? Woher weiß ich, dass es tatsächlich faul ist? – einpoklum

+1

@einpoklum "die Aufgabe wird auf dem aufrufenden Thread ausgeführt, wenn sie das erste Mal angefordert wird (Lazy Evaluation)" - zitiert von http://en.cppreference.com/w/cpp/thread/launch Aktualisiert die Antwort nach dem Finden eine Bestätigung im Standard. – alexeykuzmin0

+0

@einpoklum Sie haben Recht mit (1), und es ist ein zusätzlicher Grund, 'std :: future' zu ​​verwenden. – alexeykuzmin0

4

Es gibt ein paar Dinge hier los.

Applicative order Auswertung bedeutet, Argumente zu bewerten, bevor sie in eine Funktion übergeben werden. Normal order Auswertung bedeutet, dass die Argumente in eine Funktion übergeben werden, bevor sie ausgewertet werden.

Normale Reihenfolge Auswertung hat den Vorteil, dass einige Argumente nie ausgewertet werden und der Nachteil, dass einige Argumente immer wieder ausgewertet werden.

Lazy Bewertung bedeutet in der Regel normal order + memoization. Verschieben Sie die Auswertung in der Hoffnung, dass Sie überhaupt keine Auswertung vornehmen müssen, aber wenn Sie es müssen, denken Sie an das Ergebnis, so dass Sie es nur einmal tun müssen. Der wichtige Teil ist die Bewertung eines Begriffs nie oder nur selten, Memoisierung ist der einfachste Mechanismus, um dies zu erreichen. Das Modell promise/future ist wieder anders. Die Idee hier ist, eine Bewertung zu starten, wahrscheinlich in einem anderen Thread, sobald Sie genügend Informationen zur Verfügung haben. Sie lassen dann das Ergebnis so lange wie möglich zurück, um die Chancen zu verbessern, dass es bereits verfügbar ist.


Das promise/future Modell hat einige interessante Synergie mit lazy evaluation. Die Strategie geht:

  1. Auswertung aufschieben, bis das Ergebnis auf jeden Fall benötigt werden
  2. die Auswertung starten
  3. in einem anderen Thread geht
  4. einige andere Sachen tun
  5. Der Hintergrund-Thread beendet und speichert das Ergebnis irgendwo
  6. Der erste Thread ruft das Ergebnis ab

Memoization kann ordentlich als Ergebnis eingefügt werden wird vom Hintergrundthread erzeugt.

Trotz der Synergie zwischen den beiden sind sie nicht das gleiche Konzept.

+0

Nun, was ist mit dem ['std :: launch :: deferred] (http://en.cppreference.com/w/cpp/thread/launch) für Futures, das die Bewertung nicht so startet, wie ich es angenommen habe in der Frage, sondern wartet, bis sie gebraucht werden? Das ist auch ein Teil oder ein Aspekt des Versprechens/Zukunftsmodells. ... oder - ist das eher in der C++ - Implementierung als in der Literatur? – einpoklum

+0

Was ist damit besonders? std :: launch bietet eine asynchrone oder faule Auswertung, unterstützt aber async-eval-only-not-necess über die aktuelle API nicht. –

+0

In funktionalen Programmierung, ich glaube, ich hörte, was Sie "Memoization" nennen, stattdessen "Sharing" genannt werden. Im Vergleich ist "Memoisierung" eine Technik, um Funktionsrückgabewerte zu speichern, so dass sie nicht erneut neu berechnet werden, möglicherweise z. "fib (n) = fib (n-2) + fib (n-1)" in einen linearen Algorithmus (anstelle einer exponentiellen). Also "Memoization" ist mehr wie dynamische Programmierung, von dem, was ich gehört habe. Dennoch sind Sie in dem Sinne korrekt, dass beide Ansätze das Berechnungsergebnis in einem Cache speichern, auf den später zugegriffen werden kann. (+1) – chi