Saltar al contenido

¿Cuál es el propósito de una pila? ¿Por qué lo necesitamos?

Solución:

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 al pila de evaluación del lenguaje MSIL, y no la pila real por subproceso en tiempo de ejecución.

¿Por qué hay una transferencia de la memoria a la pila o “cargando”? Por otro lado, ¿por qué hay una transferencia de la pila a la memoria o “almacenamiento”? ¿Por qué no guardarlos 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 se puede ejecutar.

Entonces, primero respondamos a la pregunta “¿por qué tener MSIL en absoluto?” ¿Por qué no hacer que el compilador de C # escriba el código de máquina?

Porque es más económico para hacerlo de esta manera. Supongamos que no lo hicimos de esa manera; supongamos que cada lenguaje tiene que 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ódigos. Eso es mucho trabajo. Ahora suponga que desea agregar un nuevo procesador. Tienes que escribir el generador de código veinte veces, una 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 suponga que lo hacemos de 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ódigos. 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 todo eso y “reducimos” todo a un lenguaje simple en el que es fácil escribir un jitter.

Tener un lenguaje intermedio reduce el costo de producir un nuevo compilador de lenguaje dramáticamente. También reduce drásticamente el costo de soportar un nuevo chip. Si desea admitir un nuevo chip, encuentra algunos expertos en ese chip y pídeles que escriban un jitter CIL y listo; a continuación, admite todos esos idiomas en su chip.

Bien, hemos establecido por qué tenemos MSIL; porque tener un idioma intermedio abarata los costos. Entonces, ¿por qué el lenguaje es una “máquina de apilar”?

Porque las máquinas de pila son conceptualmente muy simples de manejar 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 escritores de 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 la 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 ponen 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 suponga que lo hicimos sin una pila. Lo haremos a tu 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 está recibiendo 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 “agregar” que sabe que va a quitar dos cosas de la pila y poner una cosa puede ser un solo byte. Una instrucción de suma que toma dos direcciones de operando y una dirección de resultado será enorme.

Usamos 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 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 conciso.

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 apunten 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 de Java ni ninguna otra infraestructura moderna. La primera implementación de esta estrategia que conozco es la máquina pcode de 1966.

La primera vez que escuché personalmente sobre este concepto fue cuando supe cómo los implementadores de Infocom lograron que Zork funcionara tan bien en tantas máquinas diferentes. 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 añadido de que podían implementar gestió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 simplemente podían paginar el código del disco cuando lo necesitaban y descartarlo cuando necesitaban cargar un nuevo código.

Tenga en cuenta que cuando se habla de MSIL, se habla de instrucciones para un virtual máquina. La máquina virtual utilizada en .NET es una máquina virtual basada en pila. A diferencia de una máquina virtual basada en registros, la máquina virtual Dalvik utilizada en los sistemas operativos Android es un ejemplo de eso.

La pila en la VM es virtual, depende del intérprete o del compilador justo a tiempo traducir las instrucciones de la VM al 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 jitted desde el principio. A diferencia del código de bytes de Java, por ejemplo, tiene instrucciones distintas para operaciones en tipos de datos específicos. Lo que lo hace optimizado para ser interpretado. Sin embargo, existe un intérprete de MSIL que se utiliza en .NET Micro Framework. Que se ejecuta en procesadores con recursos muy limitados, no puede permitirse la RAM necesaria para almacenar el código de la máquina.

El modelo de código de máquina real es mixto y tiene tanto una pila como 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 registros, mejorando así en gran medida la velocidad de ejecución. Un jitter de Dalvik tiene el problema opuesto.

La pila de máquinas es, por lo demás, una instalación de almacenamiento muy básica que ha existido 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 procesan datos mucho más rápido de lo que la RAM puede proporcionar y admite la recursividad. El diseño del lenguaje está fuertemente influenciado por tener una pila, visible en soporte para variables locales y alcance limitado al cuerpo del método. Un problema importante con la pila es el que da nombre a este sitio.

Hay un artículo de Wikipedia muy interesante / detallado sobre esto, Ventajas de los conjuntos de instrucciones de la máquina de pila. Necesitaría 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
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *