Por que não fazer o compilador executar um programa como este:
function a(b) { return b^2 };
function c(b) { return a(b) + 5 };
e converta-o em um programa como este:
function c(b) { return b^2 + 5 };
eliminando assim a necessidade do computador de lembrar o endereço de retorno de c (b)?
Suponho que o aumento do espaço no disco rígido e da RAM necessários para armazenar o programa e suportar sua compilação (respectivamente) seja a razão pela qual usamos pilhas de chamadas. Isso está correto?
compiler
stack
code-generation
calling-conventions
moonman239
fonte
fonte
window[prompt("Enter function name","")]()
function(a)b { if(b>0) return a(b-1); }
sem uma pilha?b
. Porém, nem todas as funções recursivas podem eliminar a recursão, e mesmo quando a função pode, em princípio, o compilador pode não ser inteligente o suficiente para fazê-lo.Respostas:
Isso é chamado de "inlining" e muitos compiladores fazem isso como uma estratégia de otimização nos casos em que faz sentido.
No seu exemplo específico, essa otimização economizaria espaço e tempo de execução. Mas se a função fosse chamada em vários locais do programa (não incomum!), Aumentaria o tamanho do código, para que a estratégia se tornasse mais duvidosa. (E, é claro, se uma função se chama direta ou indiretamente, seria impossível integrar, pois o código se tornaria infinito em tamanho.)
E, obviamente, isso só é possível para funções "privadas". As funções expostas para chamadores externos não podem ser otimizadas, pelo menos não em idiomas com vínculo dinâmico.
fonte
Há duas partes em sua pergunta: por que ter várias funções (em vez de substituir chamadas de função por sua definição) e por que implementar essas funções com pilhas de chamadas em vez de alocar estaticamente seus dados em outro lugar?
A primeira razão é recursão. Não apenas o tipo "oh, vamos fazer uma nova chamada de função para cada item desta lista", também o tipo modesto em que você pode ter duas chamadas de uma função ativas ao mesmo tempo, com muitas outras funções entre elas. Você precisa colocar variáveis locais em uma pilha para suportar isso e não pode incorporar funções recursivas em geral.
Há um problema para as bibliotecas: você não sabe quais funções serão chamadas a partir de onde e com que frequência; portanto, uma "biblioteca" nunca poderá realmente ser compilada, enviada apenas para todos os clientes em algum formato conveniente de alto nível que será inline no aplicativo. Além de outros problemas, você perde completamente o vínculo dinâmico com todas as suas vantagens.
Além disso, há muitos motivos para não incorporar funções, mesmo quando você pode:
fonte
As pilhas nos permitem ignorar elegantemente os limites impostos pelo número finito de registros.
Imagine ter exatamente 26 globais "registra az" (ou mesmo apenas os registros de 7 bytes do chip 8080) E todas as funções que você escreve neste aplicativo compartilham essa lista simples.
Um começo ingênuo seria alocar os primeiros registros para a primeira função e, sabendo que foram necessários apenas 3, inicie com "d" para a segunda função ... Você se esgota rapidamente.
Em vez disso, se você tiver uma fita metafórica, como a máquina de turing, cada função poderá iniciar uma "chamar outra função" salvando todas as variáveis que está usando e encaminhar () a fita e, em seguida, a função de chamada pode se confundir com tantas registra como deseja. Quando o chamado é retornado, ele retorna o controle para a função pai, que sabe onde capturar a saída do receptor conforme necessário e, em seguida, reproduz a fita para trás para restaurar seu estado.
Seu quadro de chamada básico é exatamente isso e é criado e descartado por seqüências de código de máquina padronizadas que o compilador realiza nas transições de uma função para outra. (Faz muito tempo que eu não lembro dos meus quadros de pilha C, mas você pode ler sobre as várias maneiras das tarefas de quem descarta o que em X86_calling_conventions .)
(a recursão é incrível, mas se você já teve que fazer malabarismos com registros sem uma pilha, apreciaria muito as pilhas.)
Embora possamos alinhar mais atualmente, ("mais velocidade" é sempre bom; "menos kb de montagem" significa muito pouco em um mundo de fluxos de vídeo). A principal limitação está na capacidade do compilador de nivelar certos tipos de padrões de código.
Por exemplo, objetos polimórficos - se você não conhece o único e único tipo de objeto a ser entregue, não pode achatar; você precisa olhar para a tabela de recursos do objeto e chamar por esse ponteiro ... trivial para fazer em tempo de execução, impossível alinhar em tempo de compilação.
Uma cadeia de ferramentas moderna pode alinhar alegremente uma função definida polimorficamente quando achatar o (s) chamador (es) o suficiente para saber exatamente qual é o sabor do obj:
acima, o compilador pode optar por manter o status interno, desde o InlineMe até o que estiver dentro do ato (), nem a necessidade de tocar em nenhuma vtables no tempo de execução.
Mas qualquer incerteza quanto ao sabor do objeto o deixará como uma chamada para uma função discreta, mesmo que outras invocações da mesma função sejam incorporadas.
fonte
Casos que essa abordagem não pode tratar:
function fib(a) { if(a>2) return fib(a-1)+fib(a-2); else return 1; }
function many(a) { for(i = 1 to a) { b(i); };}
Não são linguagens e plataformas com pilhas pouca ou nenhuma chamada. Os microprocessadores PIC têm uma pilha de hardware limitada a entre 2 e 32 entradas . Isso cria restrições de design.
COBOL proíbe recursão: https://stackoverflow.com/questions/27806812/in-cobol-is-it-possible-to-recursively-call-a-paragraph
A imposição de uma proibição de recursão significa que você pode representar o gráfico de chamada inteiro do programa estaticamente como um DAG. Seu compilador poderá emitir uma cópia de uma função para cada local a partir do qual é chamada com um salto fixo em vez de um retorno. Nenhuma pilha é necessária, apenas mais espaço de programa, potencialmente bastante para sistemas complexos. Mas para pequenos sistemas embarcados, isso significa que você pode garantir que não haja excesso de pilha durante a execução, o que seria uma má notícia para o controle de reator nuclear / turbina a jato / acelerador de carro etc.
fonte
fib
chamada.)Você quer que a função inlining , ea maioria ( otimização ) compiladores estão fazendo isso.
Observe que inlining requer que a função chamada seja conhecida (e é efetiva apenas se a função chamada não for muito grande), pois conceitualmente está substituindo a chamada pela reescrita da função chamada. Portanto, você geralmente não pode incorporar uma função desconhecida (por exemplo, um ponteiro de função - e que inclui funções de bibliotecas compartilhadas dinamicamente vinculadas -, que talvez seja visível como um método virtual em algumas tabelas , mas alguns compiladores às vezes podem otimizar através de técnicas de destirtualização ). Obviamente, nem sempre é possível alinhar funções recursivas (alguns compiladores inteligentes podem usar avaliação parcial e, em alguns casos, conseguir alinhar funções recursivas).
Observe também que o inlining, mesmo quando é facilmente possível, nem sempre é eficaz: você (na verdade seu compilador) pode aumentar tanto o tamanho do código que os caches da CPU (ou o preditor de ramificação ) funcionariam com menos eficiência e isso faria com que seu programa fosse executado Mais devagar.
Estou focando um pouco no estilo de programação funcional , já que você marcou sua pergunta como tal.
Observe que você não precisa ter nenhuma pilha de chamadas (pelo menos no sentido de máquina da expressão "pilha de chamadas"). Você poderia usar apenas a pilha.
Portanto, dê uma olhada nas continuações e leia mais sobre o estilo de passagem de continuação (CPS) e a transformação do CPS (intuitivamente, você pode usar os fechamentos de continuação como "quadros de chamada" reificados alocados no heap, e eles meio que imitam uma pilha de chamadas; você precisa de um coletor de lixo eficiente ).
Andrew Appel escreveu um livro Compilando com continuações e uma coleta de lixo de papel antiga pode ser mais rápida que a alocação de pilha . Veja também o artigo de A.Kennedy (ICFP2007) Compilando com continuações, continuação
Também recomendo a leitura do livro Lisp In Small Pieces , de Queinnec , que possui vários capítulos relacionados à continuação e compilação.
Observe também que algumas linguagens (por exemplo, Brainfuck ) ou máquinas abstratas (por exemplo , OISC , RAM ) não possuem nenhum recurso de chamada, mas ainda estão completas com Turing , portanto você (em teoria) nem precisa de nenhum mecanismo de chamada de função, mesmo que é extremamente conveniente. BTW, algumas arquiteturas antigas de conjuntos de instruções (por exemplo, IBM / 370 ) nem sequer possuem uma pilha de chamadas de hardware ou uma instrução de máquina de chamada de envio (o IBM / 370 tinha apenas uma instrução de máquina de Filial e Link )
Por fim, se todo o seu programa (incluindo todas as bibliotecas necessárias) não tiver nenhuma recursão, você poderá armazenar o endereço de retorno (e as variáveis "locais", que na verdade estão se tornando estáticas) de cada função em locais estáticos. Os antigos compiladores Fortran77 fizeram isso no início dos anos 80 (portanto, os programas compilados não usavam nenhuma pilha de chamadas naquele momento).
fonte
%esp
, etc., mas ainda mantém a contabilidade equivalente em uma pilha de espaguete com nome apropriado em outra região da RAM. O endereço de retorno, em particular, é essencialmente codificado na continuação. E, é claro, as continuações não são mais rápidas (e parece-me que é nisso que o OP estava chegando) do que não fazer chamadas de forma alguma através do inlining.Inlining (substituindo chamadas de função por funcionalidade equivalente) funciona bem como uma estratégia de otimização para pequenas funções simples. A sobrecarga de uma chamada de função pode ser efetivamente trocada por uma pequena penalidade no tamanho adicionado do programa (ou, em alguns casos, nenhuma penalidade).
No entanto, grandes funções que por sua vez chamam outras funções podem levar a uma enorme explosão no tamanho do programa, se tudo estiver embutido.
O objetivo principal das funções que podem ser chamadas é facilitar a reutilização eficiente, não apenas pelo programador, mas pela própria máquina, e isso inclui propriedades como memória razoável ou pegada no disco.
Pelo que vale: você pode ter funções que podem ser chamadas sem uma pilha de chamadas. Por exemplo: IBM System / 360. Ao programar em idiomas como FORTRAN nesse hardware, o contador de programa (endereço de retorno) seria salvo em uma pequena seção de memória reservada logo à frente do ponto de entrada da função. Ele permite funções reutilizáveis, mas não permite recursão ou código multiencadeado (uma tentativa de uma chamada recursiva ou reentrante resultaria na substituição de um endereço de retorno salvo anteriormente).
Conforme explicado por outras respostas, as pilhas são boas. Eles facilitam a recursão e as chamadas multithread. Embora qualquer algoritmo codificado para usar a recursão possa ser codificado sem depender da recursão, o resultado pode ser mais complexo, mais difícil de manter e menos eficiente. Não tenho certeza de que uma arquitetura sem pilha possa suportar multithreading.
fonte