ACTUALIZACIÓN: Me gustó tanto esta pregunta que la convertí en el tema de mi blog el 18 de noviembre de 2011 . Gracias por la gran pregunta!
Siempre me he preguntado: ¿cuál es el propósito de la pila?
Supongo que te refieres a la pila de evaluación del lenguaje MSIL, y no a la pila real por subproceso en tiempo de ejecución.
¿Por qué hay una transferencia de memoria a pila o "cargando"? Por otro lado, ¿por qué hay una transferencia de la pila a la memoria o "almacenamiento"? ¿Por qué no simplemente tenerlos todos en la memoria?
MSIL es un lenguaje de "máquina virtual". Los compiladores como el compilador de C # generan CIL , y luego, en tiempo de ejecución, otro compilador llamado compilador JIT (Just In Time) convierte el IL en código de máquina real que puede ejecutarse.
Entonces, primero respondamos la pregunta "¿por qué tener MSIL?" ¿Por qué no simplemente hacer que el compilador de C # escriba el código de la máquina?
Porque es más barato hacerlo de esta manera. Supongamos que no lo hicimos de esa manera; supongamos que cada idioma debe tener su propio generador de código de máquina. Tiene veinte lenguajes diferentes: C #, JScript .NET , Visual Basic, IronPython , F # ... Y suponga que tiene diez procesadores diferentes. ¿Cuántos generadores de código tienes que escribir? 20 x 10 = 200 generadores de código. Eso es mucho trabajo. Ahora suponga que desea agregar un nuevo procesador. Tienes que escribir el generador de código veinte veces, uno para cada idioma.
Además, es un trabajo difícil y peligroso. ¡Escribir generadores de códigos eficientes para chips en los que no eres un experto es un trabajo difícil! Los diseñadores de compiladores son expertos en el análisis semántico de su lenguaje, no en la asignación eficiente de registros de nuevos conjuntos de chips.
Ahora supongamos que lo hacemos a la manera CIL. ¿Cuántos generadores CIL tienes que escribir? Uno por idioma. ¿Cuántos compiladores JIT tienes que escribir? Uno por procesador. Total: 20 + 10 = 30 generadores de código. Además, el generador de lenguaje a CIL es fácil de escribir porque CIL es un lenguaje simple, y el generador de código de CIL a máquina también es fácil de escribir porque CIL es un lenguaje simple. Nos deshacemos de todas las complejidades de C # y VB y otras cosas y "reducimos" todo a un lenguaje simple para el cual es fácil escribir un jitter.
Tener un idioma intermedio reduce drásticamente el costo de producir un nuevo compilador de idiomas . También reduce drásticamente el costo de soportar un nuevo chip. Desea admitir un nuevo chip, encuentra algunos expertos en ese chip y hace que escriban una inquietud CIL y listo; entonces soportas todos esos idiomas en tu chip.
Bien, entonces hemos establecido por qué tenemos MSIL; porque tener un idioma intermedio reduce los costos. ¿Por qué entonces el lenguaje es una "máquina de pila"?
Porque las máquinas de pila son conceptualmente muy simples para los escritores de compiladores de idiomas. Las pilas son un mecanismo simple y fácil de entender para describir los cálculos. Las máquinas apiladoras también son conceptualmente muy fáciles de manejar para los escritores de compiladores JIT. El uso de una pila es una abstracción simplificada y, por lo tanto, una vez más, reduce nuestros costos .
Usted pregunta "¿por qué tener una pila?" ¿Por qué no simplemente hacer todo directamente de memoria? Bueno, pensemos en eso. Suponga que desea generar código CIL para:
int x = A() + B() + C() + 10;
Supongamos que tenemos la convención de que "agregar", "llamar", "almacenar", etc., siempre quitan sus argumentos de la pila y colocan su resultado (si hay uno) en la pila. Para generar código CIL para este C #, simplemente decimos 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.
Ahora supongamos que lo hicimos sin una pila. Lo haremos a su manera, donde cada código de operación toma las direcciones de sus operandos y la dirección en la que almacena su 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
...
¿Ves cómo va esto? Nuestro código se está volviendo enorme porque tenemos que asignar explícitamente todo el almacenamiento temporal que normalmente, por convención, simplemente iría a la pila . Peor aún, nuestros códigos de operación se están volviendo enormes porque ahora todos tienen que tomar como argumento la dirección en la que van a escribir su resultado y la dirección de cada operando. Una instrucción de "agregar" que sepa que va a quitar dos cosas de la pila y colocar una puede ser un solo byte. Una instrucción de agregar que toma dos direcciones de operando y una dirección de resultado será enorme.
Utilizamos códigos de operación basados en pila porque las pilas resuelven el problema común . A saber: quiero asignar algo de almacenamiento temporal, usarlo muy pronto y luego deshacerme de él rápidamente cuando haya terminado . Al suponer que tenemos una pila a nuestra disposición, podemos hacer que los códigos de operación sean muy pequeños y el código muy breve.
ACTUALIZACIÓN: algunos pensamientos adicionales
Por cierto, esta idea de reducir drásticamente los costos al (1) especificar una máquina virtual, (2) escribir compiladores que apuntan al lenguaje VM, y (3) escribir implementaciones de la VM en una variedad de hardware, no es una idea nueva en absoluto . No se originó con MSIL, LLVM, código de bytes Java ni ninguna otra infraestructura moderna. La primera implementación de esta estrategia que conozco es la máquina pcode de 1966.
Lo primero que escuché personalmente sobre este concepto fue cuando supe cómo los implementadores de Infocom lograron que Zork funcionara en tantas máquinas tan bien. Especificaron una máquina virtual llamada máquina Z y luego crearon emuladores de máquina Z para todo el hardware en el que querían ejecutar sus juegos. Esto tenía el enorme beneficio adicional de que podían implementar la administración de memoria virtual en sistemas primitivos de 8 bits; un juego podría ser más grande de lo que cabría en la memoria porque podrían simplemente paginar el código desde el disco cuando lo necesitaran y descartarlo cuando necesitaran cargar un nuevo código.