Iterativní vs rekurzivní vs ocas rekurzivní v Golangu

Napsal jsem jednoduchou funkci Fibonacci 3 různými způsoby (kód najdete zde):

Iterativní:

// Iterativní verze Fibonacciho
func fiboI (n int) int {
 var výsledek int
pro i, první, druhý: = 0, 0, 1; i <= n; i, first, second = i + 1, first + second, first {
  pokud i == n {
   výsledek = první
  }
 }
vrátit výsledek
}

Rekurzivní:

// Rekurzivní verze Fibonacci
func fiboR (n int) int {
 pokud n <2 {
  návrat n
 }
 návrat fiboR (n-2) + fiboR (n-1)
}

Tail-rekurzivní:

func fiboTail (n int) int {
 návrat fiboT (n, 0, 1)
}
// Tail-rekurzivní verze Fibonacci
func fiboT (n, first, second int) int {
 pokud n == 0 {
  vrátit se první
 }
návrat fiboT (n-1, druhý, první + druhý)
}

Benchmark:

Benchmark_FibIterative_10-4 100000000 16,4 ns / op
Benchmark_FibTailRecursive_10-4 50000000 33,3 ns / op
Benchmark_FibRecursive_10-4 5000000 394 ns / op
SLOŽIT
ok github.com/tkanos/recursion-test 5.735s

Jak se očekávalo, iterativní je rychlejší běh 100000000krát rychlostí 16,4 ns na smyčku.

Ale přemýšlím ...

Proč je rekurziv tak dlouhý?

Takže jste připraveni mluvit o rámečku zásobníku (může to být složité): D

Počítač používá rámeček zásobníku, do kterého zavádí všechny nové funkce.

Při každém vyvolání funkce interně počítač přidá do bloku zásobníku nový blok:

(poznámka: pokud přidáme mnoho funkcí (například nekonečná / nebo jen velká rekurzivní smyčka) bude mít přetečení zásobníku))

Rámec zásobníku se používá k „udržení“ „stavu“ jednotlivých funkcí. Proto bude ukládat všechny proměnné místních funkcí (a jejich hodnoty), aby udržoval kontext nadřazené funkce, jako například při dokončení funkce podřízené funkce funguje (RET), když se program vrátí k nadřazené funkci, program je schopen pokračovat v práci se všemi uloženými proměnnými.

Konkrétně to znamená, že v kódu sestavy uvidíte, že před voláním každé funkce musí počítač:

  • aktualizace registru esp (ukazatel na horní část zásobníku kdykoli).
  • aktualizace registru ebp (ukazatel na začátek zásobníku)
  • uloží všechny lokální proměnné funkce
  • uložit EIP (adresu další operace, která má být vytočena, když je funkce ukončena)

Takže když zavoláte funkci, program to udělá, poté proveďte VOLÁNÍ na adresu funkce a na konci funkce (RET) získá počítač dříve uloženou adresu EIP, aby se mohl vrátit na další provedení funkce volajícího, POP všechny staré proměnné, aby bylo možné zadat v uloženém kontextu, a pokračovat v nadřazené funkci.

A veškerá tato správa paměti + VOLÁNÍ trvá, na stovkách otevřených funkcí (kvůli rekurzi) zabere procesoru čas. Je to proto, že rekurzivní je pomalejší.

Můžete se mě zeptat: „Ale rekurze ocasu dělají totéž, a je to rychlejší“.

Ano, protože rekurze otevře pokaždé novou funkci bez uzavření poslední do odpovědi na poslední rekurzivní funkci.

Fibo (10)
Fibo (8) + Fibo (9)
Fibo (6) + Fibo (7) + Fibo (7) + Fibo (8)
.....
.....
..... + Fibo (2)

Až do Fibo (2), které mají podmínku přerušení smyčky, rámeček zásobníku spravuje všechny ostatní funkce Fibo otevřené, takže pro Fibo (10) budeme mít něco jako 177 funkce otevřené. (ano, je toho moc)

V případě rekurzivního ocasu máme v zásobníku pouze dvě funkce:

  • funkce rodičovského volání (FiboTail (10))
  • Funkce vykonávající.

Protože když je prováděná frakce ukončena (RET), je vyčištěna (protože je po skončení) a nahrazena další.

FiboTail (10) // otevřen
FiboT (10, 0, 1) // otevřeno
FiboT (9, 1, 1) // tento bude v zásobníku pouze tehdy, bude-li předchozí ukončen, takže bude ze zásobníku odstraněn.

Optimalizace rekurze ocasu

Některé kompilátory jsou optimalizovány pro práci s rekurzí ocasu:

  • dělat inline funkci (kopírování kódu z funkce, na nadřazené funkci místo toho dělat CALL + všechny věci zásobníku) (nevýhoda spustitelného souboru je větší.)
  • s vědomím, že výsledek bude pouze v poslední funkci, kompilátoři nevytvoří nový rámec zásobníku pro každé rekurzivní volání a místo toho pouze znovu použijí stejný rámec zásobníku.

Je těžké to vysvětlit, doufám, že jste tomu rozuměli. Dobré vysvětlení mezi rekurzí vs. rekurzí Tailu najdete na tomto videu:

Odkazy :

  • https://github.com/Tkanos/recursion-test
  • https://www.youtube.com/watch?v=wLRuAg0ZHt0
  • http://www.programmerinterview.com/index.php/recursion/tail-call-optimization/
  • https://medium.com/@felipedutratine/does-golang-inline-functions-b41ee2d743fa#.vus18hi4w