ATUALIZAÇÃO: gostei tanto desta pergunta que a tornei o assunto do meu blog em 18 de novembro de 2011 . Obrigado pela ótima pergunta!
Eu sempre me perguntei: qual é o propósito da pilha?
Suponho que você queira dizer a pilha de avaliação da linguagem MSIL, e não a pilha por thread real em tempo de execução.
Por que há uma transferência da memória para a pilha ou "carregando"? Por outro lado, por que há uma transferência da pilha para a memória ou "armazenamento"? Por que não colocá-los todos na memória?
MSIL é uma linguagem de "máquina virtual". Compiladores como o compilador C # geram CIL e, em tempo de execução, outro compilador chamado JIT (Just In Time) transforma o IL em código de máquina real que pode ser executado.
Então, primeiro, vamos responder à pergunta "por que o MSIL é mesmo?" Por que não apenas fazer o compilador C # escrever o código da máquina?
Porque é mais barato fazê-lo desta maneira. Suponha que não fizemos dessa maneira; suponha que cada idioma tenha que ter seu próprio gerador de código de máquina. Você tem vinte idiomas diferentes: C #, JScript .NET , Visual Basic, IronPython , F # ... E suponha que você tenha dez processadores diferentes. Quantos geradores de código você precisa escrever? 20 x 10 = 200 geradores de código. Isso dá muito trabalho. Agora, suponha que você queira adicionar um novo processador. Você precisa escrever o gerador de código para ele vinte vezes, um para cada idioma.
Além disso, é um trabalho difícil e perigoso. Escrever geradores de código eficientes para chips nos quais você não é especialista é um trabalho árduo! Os projetistas de compiladores são especialistas na análise semântica de seu idioma, e não na alocação eficiente de registros de novos conjuntos de chips.
Agora, suponha que façamos da maneira CIL. Quantos geradores CIL você precisa escrever? Um por idioma. Quantos compiladores JIT você precisa escrever? Um por processador. Total: 20 + 10 = 30 geradores de código. Além disso, o gerador de linguagem para CIL é fácil de escrever porque o CIL é uma linguagem simples, e o gerador de CIL para código de máquina também é fácil de escrever porque o CIL é uma linguagem simples. Nós nos livramos de todos os meandros do C # e do VB e outros enfeites e "abaixamos" tudo para uma linguagem simples e fácil de escrever.
Ter um idioma intermediário reduz drasticamente o custo de produção de um novo compilador de idiomas . Também reduz drasticamente o custo de suporte de um novo chip. Você deseja oferecer suporte a um novo chip, encontra alguns especialistas nesse chip e solicita que eles escrevam uma instabilidade CIL e pronto; então você suporta todos esses idiomas no seu chip.
OK, então estabelecemos por que temos o MSIL; porque ter um idioma intermediário reduz os custos. Por que então o idioma é uma "máquina de empilhar"?
Como as máquinas de pilha são conceitualmente muito simples para os escritores de compiladores de idiomas lidarem. As pilhas são um mecanismo simples e de fácil compreensão para descrever cálculos. Máquinas de empilhar também são conceitualmente muito fáceis para os escritores de compiladores JIT. Usar uma pilha é uma abstração simplificadora e, portanto, novamente, reduz nossos custos .
Você pergunta "por que ter uma pilha?" Por que não fazer tudo diretamente sem memória? Bem, vamos pensar sobre isso. Suponha que você queira gerar código CIL para:
int x = A() + B() + C() + 10;
Suponha que tenhamos a convenção de que "add", "call", "store" e assim por diante sempre retiram seus argumentos da pilha e colocam seu resultado (se houver) na pilha. Para gerar código CIL para este C #, apenas dizemos algo como:
load the address of x // The stack now contains address of x
call A() // The stack contains address of x and result of A()
call B() // Address of x, result of A(), result of B()
add // Address of x, result of A() + B()
call C() // Address of x, result of A() + B(), result of C()
add // Address of x, result of A() + B() + C()
load 10 // Address of x, result of A() + B() + C(), 10
add // Address of x, result of A() + B() + C() + 10
store in address // The result is now stored in x, and the stack is empty.
Agora, suponha que fizemos isso sem uma pilha. Vamos fazer do seu jeito, onde todo código de operação leva os endereços de seus operandos e o endereço no qual armazena seu resultado :
Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...
Você vê como isso vai? Nosso código está ficando enorme porque temos que alocar explicitamente todo o armazenamento temporário que normalmente por convenção iria para a pilha . Pior, nossos próprios opcodes estão ficando enormes, porque agora eles têm que usar como argumento o endereço no qual escreverão o resultado e o endereço de cada operando. Uma instrução "add" que sabe que vai tirar duas coisas da pilha e colocar uma coisa pode ser um único byte. Uma instrução add que usa dois endereços de operando e um endereço de resultado será enorme.
Usamos opcodes baseados em pilha porque as pilhas resolvem o problema comum . A saber: quero alocar algum armazenamento temporário, usá-lo muito em breve e depois me livrar dele rapidamente quando terminar . Ao supor que temos uma pilha à nossa disposição, podemos tornar os códigos de operação muito pequenos e o código muito concisos.
ATUALIZAÇÃO: Algumas reflexões adicionais
Aliás, essa idéia de reduzir drasticamente os custos (1) especificando uma máquina virtual, (2) escrevendo compiladores direcionados à linguagem da VM e (3) escrevendo implementações da VM em uma variedade de hardware, não é uma idéia totalmente nova. . Não se originou com MSIL, LLVM, Java bytecode ou qualquer outra infra-estrutura moderna. A primeira implementação dessa estratégia que conheço é a máquina pcode de 1966.
A primeira vez que ouvi falar desse conceito foi quando aprendi como os implementadores da Infocom conseguiram fazer com que o Zork funcionasse tão bem em tantas máquinas diferentes. Eles especificaram uma máquina virtual chamada Z-machine e, em seguida, criaram emuladores de Z-machine para todo o hardware em que queriam rodar seus jogos. Isso teve o enorme benefício adicional de poder implementar o gerenciamento de memória virtual em sistemas primitivos de 8 bits; um jogo poderia ser maior do que caberia na memória, porque eles poderiam apenas paginar o código do disco quando precisassem e descartá-lo quando precisassem carregar um novo código.