Co To Jest Optymalizacja Połączeń Ogonowych?

Bardzo prosto, co to jest optymalizacja połączeń ogonowych? Dokładniej, czy ktoś może pokazać małe fragmenty kodu, gdzie można go zastosować, a gdzie nie, z wyjaśnieniem dlaczego?

Author: ks1322, 2008-11-22

8 answers

Tail-call optimization pozwala uniknąć przydzielania nowej ramki stosu dla funkcji, ponieważ funkcja wywołująca po prostu zwróci wartość, którą otrzymuje z wywołanej funkcji. Najczęstszym zastosowaniem jest rekurencja ogonowa, gdzie funkcja rekurencyjna napisana w celu skorzystania z optymalizacji ogonowej może używać stałej przestrzeni stosu.

Scheme jest jednym z niewielu języków programowania, które gwarantują w specyfikacji, że każda implementacja musi zapewnić taką optymalizację (JavaScript również, zaczynając od ES6) , więc oto dwa przykłady funkcji czynnikowej w schemacie:

(define (fact x)
  (if (= x 0) 1
      (* x (fact (- x 1)))))

(define (fact x)
  (define (fact-tail x accum)
    (if (= x 0) accum
        (fact-tail (- x 1) (* x accum))))
  (fact-tail x 1))

Pierwsza funkcja nie jest rekurencyjna, ponieważ gdy wywołanie rekurencyjne jest wykonane, funkcja musi śledzić mnożenie, które musi zrobić z wynikiem po zakończeniu wywołania. Jako taki stos wygląda następująco:

(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 (* 1 (fact 0))))
(* 3 (* 2 (* 1 1)))
(* 3 (* 2 1))
(* 3 2)
6

Natomiast ślad stosu dla czynnika rekurencyjnego ogona wygląda następująco:

(fact 3)
(fact-tail 3 1)
(fact-tail 2 3)
(fact-tail 1 6)
(fact-tail 0 6)
6

Jak widzisz, my wystarczy tylko śledzić taką samą ilość danych dla każdego połączenia do fact-tail, ponieważ po prostu zwracamy wartość, którą otrzymujemy na sam szczyt. Oznacza to, że nawet gdybym miał zadzwonić (fakt 1000000), potrzebuję tylko tyle miejsca co (fakt 3). Tak nie jest w przypadku faktu non-tail-recursive i jako takie duże wartości mogą powodować przepełnienie stosu.

 571
Author: Kyle Cronin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-01 20:31:20

Przejdźmy przez prosty przykład: funkcja czynnikowa zaimplementowana w C.

Zaczynamy od oczywistej definicji rekurencyjnej

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    return n * fac(n - 1);
}

Funkcja kończy się wywołaniem ogonowym, jeśli ostatnia operacja przed zwróceniem funkcji jest kolejnym wywołaniem funkcji. Jeśli to wywołanie wywoła tę samą funkcję, jest rekurencyjne.

Chociaż fac() wygląda na rekurencyjny na pierwszy rzut oka, to nie jest tak, jak to, co się naprawdę dzieje

unsigned fac(unsigned n)
{
    if (n < 2) return 1;
    unsigned acc = fac(n - 1);
    return n * acc;
}

Ie ostatnią operacją jest mnożenie, a nie wywołanie funkcji.

Możliwe jest jednak przepisanie fac() jako rekurencyjny, przekazując nagromadzoną wartość w dół łańcucha wywołań jako dodatkowy argument i przekazując tylko końcowy wynik ponownie jako wartość zwracaną:

unsigned fac(unsigned n)
{
    return fac_tailrec(1, n);
}

unsigned fac_tailrec(unsigned acc, unsigned n)
{
    if (n < 2) return acc;
    return fac_tailrec(n * acc, n - 1);
}
Dlaczego to jest przydatne? Ponieważ natychmiast wracamy po wywołaniu tail, możemy odrzucić poprzedni ramkę stackframe przed wywołaniem funkcji w pozycji tail, lub, w przypadku funkcji rekurencyjnych, ponownie użyć ramki stackframe tak jak jest.

Optymalizacja wywołania ogonowego przekształca nasz kod rekurencyjny w

unsigned fac_tailrec(unsigned acc, unsigned n)
{
TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

To można wkleić do fac() i docieramy do

unsigned fac(unsigned n)
{
    unsigned acc = 1;

TOP:
    if (n < 2) return acc;
    acc = n * acc;
    n = n - 1;
    goto TOP;
}

Co jest równoważne

unsigned fac(unsigned n)
{
    unsigned acc = 1;

    for (; n > 1; --n)
        acc *= n;

    return acc;
}

Jak widzimy tutaj, wystarczająco zaawansowany optymalizator może zastąpić rekursję ogonową iteracją, która jest o wiele bardziej efektywna, ponieważ unikasz narzutu wywołania funkcji i używasz tylko stałej ilości miejsca na stosie.

 452
Author: Christoph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-08 13:57:50

TCO (Tail Call Optimization) to proces, w którym inteligentny kompilator może wykonać wywołanie funkcji i nie zająć dodatkowego miejsca na stosie. Jedyną sytuacją, w której tak się dzieje, jest sytuacja, w której ostatnia Instrukcja wykonywana w funkcji f jest wywołaniem funkcji g (Uwaga: g może być f ). Kluczem jest to, że f nie potrzebuje już przestrzeni stosu - po prostu wywołuje g, a następnie zwraca to, co g zwróci. W tym przypadku optymalizacja może być wykonane, że g po prostu działa i zwraca jakąkolwiek wartość będzie to miało do rzeczy, która nazywa się f.

Ta optymalizacja może sprawić, że wywołania rekurencyjne zajmą stałą przestrzeń stosu, a nie eksplodują.

Przykład: ta funkcja czynnikowa nie jest TCOptimizable:

def fact(n):
    if n == 0:
        return 1
    return n * fact(n-1)

Ta funkcja robi rzeczy poza wywołaniem innej funkcji w instrukcji return.

Poniższa funkcja jest TCOptimizable:

def fact_h(n, acc):
    if n == 0:
        return acc
    return fact_h(n-1, acc*n)

def fact(n):
    return fact_h(n, 1)

To dlatego, że ostatnia rzecz, która wydarzy się w którymś z tych funkcja polega na wywołaniu innej funkcji.

 151
Author: Claudiu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-23 14:18:57

Prawdopodobnie najlepszym opisem wysokiego poziomu, jaki znalazłem dla wywołań ogona, rekurencyjnych wywołań ogona i optymalizacji wywołań ogona, jest post na blogu

"co to do cholery jest:"

Dan Sugalski. O optymalizacji wywołania ogona pisze:

Rozważmy przez chwilę tę prostą funkcję:

sub foo (int a) {
  a += 15;
  return bar(a);
}

Więc co możesz zrobić, a raczej kompilator języka? Cóż, co może zrobić, to przekształcić kod widoku return somefunc(); w sekwencję niskiego poziomu pop stack frame; goto somefunc();. W naszym przykładzie oznacza to, że zanim wywołamy bar, foo czyści się, a następnie, zamiast wywoływać bar jako podprogram, wykonujemy operację niskiego poziomu goto na początek bar. Foo jest już wyczyszczone ze stosu, więc kiedy bar zaczyna wygląda to tak, jakby ktoś wywołał foo naprawdę wywołał bar, a kiedy bar zwraca jego wartość, zwraca ją bezpośrednio temu, kto wywołał foo, zamiast zwracać ją do foo, który następnie zwróci ją do swojego serwera. dzwoniący.

I o rekurencji ogonowej:

Rekurencja ogonowa ma miejsce, gdy funkcja, jako ostatnia operacja, zwróci wynik wywołania samego siebie . Rekurencja ogonowa jest łatwiejsza do opanowania bo zamiast przeskoczyć na początek jakiegoś przypadkowego funkcja gdzieś, po prostu zrobić goto powrót do początku siebie, co jest cholernie prostą rzeczą do zrobienia.

Tak, że to:

sub foo (int a, int b) {
  if (b == 1) {
    return a;
  } else {
    return foo(a*a + a, b - 1);
  }

Po cichu się odwraca do:

sub foo (int a, int b) {
  label:
    if (b == 1) {
      return a;
    } else {
      a = a*a + a;
      b = b - 1;
      goto label;
   }
To, co lubię w tym opisie, to to, jak zwięzły i łatwy jest do uchwycenia dla osób wywodzących się z imperatywnego środowiska językowego (C, C++, Java).]}
 46
Author: btiernay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-12 20:57:28

Zauważ przede wszystkim, że nie wszystkie języki go obsługują.

TCO stosuje się do specjalnego przypadku rekurencji. Chodzi o to, że jeśli ostatnią rzeczą jaką wykonujesz w funkcji jest wywołanie samej siebie (np. wywołanie samej siebie z pozycji "ogona"), może to być zoptymalizowane przez kompilator tak, aby działał jak iteracja zamiast standardowej rekursji.

Widzisz, normalnie podczas rekurencji, runtime musi śledzić wszystkie rekurencyjne wywołania, tak aby po powrocie mogło zostać wznowione przy poprzednim zadzwoń i tak dalej. (Spróbuj ręcznie zapisać wynik rekurencyjnego wywołania, aby uzyskać wizualny obraz tego, jak to działa.) Śledzenie wszystkich połączeń zajmuje miejsce, co staje się znaczące, gdy funkcja wywołuje się dużo. Ale z TCO może po prostu powiedzieć " wróć do początku, tylko tym razem zmień wartości parametrów na te nowe."Może to zrobić, ponieważ nic po wywołaniu rekurencyjnym nie odnosi się do tych wartości.

 12
Author: J Cooper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-22 07:09:41

Zobacz tutaj:

Http://tratt.net/laurie/tech_articles/articles/tail_call_optimization

Jak zapewne wiesz, rekurencyjne wywołania funkcji mogą siać spustoszenie na stosie; łatwo jest szybko wyczerpać przestrzeń stosu. Tail Call optimization jest sposobem, w jaki można utworzyć rekurencyjny algorytm stylu, który wykorzystuje stałą przestrzeń stosu, dlatego nie rośnie i nie rośnie i dostajesz błędy stosu.

 6
Author: BobbyShaftoe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-22 07:05:02
  1. Powinniśmy upewnić się, że w samej funkcji nie ma instrukcji goto .. wywołanie funkcji jest ostatnią rzeczą w funkcji callee.

  2. Rekurencje wielkoskalowe mogą używać tego do optymalizacji, ale w małej skali, Instrukcja narzutu na wywołanie funkcji jako wywołanie ogonowe zmniejsza rzeczywisty cel.

  3. TCO może powodować wiecznie działającą funkcję:

    void eternity()
    {
        eternity();
    }
    
 4
Author: grillSandwich,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-20 11:30:25

Podejście funkcji rekurencyjnych ma problem. Buduje stos wywołań O (n), co sprawia, że nasz całkowity koszt pamięci O(n). To sprawia, że jest podatny na błąd przepełnienia stosu, w którym stos wywołań staje się zbyt duży i kończy się miejsce. System optymalizacji kosztów ogonowych (TCO). Gdzie może zoptymalizować funkcje rekurencyjne, aby uniknąć budowania wysokiego stosu wywołań, a tym samym oszczędza koszty pamięci.

Istnieje wiele języków, które robią TCO jak (Javascript, Ruby i kilka C), gdzie jako Python a Java nie robi TCO.

Język JavaScript potwierdził użycie:) http://2ality.com/2015/06/tail-call-optimization.html

 1
Author: Rupesh Kumar Tiwari,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-30 01:34:59