Eu encontrei esta pergunta sobre quais linguagens otimizam a recursão da cauda. Por que o C # não otimiza a recursão da cauda, sempre que possível?
Para um caso concreto, por que este método não é otimizado em um loop ( Visual Studio 2008 de 32 bits, se for o caso) ?:
private static void Foo(int i)
{
if (i == 1000000)
return;
if (i % 100 == 0)
Console.WriteLine(i);
Foo(i+1);
}
c#
.net
optimization
tail-recursion
ripper234
fonte
fonte
preemptive
(por exemplo, algoritmo fatorial) eNon-preemptive
(por exemplo, função de ackermann). O autor deu apenas dois exemplos que mencionei, sem dar um raciocínio adequado por trás dessa bifurcação. Essa bifurcação é igual às funções recursivas caudas e não caudais?Respostas:
A compilação JIT é um ato de equilíbrio complicado entre não gastar muito tempo fazendo a fase de compilação (assim, desacelerando consideravelmente os aplicativos de curta duração) e não fazer análise suficiente para manter o aplicativo competitivo no longo prazo com uma compilação antecipada padrão .
Curiosamente, as etapas de compilação do NGen não têm como objetivo ser mais agressivas em suas otimizações. Suspeito que isso seja porque eles simplesmente não querem ter bugs em que o comportamento dependa de se o JIT ou NGen foi o responsável pelo código de máquina.
O próprio CLR oferece suporte à otimização de chamada final, mas o compilador específico da linguagem deve saber como gerar o opcode relevante e o JIT deve estar disposto a respeitá-lo. O fsc do F # irá gerar os opcodes relevantes (embora para uma recursão simples ele possa apenas converter a coisa inteira em um
while
loop diretamente). O csc do C # não.Veja esta postagem do blog para alguns detalhes (possivelmente agora desatualizado devido às mudanças recentes no JIT). Observe que as alterações do CLR para 4.0 x86, x64 e ia64 irão respeitá-lo .
fonte
Este envio de feedback do Microsoft Connect deve responder à sua pergunta. Ele contém uma resposta oficial da Microsoft, então eu recomendo ir por ela.
A propósito, como foi apontado, é importante notar que a recursão da cauda é otimizada em x64.
fonte
C # não otimiza para recursão de chamada final porque é para isso que o F # serve!
Para obter mais detalhes sobre as condições que impedem o compilador C # de realizar otimizações de chamada final, consulte este artigo: Condições de chamada final JIT CLR .
Interoperabilidade entre C # e F #
C # e F # interoperam muito bem e, como o .NET Common Language Runtime (CLR) foi projetado com essa interoperabilidade em mente, cada linguagem é projetada com otimizações que são específicas para sua finalidade e objetivos. Para obter um exemplo que mostra como é fácil chamar o código F # a partir do código C #, consulte Chamando o código F # a partir do código C # ; para obter um exemplo de chamada de funções C # a partir do código F #, consulte Chamando funções C # a partir de F # .
Para delegar interoperabilidade, consulte este artigo: Delegar interoperabilidade entre F #, C # e Visual Basic .
Diferenças teóricas e práticas entre C # e F #
Aqui está um artigo que cobre algumas das diferenças e explica as diferenças de design da recursão de chamada final entre C # e F #: Gerando Opcode de chamada final em C # e F # .
Aqui está um artigo com alguns exemplos em C #, F # e C ++ \ CLI: Aventuras na recursão de cauda em C #, F # e C ++ \ CLI
A principal diferença teórica é que o C # é projetado com loops, enquanto o F # é projetado com base nos princípios do cálculo Lambda. Para obter um livro muito bom sobre os princípios do cálculo Lambda, consulte este livro gratuito: Structure and Interpretation of Computer Programs, de Abelson, Sussman e Sussman .
Para obter um artigo introdutório muito bom sobre chamadas finais em F #, consulte este artigo: Introdução detalhada às chamadas finais em F # . Finalmente, aqui está um artigo que cobre a diferença entre recursão não cauda e recursão de chamada de cauda (em F #): Recursão de cauda vs. recursão não cauda em F sustenido .
fonte
Disseram-me recentemente que o compilador C # para 64 bits otimiza a recursão final.
C # também implementa isso. O motivo pelo qual nem sempre é aplicado é que as regras usadas para aplicar a recursão de cauda são muito rígidas.
fonte
Você pode usar a técnica do trampolim para funções recursivas de cauda em C # (ou Java). No entanto, a melhor solução (se você se preocupa apenas com a utilização da pilha) é usar este pequeno método auxiliar para agrupar partes da mesma função recursiva e torná-la iterativa enquanto mantém a função legível.
fonte
Como outras respostas mencionadas, CLR oferece suporte à otimização de chamada final e parece que historicamente sofreu melhorias progressivas. Mas suportá-lo em C # tem um
Proposal
problema aberto no repositório git para o design da linguagem de programação C # Support tail recursion # 2544 .Você pode encontrar alguns detalhes e informações úteis lá. Por exemplo @jaykrell mencionado
Também deixe-me mencionar (como informação extra), quando estamos gerando um lambda compilado usando classes de expressão no
System.Linq.Expressions
namespace, há um argumento chamado 'tailCall' que, conforme explicado em seu comentário, éAinda não experimentei e não tenho certeza de como pode ajudar em relação à sua dúvida, mas provavelmente alguém pode tentar e pode ser útil em alguns cenários:
fonte