La mayoría de las arquitecturas que he visto se basan en una pila de llamadas para guardar / restaurar el contexto antes de las llamadas a funciones. Es un paradigma tan común que las operaciones push y pop están integradas en la mayoría de los procesadores. ¿Hay sistemas que funcionan sin una pila? Si es así, ¿cómo funcionan y para qué se utilizan?
computer-architecture
CondiciónRacer
fuente
fuente
Respuestas:
Una alternativa (algo) popular a una pila de llamadas son las continuaciones .
El Parrot VM está basado en la continuación, por ejemplo. Es completamente sin pila: los datos se mantienen en registros (como Dalvik o LuaVM, Parrot está basado en registros), y el flujo de control se representa con continuaciones (a diferencia de Dalvik o LuaVM, que tienen una pila de llamadas).
Otra estructura de datos popular, utilizada típicamente por las máquinas virtuales Smalltalk y Lisp es la pila de espaguetis, que es algo así como una red de pilas.
Como señaló @rwong , el estilo de paso de continuación es una alternativa a una pila de llamadas. Los programas escritos en (o transformados en) estilo de paso de continuación nunca regresan, por lo que no hay necesidad de una pila.
Responde tu pregunta desde una perspectiva diferente: es posible tener una pila de llamadas sin tener una pila separada, asignando los marcos de la pila en el montón. Algunas implementaciones de Lisp y Scheme hacen esto.
fuente
En los viejos tiempos, los procesadores no tenían instrucciones de pila, y los lenguajes de programación no admitían la recursividad. Con el tiempo, cada vez más idiomas optan por admitir la recursividad, y el hardware siguió a la suite con capacidades de asignación de marcos de pila. Este soporte ha variado mucho a lo largo de los años con diferentes procesadores. Algunos procesadores adoptaron marcos de pila y / o registros de puntero de pila; algunas adoptaron instrucciones que lograrían la asignación de marcos de pila en una sola instrucción.
A medida que los procesadores avanzan con un solo nivel y luego con cachés de varios niveles, una ventaja crítica de la pila es la de la localidad de caché. La parte superior de la pila casi siempre está en el caché. Siempre que pueda hacer algo que tenga una gran tasa de aciertos de caché, está en el camino correcto con los procesadores modernos. La memoria caché aplicada a la pila significa que las variables locales, parámetros, etc. casi siempre están en la memoria caché y disfrutan del más alto nivel de rendimiento.
En resumen, el uso de la pila evolucionó tanto en hardware como en software. Existen otros modelos (por ejemplo, la informática de flujo de datos se intentó durante un período prolongado), sin embargo, la localidad de la pila hace que funcione realmente bien. Además, el código de procedimiento es justo lo que los procesadores quieren para el rendimiento: una instrucción que le dice qué hacer después de otra. Cuando las instrucciones están fuera de orden lineal, el procesador se ralentiza enormemente, al menos por el momento, ya que no hemos descubierto cómo hacer que el acceso aleatorio sea tan rápido como el acceso secuencial. (Por cierto, hay problemas similares en cada nivel de memoria, desde caché, memoria principal, disco ...)
Entre el rendimiento demostrado de las instrucciones de acceso secuencial y el comportamiento de almacenamiento en caché beneficioso de la pila de llamadas, tenemos, al menos en la actualidad, un modelo de rendimiento ganador.
(Podríamos incluir mutabilidad de las estructuras de datos en las obras también ...)
Esto no significa que otros modelos de programación no puedan funcionar, especialmente cuando se pueden traducir a las instrucciones secuenciales y al modelo de pila de llamadas del hardware actual. Pero existe una clara ventaja para los modelos que admiten dónde está el hardware. Sin embargo, las cosas no siempre permanecen igual, por lo que podríamos ver cambios en el futuro a medida que diferentes tecnologías de memoria y transistores permitan un mayor paralelismo. Siempre es una broma entre lenguajes de programación y capacidades de hardware, ¡así que ya veremos!
fuente
TL; DR
El resto de esta respuesta es una colección aleatoria de pensamientos y anécdotas, y por lo tanto, algo desorganizada.
La pila que ha descrito (como mecanismo de llamada de función) es específica de la programación imperativa.
Debajo de la programación imperativa, encontrará el código de la máquina. El código de máquina puede emular la pila de llamadas ejecutando una pequeña secuencia de instrucciones.
Debajo del código de máquina, encontrará el hardware responsable de ejecutar el software. Si bien el microprocesador moderno es demasiado complejo para ser descrito aquí, uno puede imaginar que existe un diseño muy simple que es lento pero que aún es capaz de ejecutar el mismo código de máquina. Un diseño tan simple hará uso de los elementos básicos de la lógica digital:
Las siguientes discusiones contenían muchos ejemplos de formas alternativas de estructurar programas imperativos.
La estructura de tal programa se verá así:
Este estilo sería apropiado para microcontroladores, es decir, para aquellos que ven el software como un complemento de las funciones del hardware.
fuente
No, no necesariamente
Lea la recolección de basura en papel viejo de Appel puede ser más rápido que la asignación de pila . Utiliza el estilo de paso continuo y muestra una implementación sin pila.
Observe también que las arquitecturas informáticas antiguas (por ejemplo, IBM / 360 ) no tenían ningún registro de pila de hardware. Pero el sistema operativo y el compilador reservaron un registro para el puntero de la pila por convención (relacionado con las convenciones de llamada ) para que pudieran tener una pila de llamadas de software .
En principio, todo un compilador y optimizador de programa C podría detectar el caso (algo común para los sistemas integrados) en el que el gráfico de llamadas es estáticamente conocido y sin recurrencia (o punteros de función). En dicho sistema, cada función podría mantener su dirección de retorno en una ubicación estática fija (y así fue como Fortran77 funcionaba en las computadoras de la era de 1970).
En estos días, los procesadores también tienen pilas de llamadas (e instrucciones de máquina de llamada y devolución) conscientes de los cachés de la CPU .
fuente
SUBROUTINE
yFUNCTION
. Sin embargo, tiene razón para versiones anteriores (FORTRAN-IV y posiblemente WATFIV).TR
yTRT
.Tienes algunas buenas respuestas hasta ahora; déjame darte un ejemplo poco práctico pero altamente educativo de cómo puedes diseñar un lenguaje sin la noción de pilas o "flujo de control". Aquí hay un programa que determina los factoriales:
Ponemos este programa en una cadena y evaluamos el programa mediante sustitución textual. Entonces, cuando estamos evaluando
f(3)
, hacemos una búsqueda y reemplazamos con 3 para i, así:Excelente. Ahora realizamos otra sustitución textual: vemos que la condición del "si" es falsa y reemplazamos otra cadena, produciendo el programa:
Ahora hacemos otra cadena de reemplazo en todas las subexpresiones que involucran constantes:
Y ya ves cómo va esto; No voy a trabajar más el punto. Podríamos seguir haciendo una serie de sustituciones de cuerdas hasta que nos pongamos manos a la obra
let x = 6
y hayamos terminado.Usamos la pila tradicionalmente para variables locales e información de continuación; recuerda, una pila no te dice de dónde vienes, te dice a dónde vas después con ese valor de retorno en la mano.
En el modelo de programación de sustitución de cadenas, no hay "variables locales" en la pila; los parámetros formales se sustituyen por sus valores cuando la función se aplica a su argumento, en lugar de colocarse en una tabla de búsqueda en la pila. Y no hay "ir a otro lado" porque la evaluación del programa es simplemente aplicar reglas simples para la sustitución de cadenas para producir un programa diferente pero equivalente.
Ahora, por supuesto, hacer sustituciones de cuerdas probablemente no sea el camino a seguir. Pero los lenguajes de programación que admiten el "razonamiento equitativo" (como Haskell) utilizan lógicamente esta técnica.
fuente
Desde la publicación de Parnas en 1972 de Sobre los criterios que se utilizarán para descomponer los sistemas en módulos, se ha aceptado razonablemente que la información oculta en el software es algo bueno. Esto sigue a un largo debate a lo largo de los años 60 sobre la descomposición estructural y la programación modular.
Modularidad
Un resultado necesario de las relaciones de caja negra entre módulos implementados por diferentes grupos en cualquier sistema de subprocesos múltiples requiere un mecanismo que permita la reentrada y un medio para rastrear el gráfico dinámico de llamadas del sistema. El flujo controlado de ejecución tiene que pasar dentro y fuera de múltiples módulos.
Alcance dinámico
Tan pronto como el alcance léxico sea insuficiente para rastrear el comportamiento dinámico, se requiere cierta contabilidad en tiempo de ejecución para rastrear la diferencia.
Dado que cualquier subproceso (por definición) tiene solo un puntero de instrucción actual, una pila LIFO es apropiada para rastrear cada invocación.
Excepciones
Por lo tanto, si bien el modelo de continuación no mantiene una estructura de datos explícitamente para la pila, ¡todavía hay una llamada anidada de módulos que debe mantenerse en alguna parte!
Incluso los lenguajes declarativos mantienen el historial de evaluación o, por el contrario, aplanan el plan de ejecución por razones de rendimiento y mantienen el progreso de alguna otra manera.
La estructura de bucle sin fin identificada por rwong es común en aplicaciones de alta confiabilidad con programación estática que no permite muchas estructuras de programación comunes pero exige que toda la aplicación se considere una caja blanca sin ocultar información significativa.
Múltiples bucles sin fin concurrentes no requieren ninguna estructura para contener direcciones de retorno ya que no llaman a funciones, lo que hace que la pregunta sea discutible. Si se comunican usando variables compartidas, estas pueden degenerar fácilmente en análogos de direcciones de retorno de estilo Fortran heredados.
fuente
Todos los mainframes antiguos (IBM System / 360) no tenían ninguna noción de pila. En el 260, por ejemplo, los parámetros se construyeron en una ubicación fija en la memoria y cuando se llamó a una subrutina, se llamó
R1
apuntando al bloque de parámetros yR14
conteniendo la dirección de retorno. La rutina llamada, si quería llamar a otra subrutina, tendría que almacenarseR14
en una ubicación conocida antes de hacer esa llamada.Esto es mucho más confiable que una pila porque todo se puede almacenar en ubicaciones de memoria fija establecidas en el momento de la compilación y se puede garantizar al 100% que los procesos nunca se quedarán sin pila. No hay ninguno de los "Asignar 1MB y cruzar los dedos" que tenemos que hacer hoy en día.
Se permitieron llamadas de subrutina recursivas en PL / I especificando la palabra clave
RECURSIVE
. Significaron que la memoria utilizada por la subrutina era asignada dinámicamente en lugar de estáticamente. Pero las llamadas recursivas eran tan raras como lo son ahora.La operación sin apilamiento también facilita mucho el subprocesamiento múltiple masivo, por lo que a menudo se hacen intentos para que los lenguajes modernos no tengan acecho. No hay ninguna razón en absoluto, por ejemplo, por qué un compilador de C ++ no puede modificarse en el back-end para usar memoria asignada dinámicamente en lugar de pilas.
fuente