En informática , ingeniería informática e implementaciones de lenguajes de programación , una máquina de pila es un procesador de computadora o una máquina virtual en la que la interacción principal es mover valores temporales de corta duración hacia y desde una pila de inserción . En el caso de un procesador de hardware, se utiliza una pila de hardware . El uso de una pila reduce significativamente la cantidad requerida de registros de procesador . Las máquinas de pila extienden los autómatas de inserción con operaciones de carga/almacenamiento adicionales o múltiples pilas y, por lo tanto, son Turing-completas .
La mayoría o todas las instrucciones de la máquina de pila suponen que los operandos serán de la pila y los resultados se colocarán en la pila. La pila contiene fácilmente más de dos entradas o más de un resultado, por lo que se puede calcular un rico conjunto de operaciones. En el código de máquina de pila (a veces llamado código p ), las instrucciones con frecuencia tendrán solo un código de operación que ordene una operación, sin campos adicionales que identifiquen una constante, registro o celda de memoria, conocido como formato de dirección cero . [1] Se dice que una computadora que opera de tal manera que la mayoría de sus instrucciones no incluyen direcciones explícitas utiliza instrucciones de dirección cero. [2] Esto simplifica enormemente la decodificación de instrucciones. Las bifurcaciones, las cargas inmediatas y las instrucciones de carga/almacenamiento requieren un campo de argumento, pero las máquinas de pila a menudo organizan que los casos frecuentes de estos aún encajen con el código de operación en un grupo compacto de bits . La selección de operandos de resultados anteriores se realiza implícitamente ordenando las instrucciones. Algunos conjuntos de instrucciones de máquina de pila están destinados a la ejecución interpretativa de una máquina virtual, en lugar de controlar el hardware directamente.
Los operandos de constantes enteras se introducen mediante instrucciones Push
o Load Immediate
. A menudo, se accede a la memoria mediante instrucciones Load
o independientes que contienen una dirección de memoria o calculan la dirección a partir de valores en la pila. Todas las máquinas de pila prácticas tienen variantes de los códigos de operación de carga y almacenamiento para acceder a variables locales y parámetros formales sin cálculos de dirección explícitos. Esto puede hacerse mediante desplazamientos desde la dirección actual de la parte superior de la pila o mediante desplazamientos desde un registro estable de base de marco.Store
El conjunto de instrucciones lleva a cabo la mayoría de las acciones de la ALU con operaciones de sufijo ( notación polaca inversa ) que funcionan solo en la pila de expresiones, no en registros de datos o celdas de memoria principal. Esto puede resultar muy conveniente para ejecutar lenguajes de alto nivel porque la mayoría de las expresiones aritméticas se pueden traducir fácilmente a notación de sufijo.
Por ejemplo, considere la expresión A *( B - C )+( D + E ), escrita en notación polaca inversa como A B C - * D E + +. Compilarla y ejecutarla en una máquina de pila imaginaria simple tomaría la forma:
# contenido de la pila (más a la izquierda = arriba = más reciente): Empuja A # A Empuja B # BA Empujar C#CBA restar # BC A multiplicar # A*(BC) Empujar D # DA*(BC) Empujar E # EDA*(BC) suma # D+EA*(BC) suma # A*(BC)+(D+E)
Las operaciones aritméticas 'restar', 'multiplicar' y 'sumar' actúan sobre los dos operandos superiores de la pila. La computadora toma ambos operandos de los valores superiores (más recientes) de la pila y reemplaza esos dos valores con la diferencia, suma o producto calculado. En otras palabras, los operandos de la instrucción se "extraen" de la pila y sus resultados se "vuelven a colocar" en la pila, listos para la siguiente instrucción.
Las máquinas de pila pueden tener su pila de expresión y su pila de llamada-retorno separadas o como una estructura integrada. Si están separadas, las instrucciones de la máquina de pila pueden canalizarse con menos interacciones y menor complejidad de diseño, por lo que normalmente se ejecutará más rápido.
La optimización del código compilado es bastante posible. Se ha demostrado que la optimización del back-end de la salida del compilador mejora significativamente el código [3] [4] y, potencialmente, el rendimiento, mientras que la optimización global dentro del propio compilador logra mayores ganancias [5] .
Algunas máquinas de pila tienen una pila de tamaño limitado, implementada como un archivo de registro. La ALU accederá a ella con un índice. Un archivo de registro grande utiliza muchos transistores y, por lo tanto, este método solo es adecuado para sistemas pequeños. Algunas máquinas tienen una pila de expresiones en memoria y una pila de registros separada. En este caso, el software o una interrupción pueden mover datos entre ellas. Algunas máquinas tienen una pila de tamaño ilimitado, implementada como una matriz en RAM, que se almacena en caché mediante una cierta cantidad de registros de dirección "en la parte superior de la pila" para reducir el acceso a la memoria. A excepción de las instrucciones explícitas de "carga desde la memoria", el orden de uso de los operandos es idéntico al orden de los operandos en la pila de datos, por lo que se puede lograr fácilmente una excelente precarga.
Considere X+1
. Se compila a Load X
; Load 1
; Add
. Con una pila almacenada completamente en RAM, esto realiza escrituras y lecturas implícitas de la pila en memoria:
para un total de 5 referencias de caché de datos.
El siguiente paso es una máquina de pila o intérprete con un único registro en la parte superior de la pila. El código anterior hace lo siguiente:
para un total de 3 referencias de caché de datos, en el peor de los casos. Generalmente, los intérpretes no rastrean el vacío, porque no tienen que hacerlo: todo lo que está por debajo del puntero de pila es un valor no vacío y el registro de caché TOS siempre se mantiene activo. Sin embargo, los intérpretes Java típicos no almacenan en búfer la parte superior de la pila de esta manera, porque el programa y la pila tienen una mezcla de valores de datos cortos y anchos.
Si la máquina de pila cableada tiene 2 o más registros en la parte superior de la pila, o un archivo de registro, entonces se evita todo acceso a la memoria en este ejemplo y solo hay 1 ciclo de caché de datos.
La descripción de un método que requiere que sólo se almacenen dos valores a la vez en los registros, con un conjunto limitado de operandos predefinidos que se pueden ampliar mediante la definición de otros operandos, funciones y subrutinas, fue proporcionada por primera vez en una conferencia por Robert S. Barton en 1961. [6] [7]
Los ejemplos de conjuntos de instrucciones de pila ejecutados directamente en hardware incluyen
Ejemplos de máquinas de pila virtuales interpretadas en software:
Las máquinas de pila pura son bastante ineficientes para los procedimientos que acceden a múltiples campos del mismo objeto. El código de la máquina de pila debe recargar el puntero del objeto para cada cálculo de puntero+desplazamiento. Una solución habitual para esto es agregar algunas características de máquina de registro a la máquina de pila: un archivo de registro visible dedicado a almacenar direcciones e instrucciones de estilo de registro para realizar cargas y cálculos de direcciones simples. Es poco común que los registros sean de propósito general completo, porque entonces no hay una razón sólida para tener una pila de expresiones e instrucciones de posfijo.
Otro híbrido común es comenzar con una arquitectura de máquina de registros y agregar otro modo de dirección de memoria que emula las operaciones push o pop de las máquinas de pila: 'memaddress = reg; reg += instr.displ'. Esto se usó por primera vez en la minicomputadora PDP-11 de DEC . [26] Esta característica se trasladó a las computadoras VAX y a los microprocesadores Motorola 6800 y M68000 . Esto permitió el uso de métodos de pila más simples en los primeros compiladores. También admitió de manera eficiente máquinas virtuales que usaban intérpretes de pila o código enhebrado . Sin embargo, esta característica no ayudó a que el código propio de la máquina de registros se volviera tan compacto como el código de máquina de pila puro. Además, la velocidad de ejecución era menor que cuando se compilaba bien con la arquitectura de registros. Es más rápido cambiar el puntero superior de la pila solo ocasionalmente (una vez por llamada o retorno) en lugar de subirlo y bajarlo constantemente a lo largo de cada declaración del programa, y es aún más rápido evitar las referencias de memoria por completo.
Más recientemente, las llamadas máquinas de pila de segunda generación han adoptado una colección dedicada de registros que sirven como registros de dirección, descargando la tarea de direccionamiento de memoria de la pila de datos. Por ejemplo, MuP21 se basa en un registro llamado "A", mientras que los procesadores GreenArrays más recientes se basan en dos registros: A y B. [23]
La familia de microprocesadores Intel x86 tiene un conjunto de instrucciones de tipo registro (acumulador) para la mayoría de las operaciones, pero utiliza instrucciones de pila para su aritmética de punto flotante x87 , Intel 8087 , que se remonta al coprocesador iAPX87 (8087) para el 8086 y el 8088. Es decir, no hay registros de punto flotante accesibles para el programador, sino solo una pila de 80 bits de ancho y 8 niveles de profundidad. El x87 depende en gran medida de la CPU x86 para obtener asistencia en la realización de sus operaciones.
La mayoría de las computadoras actuales (de cualquier estilo de conjunto de instrucciones) y la mayoría de los compiladores utilizan una gran pila de llamadas y retornos en la memoria para organizar las variables locales de corta duración y los enlaces de retorno para todos los procedimientos o funciones activos en ese momento. Cada llamada anidada crea un nuevo marco de pila en la memoria, que persiste hasta que se completa esa llamada. Esta pila de llamadas y retornos puede ser administrada completamente por el hardware a través de registros de dirección especializados y modos de dirección especiales en las instrucciones. O puede ser simplemente un conjunto de convenciones seguidas por los compiladores, utilizando registros genéricos y modos de dirección de registro+desplazamiento. O puede ser algo intermedio.
Dado que esta técnica es ahora casi universal, incluso en máquinas de registro, no resulta útil referirse a todas estas máquinas como máquinas de pila. Ese término se reserva comúnmente para máquinas que también utilizan una pila de expresiones e instrucciones aritméticas de solo pila para evaluar las partes de una sola instrucción.
Las computadoras suelen proporcionar acceso directo y eficiente a las variables globales del programa y a las variables locales únicamente del procedimiento o función más interno actual, el marco de pila superior. El direccionamiento de "nivel superior" del contenido de los marcos de pila de los invocadores no suele ser necesario y el hardware no lo admite de forma directa. Si es necesario, los compiladores admiten esto al pasar punteros de marco como parámetros ocultos adicionales.
Algunas máquinas de pila Burroughs admiten referencias de nivel superior directamente en el hardware, con modos de dirección especializados y un archivo de registro de "visualización" especial que contiene las direcciones de marco de todos los ámbitos externos. Actualmente, solo MCST Elbrus ha hecho esto en hardware. Cuando Niklaus Wirth desarrolló el primer compilador Pascal para el CDC 6000 , descubrió que era más rápido en general pasar los punteros de marco como una cadena, en lugar de actualizar constantemente matrices completas de punteros de marco. Este método de software tampoco agrega sobrecarga para lenguajes comunes como C, que carecen de referencias de nivel superior.
Las mismas máquinas Burroughs también admitían la anidación de tareas o subprocesos. La tarea y su creador comparten los marcos de pila que existían en el momento de la creación de la tarea, pero no los marcos posteriores del creador ni los marcos propios de la tarea. Esto se apoyaba en una pila de cactus , cuyo diagrama de diseño se parecía al tronco y los brazos de un cactus saguaro . Cada tarea tenía su propio segmento de memoria que contenía su pila y los marcos que le pertenecían. La base de esta pila está vinculada a la mitad de la pila de su creador. En máquinas con un espacio de direcciones plano convencional, la pila del creador y las pilas de tareas serían objetos de montón separados en un solo montón.
En algunos lenguajes de programación, los entornos de datos de ámbito externo no siempre están anidados en el tiempo. Estos lenguajes organizan sus "registros de activación" de procedimientos como objetos de montón separados en lugar de como marcos de pila anexados a una pila lineal.
En lenguajes simples como Forth, que carecen de variables locales y nombres de parámetros, los marcos de pila no contendrían nada más que direcciones de rama de retorno y sobrecarga de administración de marcos. Por lo tanto, su pila de retorno contiene direcciones de retorno simples en lugar de marcos. La pila de retorno está separada de la pila de valores de datos, para mejorar el flujo de configuración de llamadas y retornos.
Las máquinas de pila suelen compararse con las máquinas de registro, que almacenan valores en una matriz de registros . Las máquinas de registro pueden almacenar estructuras similares a las de pila en esta matriz, pero una máquina de registro tiene instrucciones que evitan la interfaz de pila. Las máquinas de registro superan habitualmente a las máquinas de pila, [27] y las máquinas de pila han seguido siendo un actor de nicho en los sistemas de hardware. Pero las máquinas de pila se utilizan a menudo para implementar máquinas virtuales debido a su simplicidad y facilidad de implementación. [28]
Las máquinas de pila tienen una mayor densidad de código . A diferencia de las instrucciones de máquina de pila comunes que pueden caber fácilmente en 6 bits o menos, las máquinas de registro requieren dos o tres campos de número de registro por instrucción ALU para seleccionar operandos; las máquinas de registro más densas tienen un promedio de unos 16 bits por instrucción más los operandos. Las máquinas de registro también utilizan un campo de desplazamiento más amplio para los códigos de operación de carga y almacenamiento. El código compacto de una máquina de pila naturalmente cabe más instrucciones en la caché y, por lo tanto, podría lograr una mejor eficiencia de caché , reduciendo los costos de memoria o permitiendo sistemas de memoria más rápidos por un costo determinado. Además, la mayoría de las instrucciones de máquina de pila son muy simples, hechas de solo un campo de código de operación o un campo de operando. Por lo tanto, las máquinas de pila requieren muy pocos recursos electrónicos para decodificar cada instrucción.
Un programa debe ejecutar más instrucciones cuando se compila en una máquina de pila que cuando se compila en una máquina de registro o de memoria a memoria. Cada carga variable o constante requiere su propia instrucción Load independiente, en lugar de estar agrupada dentro de la instrucción que utiliza ese valor. Las instrucciones separadas pueden ser simples y ejecutarse más rápido, pero el recuento total de instrucciones sigue siendo mayor.
La mayoría de los intérpretes de registros especifican sus registros por número. Pero no se puede acceder a los registros de una máquina anfitriona en una matriz indexada, por lo que se asigna una matriz de memoria para los registros virtuales. Por lo tanto, las instrucciones de un intérprete de registros deben usar memoria para pasar los datos generados a la siguiente instrucción. Esto obliga a los intérpretes de registros a ser mucho más lentos en microprocesadores fabricados con una regla de proceso fina (es decir, transistores más rápidos sin mejorar las velocidades del circuito, como el Haswell x86). Estos requieren varios relojes para el acceso a la memoria, pero solo un reloj para el acceso a los registros. En el caso de una máquina de pila con un circuito de reenvío de datos en lugar de un archivo de registros, los intérpretes de pila pueden asignar los registros de la máquina anfitriona para los primeros operandos de la pila en lugar de la memoria de la máquina anfitriona.
En una máquina de pila, los operandos utilizados en las instrucciones siempre se encuentran en un desplazamiento conocido (establecido en el puntero de pila), desde una ubicación fija (la parte inferior de la pila, que en un diseño de hardware podría estar siempre en la ubicación de memoria cero), lo que permite ahorrar valioso almacenamiento en caché o en CPU que se utiliza para almacenar tantas direcciones de memoria o números de índice. Esto puede preservar dichos registros y caché para su uso en computación sin flujo. [ cita requerida ]
Algunos en la industria creen que las máquinas de pila ejecutan más ciclos de caché de datos para valores temporales y variables locales que las máquinas de registro. [29]
En las máquinas de pila, los valores temporales suelen volcarse en la memoria, mientras que en las máquinas con muchos registros estos valores temporales suelen permanecer en los registros. (Sin embargo, estos valores a menudo deben volcarse en "marcos de activación" al final de la definición de un procedimiento, un bloque básico o, como mínimo, en un búfer de memoria durante el procesamiento de interrupciones). Los valores volcados en la memoria agregan más ciclos de caché. Este efecto de volcado depende de la cantidad de registros ocultos utilizados para almacenar en búfer los valores de la parte superior de la pila, de la frecuencia de las llamadas a procedimientos anidados y de las tasas de procesamiento de interrupciones del equipo host.
En las máquinas de registro que utilizan compiladores optimizadores, es muy común que las variables locales más utilizadas permanezcan en los registros en lugar de en las celdas de memoria del marco de pila. Esto elimina la mayoría de los ciclos de caché de datos para leer y escribir esos valores. El desarrollo de la "programación de pila" para realizar análisis de variables en vivo y, por lo tanto, retener variables clave en la pila durante períodos prolongados, ayuda a solucionar este problema. [3] [4] [5]
Por otro lado, las máquinas de registro deben volcar muchos de sus registros en la memoria a través de llamadas a procedimientos anidados. La decisión de qué registros volcar y cuándo se debe hacerlo se toma de forma estática en el momento de la compilación en lugar de en función de la profundidad dinámica de las llamadas. Esto puede generar más tráfico de caché de datos que en una implementación de máquina de pila avanzada.
En las máquinas de registro, una subexpresión común (una subexpresión que se utiliza varias veces con el mismo valor de resultado) se puede evaluar solo una vez y su resultado se puede guardar en un registro rápido. Las reutilizaciones posteriores no tienen costo de tiempo ni de código, solo una referencia de registro. Esta optimización acelera las expresiones simples (por ejemplo, cargar la variable X o el puntero P) así como las expresiones complejas menos comunes.
En cambio, con las máquinas de pila, los resultados se pueden almacenar de dos maneras. En primer lugar, los resultados se pueden almacenar utilizando una variable temporal en la memoria. El almacenamiento y las recuperaciones posteriores cuestan instrucciones adicionales y ciclos de caché de datos adicionales. Hacer esto solo es una ventaja si el cálculo de la subexpresión cuesta más tiempo que la recuperación de la memoria, que en la mayoría de las CPU de pila, casi siempre es el caso. Nunca vale la pena para las variables simples y las recuperaciones de punteros, porque ya tienen el mismo costo de un ciclo de caché de datos por acceso. Solo vale la pena marginalmente para expresiones como X+1
. Estas expresiones más simples constituyen la mayoría de las expresiones redundantes y optimizables en programas escritos en lenguajes distintos de los lenguajes concatenativo . Un compilador optimizador solo puede ganar en redundancias que el programador podría haber evitado en el código fuente. [ cita requerida ]
La segunda forma deja un valor calculado en la pila de datos y lo duplica según sea necesario. Esto utiliza operaciones para copiar las entradas de la pila. La pila debe tener la profundidad suficiente para las instrucciones de copia disponibles de la CPU. El código de pila escrito a mano a menudo utiliza este enfoque y logra velocidades similares a las de las máquinas de registro de propósito general. [30] [9] Desafortunadamente, los algoritmos para una "programación de pila" óptima no se utilizan ampliamente en los lenguajes de programación.
En las máquinas modernas, el tiempo necesario para recuperar una variable de la caché de datos suele ser varias veces mayor que el tiempo necesario para las operaciones básicas de ALU. Un programa se ejecuta más rápido sin paradas si sus cargas de memoria pueden iniciarse varios ciclos antes de la instrucción que necesita esa variable. Las máquinas complejas pueden hacer esto con una secuencia de comandos profunda y una "ejecución fuera de orden" que examina y ejecuta muchas instrucciones a la vez. Las máquinas de registro pueden incluso hacer esto con un hardware "en orden" mucho más simple, una secuencia de comandos poco profunda y compiladores ligeramente más inteligentes. El paso de carga se convierte en una instrucción independiente, y esa instrucción se programa estáticamente mucho antes en la secuencia de código. El compilador coloca pasos independientes entre ellos.
La programación de accesos a memoria requiere registros explícitos y de repuesto. No es posible en máquinas de pila sin exponer algún aspecto de la microarquitectura al programador. Para la expresión AB -, B debe evaluarse y empujarse inmediatamente antes del paso Minus. Sin permutación de pila o multihilo de hardware, se puede colocar relativamente poco código útil mientras se espera que finalice la carga B. Las máquinas de pila pueden evitar el retraso de memoria ya sea con una tubería de ejecución desordenada profunda que cubra muchas instrucciones a la vez o, más probablemente, pueden permutar la pila de modo que puedan trabajar en otras cargas de trabajo mientras se completa la carga, o pueden entrelazar la ejecución de diferentes hilos de programa, como en el sistema Unisys A9. [31] Sin embargo, las cargas computacionales cada vez más paralelas de la actualidad sugieren que esto podría no ser la desventaja que se ha hecho creer en el pasado.
Las máquinas de pila pueden omitir la etapa de obtención de operandos de una máquina de registro. [30] Por ejemplo, en el microprocesador Java Optimized Processor (JOP) los 2 operandos superiores de la pila ingresan directamente a un circuito de reenvío de datos que es más rápido que el archivo de registro. [32]
El algoritmo de Tomasulo encuentra paralelismo a nivel de instrucción al emitir instrucciones a medida que sus datos están disponibles. Conceptualmente, las direcciones de las posiciones en una pila no son diferentes a los índices de registro de un archivo de registros. Esta visión permite la ejecución fuera de orden del algoritmo de Tomasulo para su uso con máquinas de pila.
La ejecución fuera de orden en las máquinas de pila parece reducir o evitar muchas dificultades teóricas y prácticas. [33] La investigación citada muestra que una máquina de pila de este tipo puede explotar el paralelismo a nivel de instrucción, y el hardware resultante debe almacenar en caché los datos de las instrucciones. Estas máquinas evitan eficazmente la mayoría de los accesos a la memoria de la pila. El resultado logra un rendimiento (instrucciones por ciclo de reloj ) comparable a las máquinas de arquitectura de carga-almacenamiento , con densidades de código mucho más altas (porque las direcciones de operandos son implícitas).
Un problema que se planteó en la investigación fue que se necesitan aproximadamente 1,88 instrucciones de máquina de pila para realizar el trabajo de una instrucción en una máquina con arquitectura de carga y almacenamiento. [ cita requerida ] Por lo tanto, las máquinas de pila fuera de orden competitivas requieren aproximadamente el doble de recursos electrónicos para realizar el seguimiento de las instrucciones ("estaciones de emisión"). Esto podría compensarse con ahorros en caché y memoria de instrucciones y circuitos de decodificación de instrucciones.
Algunas máquinas de pila simples tienen un diseño de chip que está totalmente personalizado hasta el nivel de los registros individuales. El registro de dirección de la parte superior de la pila y los N buffers de datos de la parte superior de la pila se construyen a partir de circuitos de registro individuales separados, con sumadores separados y conexiones ad hoc.
Sin embargo, la mayoría de las máquinas de pila se construyen a partir de componentes de circuitos más grandes donde los N buffers de datos se almacenan juntos dentro de un archivo de registro y comparten buses de lectura/escritura. Las instrucciones de pila decodificadas se asignan a una o más acciones secuenciales en ese archivo de registro oculto. Las operaciones de carga y ALU actúan sobre unos pocos registros superiores, y los desbordamientos y rellenos implícitos actúan sobre los registros inferiores. El decodificador permite que el flujo de instrucciones sea compacto. Pero si el flujo de código tuviera campos de selección de registro explícitos que manipularan directamente el archivo de registro subyacente, el compilador podría hacer un mejor uso de todos los registros y el programa se ejecutaría más rápido.
Las máquinas de pila microprogramadas son un ejemplo de esto. El motor de microcódigo interno es una especie de máquina de registros similar a RISC o una máquina similar a VLIW que utiliza múltiples archivos de registros. Cuando se controla directamente mediante un microcódigo específico de la tarea, ese motor realiza mucho más trabajo por ciclo que cuando se controla indirectamente mediante un código de pila equivalente para esa misma tarea.
Los traductores de código objeto para HP 3000 y Tandem T/16 son otro ejemplo. [34] [35] Tradujeron secuencias de código de pila en secuencias equivalentes de código RISC. Pequeñas optimizaciones "locales" eliminaron gran parte de la sobrecarga de una arquitectura de pila. Se utilizaron registros de repuesto para factorizar cálculos de direcciones repetidos. El código traducido aún conservaba mucha sobrecarga de emulación debido a la falta de coincidencia entre las máquinas original y de destino. A pesar de esa carga, la eficiencia de ciclo del código traducido coincidía con la eficiencia de ciclo del código de pila original. Y cuando el código fuente se recompiló directamente en la máquina de registros a través de compiladores optimizadores, la eficiencia se duplicó. Esto demuestra que la arquitectura de pila y sus compiladores no optimizadores estaban desperdiciando más de la mitad de la potencia del hardware subyacente.
Los archivos de registro son buenas herramientas para la computación porque tienen un alto ancho de banda y una latencia muy baja, en comparación con las referencias de memoria a través de cachés de datos. En una máquina simple, el archivo de registro permite leer dos registros independientes y escribir un tercero, todo en un ciclo de ALU con una latencia de un ciclo o menos. Mientras que el caché de datos correspondiente puede iniciar solo una lectura o una escritura (no ambas) por ciclo, y la lectura normalmente tiene una latencia de dos ciclos de ALU. Eso es un tercio del rendimiento con el doble de retraso de canalización. En una máquina compleja como Athlon que completa dos o más instrucciones por ciclo, el archivo de registro permite leer cuatro o más registros independientes y escribir otros dos, todo en un ciclo de ALU con una latencia de un ciclo. Mientras que el caché de datos de puerto doble correspondiente puede iniciar solo dos lecturas o escrituras por ciclo, con múltiples ciclos de latencia. Nuevamente, eso es un tercio del rendimiento de los registros. Es muy costoso construir un caché con puertos adicionales.
Dado que una pila es un componente de la mayoría de los programas de software, incluso cuando el software utilizado no es estrictamente una máquina de pila, una máquina de pila de hardware podría imitar más de cerca el funcionamiento interno de sus programas. Los registros del procesador tienen un alto costo térmico y una máquina de pila podría afirmar tener una mayor eficiencia energética. [22]
Para responder a una interrupción es necesario guardar los registros en una pila y luego pasar al código de manejo de interrupciones. A menudo, las máquinas de pila responden más rápidamente a las interrupciones, porque la mayoría de los parámetros ya están en una pila y no es necesario colocarlos allí. Algunas máquinas de registro se ocupan de esto al tener varios archivos de registro que se pueden intercambiar instantáneamente [36], pero esto aumenta los costos y ralentiza el archivo de registro.
Los intérpretes para máquinas de pila virtuales son más fáciles de construir que los intérpretes para máquinas de registro; la lógica para manejar los modos de dirección de memoria está en un solo lugar en lugar de repetirse en muchas instrucciones. Las máquinas de pila también tienden a tener menos variaciones de un código de operación; un código de operación generalizado manejará tanto casos frecuentes como casos especiales poco conocidos de referencias de memoria o configuración de llamadas de función. (Pero la densidad del código a menudo se mejora agregando formas cortas y largas para la misma operación).
Los intérpretes para máquinas de pila virtual suelen ser más lentos que los intérpretes para otros estilos de máquinas virtuales. [37] Esta desaceleración es peor cuando se ejecuta en máquinas host con canales de ejecución profundos, como los chips x86 actuales.
En algunos intérpretes, el intérprete debe ejecutar un salto de conmutación de N vías para decodificar el siguiente código de operación y pasar a sus pasos para ese código de operación en particular. Otro método para seleccionar códigos de operación es el código enhebrado . Los mecanismos de precarga de la máquina anfitriona no pueden predecir ni buscar el objetivo de ese salto indexado o indirecto. Por lo tanto, la secuencia de ejecución de la máquina anfitriona debe reiniciarse cada vez que el intérprete alojado decodifica otra instrucción virtual. Esto sucede con más frecuencia en las máquinas de pila virtual que en otros estilos de máquina virtual. [38]
Un ejemplo es el lenguaje de programación Java . Su máquina virtual canónica se especifica como una máquina de pila de 8 bits. Sin embargo, la máquina virtual Dalvik para Java utilizada en los teléfonos inteligentes Android es una máquina de registro virtual de 16 bits, una elección hecha por razones de eficiencia. Las instrucciones aritméticas obtienen o almacenan directamente variables locales a través de campos de instrucciones de 4 bits (o más grandes). [39] De manera similar, la versión 5.0 de Lua reemplazó su máquina de pila virtual con una máquina de registro virtual más rápida. [40] [41]
Desde que la máquina virtual Java se hizo popular, los microprocesadores han empleado predictores de bifurcaciones avanzados para saltos indirectos. [42] Este avance evita la mayoría de los reinicios de canalización a partir de saltos de N vías y elimina gran parte de los costos de conteo de instrucciones que afectan a los intérpretes de pila.