Por que o Java não tem otimização para recursão de cauda?


92

Pelo que li: O motivo é que não é fácil determinar qual método será chamado de fato, pois temos herança.

No entanto, por que o Java pelo menos não possui otimização de recursão de cauda para métodos estáticos e aplica a maneira correta de chamar métodos estáticos com o compilador?

Por que o Java não tem nenhum suporte para recursão de cauda?

Não tenho certeza se há alguma dificuldade aqui.


Em relação à duplicata sugerida , conforme explicado por Jörg W Mittag 1 :

  • A outra pergunta é sobre o TCO, essa sobre o TRE. O TRE é muito mais simples que o TCO.
  • Além disso, a outra pergunta pergunta sobre quais limitações a JVM impõe às implementações de linguagem que desejam compilar com a JVM, esta pergunta sobre Java, que é a única linguagem que não é restrita pela JVM, pois a especificação da JVM pode ser alterada por as mesmas pessoas que projetam Java.
  • E, finalmente, não há sequer uma restrição na JVM sobre o TRE, porque a JVM possui GOTO intra-método, que é tudo o que é necessário para o TRE

1 Formatação adicionada para destacar pontos feitos.


26
Para citar Eric Lippert : "Os recursos não são baratos; são extremamente caros e não apenas justificam seu próprio custo, mas também justificam o custo de oportunidade de não realizar os outros cem recursos que poderíamos ter feito com esse orçamento". O Java foi projetado em parte para atrair desenvolvedores de C / C ++ e a otimização da recursão da cauda não é garantida nessas linguagens.
Doval

5
Corrija-me se eu estiver errado, mas Eric Lippert é o designer de C # que possui otimização de recursão de cauda?
InformedA

1
Ele está na equipe do compilador C #, sim.
Doval

10
Pelo que entendi, o JIT pode fazê-lo , se certas condições forem atendidas , talvez. Portanto, na prática, você não pode confiar nisso. Mas se o C # possui ou não, isso é irrelevante para o ponto de Eric.
Doval

4
@InstructedA Se você se aprofundar um pouco mais, poderá ver que isso nunca é feito no compilador JIT de 32 bits. O JIT de 64 bits é mais novo e inteligente de várias maneiras. O compilador experimental ainda mais recente (para 32 e 64 bits) ainda é mais inteligente e oferecerá suporte à otimização da recursão de cauda na IL que não solicita explicitamente. Há outro ponto que você precisa levar em consideração - os compiladores JIT não têm muito tempo. Eles são altamente otimizados para velocidade - os aplicativos que podem levar horas para compilar em C ++ ainda precisarão ir da IL para a nativa em algumas centenas de ms, no máximo (pelo menos parcialmente).
Luaan 5/02

Respostas:


132

Conforme explicado por Brian Goetz (arquiteto de linguagem Java da Oracle) neste vídeo :

nas classes jdk [...] existem vários métodos sensíveis à segurança que contam com a contagem de quadros de pilha entre o código da biblioteca jdk e o código de chamada para descobrir quem os está chamando.

Qualquer coisa que alterasse o número de quadros na pilha quebraria isso e causaria um erro. Ele admite que esse foi um motivo estúpido e, portanto, os desenvolvedores do JDK substituíram esse mecanismo.

Ele ainda menciona que não é uma prioridade, mas que a recursão da cauda

acabará sendo feito.

Nota: isso se aplica ao HotSpot e ao OpenJDK, outras VMs podem variar.


7
Estou surpreso que haja uma resposta tão cortada e seca como essa! Mas parece realmente a resposta - é possível agora, por motivos técnicos desatualizados, isso ainda não foi feito, e agora esperamos que alguém decida que é importante o suficiente para implementar.
precisa saber é o seguinte

1
Por que não implementar uma solução alternativa? Como um rótulo-goto por baixo das cobertas que apenas pula para o topo da chamada do método com novos valores para argumentos? Isso seria uma otimização em tempo de compilação e não precisaria mudar os quadros da pilha ou causar violações de segurança.
James Watkins

2
Será muito melhor para nós se você ou outra pessoa aqui puder aprofundar e fornecer especificamente o que são esses "métodos sensíveis à segurança". Obrigado!
InformedA

3
@InstructedA - ver securingjava.com/chapter-three/chapter-three-6.html que contém uma descrição detalhada de como o sistema Manager Java Segurança trabalhou cerca o lançamento do Java 2.
Jules

3
Uma solução alternativa é usar outra linguagem da JVM. Scala, por exemplo, faz isso no compilador, não no tempo de execução.
James Moore

24

Java não possui otimização de chamada final pelo mesmo motivo que a maioria das linguagens imperativas não possui. Loops imperativos são o estilo preferido da linguagem, e o programador pode substituir a recursão da cauda por loops imperativos. A complexidade não vale a pena para um recurso cujo uso é desencorajado por uma questão de estilo.

Essa coisa em que os programadores às vezes querem escrever no estilo FP em linguagens imperativas só entrou em voga nos últimos 10 anos ou mais, depois que os computadores começaram a escalar núcleos em vez de GHz. Mesmo agora, não é tão popular. Se eu sugerisse substituir um loop imperativo pela recursão da cauda no trabalho, metade dos revisores de código riria e a outra metade pareceria confusa. Mesmo na programação funcional, você geralmente evita a recursão da cauda, ​​a menos que outras construções, como funções de ordem superior, não se encaixem corretamente.


36
Esta resposta não parece certa. Só porque a recursão da cauda não é estritamente necessária não significa que não seria útil. Soluções recursivas são freqüentemente mais fáceis de entender do que a iteração, mas a falta de chamadas finais significa que algoritmos recursivos se tornam incorretos para grandes tamanhos de problemas que explodiriam a pilha. Trata-se de correção, não de desempenho (a velocidade de negociação pela simplicidade geralmente vale a pena). Como observa a resposta correta, a falta de chamadas finais é devido a um modelo de segurança estranho que depende de rastreamentos de pilha, e não a fanatismo imperativo.
amon

4
@amon James Gosling disse uma vez que não adicionaria um recurso ao Java, a menos que várias pessoas o solicitassem , e só então ele o consideraria . Portanto, não ficaria surpreso se parte da resposta realmente fosse "você sempre pode usar um forloop" (e da mesma forma para funções de primeira classe versus objetos). Eu não chegaria ao ponto de chamá-lo de "fanatismo imperativo", mas não acho que teria sido uma demanda muito alta em 1995, quando a principal preocupação provavelmente era a velocidade do Java e a falta de genéricos.
Doval

7
@amon, um modelo de segurança me parece uma razão legítima para não adicionar o TCO a um idioma pré-existente, mas um motivo ruim para não projetá-lo no idioma em primeiro lugar. Você não descarta um recurso principal visível do programador para incluir um recurso menor nos bastidores. "Por que o Java 8 não tem TCO" é uma pergunta muito diferente de "Por que o Java 1.0 não tem TCO?" Eu estava respondendo ao último.
Karl Bielefeldt

3
@rwong, você pode transformar qualquer função recursiva em uma iterativa. (Se I podem, está escrito um exemplo aqui )
Alain

3
@ZanLynx depende do tipo de problema. Por exemplo, o padrão de visitante pode se beneficiar do TCO (dependendo das especificidades da estrutura e do visitante) sem ter nada a ver com o FP. Passar por máquinas de estado é semelhante, embora a transformação usando trampolins pareça um pouco mais natural (dependendo do problema). Além disso, embora você possa, tecnicamente, implementar árvores usando loops, acredito que o consenso é que a recursão é muito mais natural (semelhante ao DFS, embora neste caso você possa evitar a recursão devido a limites de pilha, mesmo com o TCO).
Maciej Piechotka

5

Java não possui otimização de chamada alta porque a JVM não possui um bytecode para chamadas finais (para algum ponteiro de função estaticamente desconhecido , por exemplo, um método em alguma tabela).

Parece que, por razões sociais (e talvez técnicas), a adição de uma nova operação de bytecode na JVM (que a tornaria incompatível com versões anteriores dessa JVM) é terrivelmente difícil para o proprietário da especificação da JVM.

Os motivos técnicos para não incluir um novo bytecode na especificação da JVM incluem o fato de que as implementações da JVM da vida real são peças de software terrivelmente complexas (por exemplo, devido às muitas otimizações de JIT que está sendo executada).

As chamadas finais para alguma função desconhecida requerem a substituição do quadro de pilha atual por um novo, e essa operação deve ficar na JVM (não é apenas uma questão de alterar o compilador gerador de bytecode).


17
A questão não é sobre chamadas de cauda, ​​é sobre recursão de cauda. E a pergunta não é sobre a linguagem de bytecode da JVM, é sobre a linguagem de programação Java, que é uma linguagem completamente diferente. O Scala compila no bytecode da JVM (entre outros) e tem eliminação da recursão da cauda. As implementações de esquema na JVM têm chamadas de cauda apropriadas completas. O Java 7 (JVM Spec, 3ª Ed.) Adicionou um novo bytecode. O J9 JVM da IBM executa o TCO mesmo sem precisar de um código de código especial.
Jörg W Mittag 04/02

1
@ JörgWMittag: Isso é verdade, mas então, eu me pergunto se é realmente verdade que a linguagem de programação Java não possui chamadas de cauda apropriadas. Pode ser mais preciso afirmar que a linguagem de programação Java não precisa ter chamadas de cauda apropriadas, pois nada na especificação exige isso. (Ou seja: eu não tenho certeza de que qualquer coisa na especificação, na verdade, proíbe uma implementação de eliminar chamadas cauda Ele simplesmente não é mencionado..)
ruach

4

A menos que um idioma tenha uma sintaxe especial para fazer uma chamada final (recursiva ou não) e um compilador gritar quando uma chamada final for solicitada, mas não puder ser gerada, a otimização "opcional" da chamada final ou da recursão final produzirá situações em que uma peça do código pode exigir menos de 100 bytes de pilha em uma máquina, mas mais de 100.000.000 bytes de pilha em outra. Tal diferente deve ser considerado qualitativo, e não meramente quantitativo.

Espera-se que as máquinas tenham tamanhos de pilha diferentes e, portanto, é sempre possível que o código funcione em uma máquina, mas exploda a pilha em outra. Geralmente, no entanto, o código que funcionará em uma máquina mesmo quando a pilha é artificialmente restrita provavelmente funcionará em todas as máquinas com tamanhos de pilha "normais". Se, no entanto, um método que percorre 1.000.000 de profundidade for otimizado para uma chamada final em uma máquina, mas não em outra, a execução na máquina anterior provavelmente funcionará mesmo que sua pilha seja incomumente pequena e falhe na segunda, mesmo que sua pilha seja extraordinariamente grande .


2

Eu acho que a recursão da chamada de cauda não é usada em Java principalmente porque isso alteraria os rastreamentos de pilha e, portanto, tornaria a depuração de um programa muito mais difícil. Acho que um dos principais objetivos do Java é permitir que os programadores depurem facilmente seu código, e o rastreamento de pilha é essencial para fazer isso, especialmente em um ambiente de programação altamente orientado a objetos. Como a iteração poderia ser usada, o comitê de idiomas deve ter percebido que não vale a pena adicionar recursão final.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.