c# - sistemas - ¿Cuál es el propósito de una pila? ¿Por qué lo necesitamos?
sistema de recomendacion tesis (6)
Así que estoy aprendiendo MSIL ahora mismo para aprender a depurar mis aplicaciones C # .NET.
Siempre me he preguntado: ¿cuál es el propósito de la pila?
Solo para poner mi pregunta en contexto:
¿Por qué hay una transferencia de memoria a pila o "cargando"? Por otro lado, ¿por qué hay una transferencia de pila a memoria o "almacenamiento"? ¿Por qué no solo tenerlos a todos en la memoria?
- ¿Es porque es más rápido?
- ¿Es porque está basado en RAM?
- Por eficiencia?
Estoy tratando de entender esto para ayudarme a entender los códigos CIL mucho más profundamente.
ACTUALIZACIÓN: Me gustó esta pregunta tanto que la convertí en 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 hilo 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 pila a memoria o "almacenamiento"? ¿Por qué no solo tenerlos a todos en la memoria?
MSIL es un lenguaje de "máquina virtual". Los compiladores como el compilador C # generan CIL , y luego, en tiempo de ejecución, otro compilador llamado JIT (Just In Time) convierte el IL en un código de máquina real que puede ejecutar.
Entonces, primero respondamos a la pregunta "¿por qué MSIL?" ¿Por qué no 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 tiene que tener su propio generador de código de máquina. Tiene veinte idiomas diferentes: C #, JScript .NET , Visual Basic, IronPython , F# ... Y suponga que tiene diez procesadores diferentes. ¿Cuántos generadores de código tienes para 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ódigo 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 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 "rebajamos" todo a un lenguaje simple para el que es fácil escribir un jitter.
Tener un lenguaje intermedio reduce considerablemente el costo de producir un compilador de lenguaje nuevo. También reduce el costo de soportar dramáticamente un nuevo chip. Quieres apoyar un nuevo chip, encuentras algunos expertos en ese chip y les pides que escriban un jitter de CIL y listo. entonces soportas todos esos idiomas en tu chip.
OK, así que hemos establecido por qué tenemos MSIL; Porque tener un lenguaje intermedio baja los costos. ¿Por qué entonces el lenguaje es una "máquina de pila"?
Debido a que las máquinas de pila son conceptualmente muy simples para los escritores de compiladores de lenguaje. Las pilas son un mecanismo simple y fácil de entender para describir cálculos. Las máquinas de pila también son conceptualmente muy fáciles de manejar para los compiladores JIT. Usar una pila es una abstracción simplificadora y, por lo tanto, nuevamente, reduce nuestros costos .
Usted pregunta "¿por qué tener una pila en absoluto?" ¿Por qué no hacer todo directamente de memoria? Bueno, vamos a pensar en eso. Supongamos que desea generar un 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 ponen su resultado (si lo hay) en la pila. Para generar el código CIL para este C # solo 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 iría a la pila por convención . 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 sabe que va a quitar dos cosas de la pila y poner una cosa puede ser un solo byte. Una instrucción adicional que toma dos direcciones de operandos y una dirección de resultados será enorme.
Usamos opcodes 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 termine . Suponiendo 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 tenso.
ACTUALIZACIÓN: Algunos pensamientos adicionales
Incidentalmente, esta idea de reducir drásticamente los costos al (1) especificar una máquina virtual, (2) escribir compiladores que apunten al lenguaje VM, y (3) escribir implementaciones de la VM en una variedad de hardware, no es una idea nueva. . No se originó con MSIL, LLVM, el código de bytes de Java ni ninguna otra infraestructura moderna. La implementación más temprana de esta estrategia que conozco es la máquina pcode de 1966.
Lo primero que escuché personalmente de este concepto fue cuando aprendí cómo los implementadores de Infocom lograron que Zork funcionara en tantas máquinas diferentes tan bien. Especificaron una máquina virtual llamada Z-machine y luego hicieron emuladores de Z-machine para todo el hardware en el que querían ejecutar sus juegos. Esto tuvo la enorme ventaja 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 solo podían colocar el código desde el disco cuando lo necesitaban y descartarlo cuando necesitaban cargar un nuevo código.
Hay un artículo de Wikipedia muy interesante / detallado sobre esto, Ventajas de los conjuntos de instrucciones de la máquina de pila . Tendría que citarlo por completo, por lo que es más fácil simplemente poner un enlace. Simplemente citaré los subtítulos
- Código objeto muy compacto
- Compiladores simples / intérpretes simples
- Estado mínimo del procesador
Para añadir un poco más a la pregunta de la pila. El concepto de pila deriva del diseño de la CPU donde el código de máquina en la unidad lógica aritmética (ALU) opera en los operandos que se encuentran en la pila. Por ejemplo, una operación de multiplicación puede tomar los dos operandos superiores de la pila, multiplicarlos y colocar el resultado nuevamente en la pila. El lenguaje de máquina normalmente tiene dos funciones básicas para agregar y eliminar operandos de la pila; PUSH y POP. En los dsp (procesador de señal digital) y en los controladores de la máquina de muchos cpu (como el que controla una lavadora) la pila se encuentra en el propio chip. Esto permite un acceso más rápido a la ALU y consolida la funcionalidad requerida en un solo chip.
Si no se sigue el concepto de pila / montón y los datos se cargan en una ubicación de memoria aleatoria O los datos se almacenan desde ubicaciones de memoria aleatorias ... será muy desestructurado y no administrado.
Estos conceptos se utilizan para almacenar datos en una estructura predefinida para mejorar el rendimiento, el uso de la memoria ... y, por lo tanto, se denominan estructuras de datos.
Tenga en cuenta que cuando habla de MSIL, está hablando de instrucciones para una máquina virtual . La máquina virtual utilizada en .NET es una máquina virtual basada en pila. A diferencia de una máquina virtual basada en el registro, la máquina virtual Dalvik utilizada en los sistemas operativos Android es un ejemplo de ello.
La pila en la máquina virtual es virtual, le corresponde al intérprete o al compilador justo a tiempo traducir las instrucciones de la máquina virtual en el código real que se ejecuta en el procesador. Lo que en el caso de .NET es casi siempre un jitter, el conjunto de instrucciones MSIL fue diseñado para ser jit desde el principio. A diferencia del código de bytes de Java, por ejemplo, tiene instrucciones distintas para las operaciones en tipos de datos específicos. Lo que lo hace optimizado para ser interpretado. Sin embargo, en realidad existe un intérprete de MSIL, se usa en .NET Micro Framework. Que se ejecuta en procesadores con recursos muy limitados, no puede pagar la RAM necesaria para almacenar el código de la máquina.
El modelo de código de máquina real es mixto, con una pila y registros. Uno de los grandes trabajos del optimizador de código JIT es encontrar formas de almacenar variables que se mantienen en la pila en los registros, lo que mejora considerablemente la velocidad de ejecución. Un jitter Dalvik tiene el problema opuesto.
De lo contrario, la pila de máquinas es una instalación de almacenamiento muy básica que ha estado presente en los diseños de procesadores durante mucho tiempo. Tiene una muy buena localidad de referencia, una característica muy importante en las CPU modernas que mastican los datos mucho más rápido de lo que la RAM puede proporcionar y admite la recursión. El diseño del lenguaje está muy influenciado por tener una pila, visible en el soporte para variables locales y alcance limitado al cuerpo del método. Un problema importante con la pila es el nombre de este sitio.
Uno puede tener un sistema funcionando sin pilas, usando el estilo de codificación de paso de continuación . Luego, los marcos de llamada se convierten en continuaciones asignadas en el montón de basura recolectada (el recolector de basura necesitaría un poco de pila).
Vea los escritos anteriores de Andrew Appel: Compilar con continuaciones y recolección de basura puede ser más rápido que la asignación de pila
(Puede que esté un poco equivocado hoy debido a problemas de caché)