Muito simplesmente, o que é otimização de chamada de cauda?
Mais especificamente, quais são alguns pequenos trechos de código onde eles podem ser aplicados e onde não, com uma explicação do porquê?
Muito simplesmente, o que é otimização de chamada de cauda?
Mais especificamente, quais são alguns pequenos trechos de código onde eles podem ser aplicados e onde não, com uma explicação do porquê?
Respostas:
A otimização de chamada de cauda é onde você pode evitar a alocação de um novo quadro de pilha para uma função porque a função de chamada retornará simplesmente o valor que obtém da função chamada. O uso mais comum é a recursão de cauda, onde uma função recursiva escrita para aproveitar a otimização de chamada de cauda pode usar espaço de pilha constante.
O Scheme é uma das poucas linguagens de programação que garantem na especificação que qualquer implementação deve fornecer essa otimização (o JavaScript também inicia no ES6) ; portanto, aqui estão dois exemplos da função fatorial no Scheme:
A primeira função não é recursiva de cauda porque, quando a chamada recursiva é feita, a função precisa acompanhar a multiplicação que precisa fazer com o resultado após o retorno da chamada. Como tal, a pilha tem a seguinte aparência:
Por outro lado, o rastreamento da pilha para o fatorial recursivo da cauda é o seguinte:
Como você pode ver, só precisamos acompanhar a mesma quantidade de dados para cada chamada à realidade, porque estamos simplesmente retornando o valor que atingimos até o topo. Isso significa que, mesmo que eu chame (fato 1000000), preciso apenas da mesma quantidade de espaço que (fato 3). Esse não é o caso do fato não recursivo de cauda e, como tais, valores grandes podem causar um estouro de pilha.
fonte
Vamos seguir um exemplo simples: a função fatorial implementada em C.
Começamos com a definição recursiva óbvia
Uma função termina com uma chamada final se a última operação antes do retorno da função for outra chamada de função. Se essa chamada chamar a mesma função, será recursiva da cauda.
Embora
fac()
pareça recursivo à primeira vista, não é como o que realmente acontece éou seja, a última operação é a multiplicação e não a chamada de função.
No entanto, é possível reescrever
fac()
para ser recursivo passando o valor acumulado na cadeia de chamadas como um argumento adicional e passando apenas o resultado final novamente como o valor de retorno:Agora, por que isso é útil? Como retornamos imediatamente após a chamada final, podemos descartar o stackframe anterior antes de chamar a função na posição final ou, no caso de funções recursivas, reutilizar o stackframe como está.
A otimização de chamada de cauda transforma nosso código recursivo em
Isso pode ser incorporado
fac()
e chegamos aque é equivalente a
Como podemos ver aqui, um otimizador suficientemente avançado pode substituir a recursão da cauda pela iteração, o que é muito mais eficiente, pois você evita a sobrecarga da chamada de função e usa apenas uma quantidade constante de espaço na pilha.
fonte
TCO (Otimização de chamada de cauda) é o processo pelo qual um compilador inteligente pode fazer uma chamada para uma função e não ocupa espaço adicional na pilha. A única situação em que isso ocorre é se a última instrução executada em uma função f for uma chamada para uma função g (Nota: g pode ser f ). A chave aqui é que f não precisa mais de espaço na pilha - simplesmente chama g e depois retorna o que g retornaria. Nesse caso, pode-se fazer a otimização de que g simplesmente roda e retorna qualquer valor que ele teria para a coisa chamada f.
Essa otimização pode fazer com que chamadas recursivas ocupem espaço constante na pilha, em vez de explodir.
Exemplo: esta função fatorial não é TCOptimizable:
Essa função faz outras coisas além de chamar outra função em sua declaração de retorno.
Esta função abaixo é TCOptimizable:
Isso ocorre porque a última coisa que acontece em qualquer uma dessas funções é chamar outra função.
fonte
(cons a (foo b))
ou(+ c (bar d))
na posição da cauda da mesma maneira.Provavelmente, a melhor descrição de alto nível que encontrei para chamadas de cauda, chamadas de cauda recursivas e otimização de chamada de cauda é a postagem do blog
"O que diabos é: Uma chamada de cauda"
de Dan Sugalski. Na otimização de chamada de cauda, ele escreve:
E na recursão da cauda:
Para que isso:
é silenciosamente transformado em:
O que eu gosto nessa descrição é o quão fácil e sucinto é entender para aqueles que têm um histórico imperativo de linguagem (C, C ++, Java)
fonte
foo
chamada inicial da função não está otimizada? Ele está apenas chamando uma função como seu último passo, e está simplesmente retornando esse valor, certo?Observe primeiro que nem todos os idiomas o suportam.
O TCO aplica-se a um caso especial de recursão. A essência disso é que, se a última coisa que você faz em uma função é chamada de si mesma (por exemplo, está se chamando da posição "tail"), isso pode ser otimizado pelo compilador para agir como iteração em vez de recursão padrão.
Você vê, normalmente durante a recursão, o tempo de execução precisa acompanhar todas as chamadas recursivas, para que, quando a pessoa retorne, possa retomar a chamada anterior e assim por diante. (Tente escrever manualmente o resultado de uma chamada recursiva para ter uma idéia visual de como isso funciona.) Manter o controle de todas as chamadas ocupa espaço, o que é significativo quando a função se chama muito. Mas com o TCO, ele pode apenas dizer "volte ao início, só que desta vez altere os valores dos parâmetros para esses novos". Isso é possível porque nada após a chamada recursiva se refere a esses valores.
fonte
foo
chamada inicial do método não é otimizada?Exemplo executável mínimo do GCC com análise de desmontagem x86
Vamos ver como o GCC pode fazer automaticamente otimizações de chamada de cauda para nós, observando o assembly gerado.
Isso servirá como um exemplo extremamente concreto do que foi mencionado em outras respostas, como https://stackoverflow.com/a/9814654/895245 que a otimização pode converter chamadas de função recursivas em um loop.
Por sua vez, economiza memória e melhora o desempenho, pois os acessos à memória costumam ser a principal coisa que torna os programas lentos atualmente. .
Como entrada, fornecemos ao GCC um fatorial não otimizado baseado em pilha:
tail_call.c
GitHub upstream .
Compilar e desmontar:
Onde
-foptimize-sibling-calls
é o nome da generalização de chamadas de cauda de acordo comman gcc
:como mencionado em: Como verifico se o gcc está executando a otimização da recursão de cauda?
Eu escolho
-O1
porque:-O0
. Eu suspeito que isso ocorre porque faltam transformações intermediárias necessárias.-O3
produz um código incrivelmente eficiente que não seria muito educativo, embora também seja otimizado.Desmontagem com
-fno-optimize-sibling-calls
:Com
-foptimize-sibling-calls
:A principal diferença entre os dois é que:
os
-fno-optimize-sibling-calls
usoscallq
, que é a chamada de função não otimizada típica.Esta instrução envia o endereço de retorno para a pilha, aumentando-o.
Além disso, esta versão também faz
push %rbx
, que empurra%rbx
para a pilha .O GCC faz isso porque armazena o
edi
qual é o primeiro argumento da função (n
)ebx
e depois chamafactorial
.O GCC precisa fazer isso porque está se preparando para outra chamada para
factorial
, que usará o novoedi == n-1
.Ele escolhe
ebx
porque este registro é salvo por chamada: O que os registros são preservados por meio de uma chamada de função x86-64 do linux, para que a sub- chamadafactorial
não o altere e percan
.o
-foptimize-sibling-calls
não usa nenhuma instrução que empurre para a pilha: apenasgoto
salta dentrofactorial
com as instruçõesje
ejne
.Portanto, esta versão é equivalente a um loop while, sem nenhuma chamada de função. O uso da pilha é constante.
Testado no Ubuntu 18.10, GCC 8.2.
fonte
Olhe aqui:
http://tratt.net/laurie/tech_articles/articles/tail_call_optimization
Como você provavelmente sabe, chamadas de função recursivas podem causar estragos em uma pilha; é fácil ficar rapidamente sem espaço na pilha. A otimização de chamada de cauda é a maneira pela qual você pode criar um algoritmo de estilo recursivo que utiliza espaço constante da pilha; portanto, ele não cresce e cresce e você recebe erros de pilha.
fonte
Devemos garantir que não haja instruções goto na própria função. O cuidado pela chamada da função é a última coisa na função callee.
Recursões em larga escala podem usar isso para otimizações, mas em pequena escala, a sobrecarga de instruções para fazer a chamada de função ser uma chamada final reduz o objetivo real.
O TCO pode causar uma função de execução permanente:
fonte
A abordagem da função recursiva tem um problema. Ele cria uma pilha de chamadas do tamanho O (n), que faz com que nossa memória total custe O (n). Isso o torna vulnerável a um erro de estouro de pilha, em que a pilha de chamadas fica muito grande e fica sem espaço.
Esquema de otimização de chamada de cauda (TCO). Onde ele pode otimizar funções recursivas para evitar a criação de uma pilha alta de chamadas e, portanto, economiza o custo da memória.
Existem muitas linguagens que fazem TCO como (JavaScript, Ruby e poucos C), enquanto Python e Java não fazem TCO.
A linguagem JavaScript foi confirmada usando :) http://2ality.com/2015/06/tail-call-optimization.html
fonte
Em uma linguagem funcional, a otimização da chamada de cauda é como se uma chamada de função pudesse retornar uma expressão parcialmente avaliada como resultado, que seria avaliada pelo chamador.
f 6 reduz para g 6. Portanto, se a implementação puder retornar g 6 como resultado e, em seguida, chamar essa expressão, ela salvará um quadro de pilha.
Além disso
Reduz para f 6 para g 6 ou h 6. Portanto, se a implementação avalia c 6 e descobre que é verdade, pode reduzir,
Um simples intérprete de otimização de chamada sem cauda pode se parecer com isso,
Um intérprete de otimização de chamada de cauda pode se parecer com isso,
fonte