2017-01-31 9 views
13

Właśnie przeczytałem:Leniwe oceny w C++ 14/17 - tylko lambdy, a także futures itp.?

Lazy Evaluation in C++

i zauważył, że to trochę stary i większość odpowiedzi uważają pre-2011 C++. W dzisiejszych czasach mamy składniowe lambdy, które można nawet wywnioskować typ zwracany, ocena tak leniwy wydaje się sprowadzać po prostu przekazując je wokół: Zamiast

auto x = foo(); 

wykonać

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

a następnie ocenić, kiedy/gdzie musisz:

auto x = unevaluted_x(); 

Wydaje się, że nie ma w tym nic więcej. Jednak jeden z modeli answers there sugeruje użycie asynchronicznego uruchamiania przy użyciu futures. Czy ktoś może wyjaśnić, dlaczego/jeśli przyszłość jest ważna dla leniwych prac ewaluacyjnych, w C++ lub bardziej abstrakcyjnie? Wydaje się, że futures można bardzo dobrze ocenić z niecierpliwością, ale po prostu, powiedzmy, w innym wątku i być może z mniejszym priorytetem niż to, co je stworzyło; i tak czy inaczej, powinna być zależna od wdrożenia, prawda?

Czy są też inne współczesne konstrukcje C++, o których warto pamiętać w kontekście leniwej oceny?

+1

kontrakty futures służą do oczekiwania na wynik niektórych procesów (prawdopodobnie asynchronicznych). Są jednorazowe i dość ciężkie. Jeśli szukasz leniwej oceny w tym samym wątku, prawdopodobnie nie to, czego potrzebujesz. Tworzona jest biblioteka o nazwie boost.outcome. Jest to w zasadzie lekki futures (nie przeznaczony do pracy z nitkami krzyżowymi). Jeśli chcesz wielokrotnie wywoływać swoją leniwą funkcję, prawdopodobnie odpowiedni jest obiekt funkcji lub lambda. Możesz również zajrzeć do boost.hana lub podobnego. –

Odpowiedz

12

Kiedy piszesz

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

każdym razem, gdy chcą uzyskać wartość (podczas rozmowy unevaluated_x) to obliczone, marnowania zasobów obliczeniowych. Tak więc, aby pozbyć się tej nadmiernej pracy, dobrze jest śledzić, czy lambda została już wywołana (może w innym wątku lub w innym miejscu w bazie kodu). Aby to zrobić, potrzebujemy opakowania o wartości 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; 
}; 

Należy pamiętać, że ten kod jest tylko przykładem i nie jest bezpieczny dla wątków.

Ale zamiast wymyślania koła, można po prostu użyć std::shared_future:

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

wymaga mniej kodu do zapisu i obsługuje kilka innych funkcji (jak sprawdzić, czy wartość została już obliczone, bezpieczeństwo nici, itp).

Jest dodaje się tekst w standardowym [futures.async, (3,2)]:

Jeśli launch::deferred jest w polityce, sklepy DECAY_COPY(std::forward<F>(f)) i DECAY_COPY(std::forward<Args>(args))... we wspólnym państwie. Te kopie f i args stanowią odroczoną funkcję. Inwokacja funkcji odroczonej ocenia INVOKE(std::move(g), std::move(xyz)), gdzie g jest zapisaną wartością DECAY_COPY(std::forward<F>(f)) i xyz jest przechowywaną kopią DECAY_COPY(std::forward<Args>(args)).... Każda wartość zwracana jest przechowywana jako wynik w stanie współdzielonym. Każdy wyjątek propagowany po wykonaniu opóźnionej funkcji jest przechowywany jako wyjątkowy wynik w stanie współdzielonym. Stan wspólny nie jest gotowy do ukończenia funkcji .Pierwsze wywołanie funkcji czasu bez oczekiwania (30.6.4) na asynchronicznym obiekcie zwracającym, odnoszącym się do tego stanu współużytkowanego, powinno wywołać funkcję odroczoną w wątku, który wywołał funkcję oczekiwania. Po rozpoczęciu oceny INVOKE(std::move(g),std::move(xyz)) funkcja nie jest już uważana za odroczoną. [Uwaga: Jeśli ta zasada ma wartość określona razem z innymi zasadami, na przykład podczas korzystania z wartości strategii launch::async | launch::deferred, implementacje powinny odroczyć wywołanie lub wybrać zasadę, jeśli nie można efektywniej wykorzystać współbieżności. -lub wstępna]

Masz więc gwarancję, że obliczenia nie zostaną wywołane, zanim będzie to konieczne.

+0

(1) Twoja cached_lambda nie jest bezpieczna dla wątków w tym sensie, że możesz wywoływać lambdę dwukrotnie z różnych wątków. Ponadto zapomniałeś ustawić 'calcul' na 'true' (przynajmniej dokona edycji tej części). (2) Ale jakie mam gwarancje dotyczące tego, kiedy przyszłość faktycznie zostanie wykonana? Skąd mam wiedzieć, że jest leniwy? – einpoklum

+1

@einpoklum "zadanie jest wykonywane na wątku wywołującym przy pierwszym żądaniu jego wyniku (ocena leniwy)" - cytowane z http://en.cppreference.com/w/cpp/thread/launch Zaktualizuje odpowiedź po znalezieniu potwierdzenie w standardzie. – alexeykuzmin0

+0

@einpoklum Masz rację (1) i jest to dodatkowy powód do używania 'std :: future'. – alexeykuzmin0

4

Jest tu kilka rzeczy.

Applicative order ewaluacja oznacza ocenę argumentów przed przekazaniem ich do funkcji. Normal order ewaluacja oznacza przekazanie argumentów do funkcji przed ich oceną.

Ocena normalnego zamówienia ma tę zaletę, że niektóre argumenty nigdy nie są oceniane, a wada, że ​​niektóre argumenty są oceniane wielokrotnie.

Lazy ocena zwykle oznacza normal order + memoization. Odłóż ocenę w nadziei, że wcale nie musisz oceniać, ale jeśli musisz, zapamiętaj wynik, więc musisz zrobić to tylko raz. Ważną częścią jest ocenianie terminu nigdy lub raz, najprostszym mechanizmem tego jest zapamiętywanie.

Model promise/future znów jest inny. Chodzi o to, aby rozpocząć ocenę, prawdopodobnie w innym wątku, gdy tylko dostępna jest wystarczająca ilość informacji. Następnie odkładasz wynik tak długo, jak to możliwe, aby zwiększyć szanse, że jest już dostępna.


Model ma interesującą synergię z leniwą oceną. Strategia idzie:

  1. Postpone ocenę aż pewnością będą potrzebne wynikiem
  2. Zacznij ocenę dzieje w innym wątku
  3. Czy jakieś inne rzeczy
  4. Gwint tło uzupełnia i zapisuje wynik gdzieś
  5. Początkowy wątek powoduje odczytanie wyniku:

Zapamiętywanie może być starannie wprowadzone, gdy wynik jest tworzony przez wątek tła.

Pomimo synergii między tymi dwoma, nie są one tym samym pojęciem.

+0

Cóż, a co z ['std :: launch :: deferred] (http://en.cppreference.com/w/cpp/thread/launch) dla kontraktów futures, co nie rozpoczyna się tak jak zakładałem w pytaniu, ale raczej czeka, aż będą potrzebne? To także część lub aspekt modelu obietnicy/przyszłości. ... lub - czy tak jest w przypadku implementacji C++, a nie w literaturze? – einpoklum

+0

A co z tym w szczególności? std :: launch oferuje asynchronizację lub leniwą ocenę, ale nie obsługuje async-eval-only-when-necessary za pośrednictwem bieżącego API. –

+0

W programowaniu funkcjonalnym, wydaje mi się, że słyszałem to, co nazywacie "zapamiętywaniem", zamiast tego nazywać się "dzieleniem się". Dla porównania "zapamiętywanie" jest techniką służącą do zapisywania wartości zwracanych funkcji, aby nie zostały ponownie obliczone, na przykład 'fib (n) = fib (n-2) + fib (n-1)' w algorytmie liniowym (zamiast egzupcji). Więc "zapamiętywanie" przypomina bardziej dynamiczne programowanie, z tego, co słyszałem. Mimo to masz rację w tym sensie, że oba podejścia zapisują wynik obliczeń w pamięci podręcznej, do której można uzyskać dostęp w późniejszym czasie. (+1) – chi