En programación de computadoras , un procesador es una subrutina que se utiliza para inyectar un cálculo en otra subrutina. Los procesadores se utilizan principalmente para retrasar un cálculo hasta que se necesite su resultado, o para insertar operaciones al principio o al final de la otra subrutina. Tienen muchas otras aplicaciones en la generación de código compilador y programación modular .
El término se originó como una forma irregular caprichosa del verbo pensar . Se refiere al uso original de procesadores en los compiladores ALGOL 60 , que requerían un análisis (pensamiento) especial para determinar qué tipo de rutina generar. [1] [2]
Los primeros años de la investigación de los compiladores vieron una amplia experimentación con diferentes estrategias de evaluación . Una pregunta clave fue cómo compilar una llamada a una subrutina si los argumentos pueden ser expresiones matemáticas arbitrarias en lugar de constantes. Un enfoque, conocido como " llamada por valor ", calcula todos los argumentos antes de la llamada y luego pasa los valores resultantes a la subrutina. En el enfoque rival " llamar por nombre ", la subrutina recibe la expresión del argumento no evaluado y debe evaluarla.
Una implementación simple de "llamar por nombre" podría sustituir el código de una expresión de argumento por cada aparición del parámetro correspondiente en la subrutina, pero esto puede producir múltiples versiones de la subrutina y múltiples copias del código de expresión. Como mejora, el compilador puede generar una subrutina auxiliar, llamada procesador , que calcula el valor del argumento. La dirección y el entorno [a] de esta subrutina auxiliar luego se pasan a la subrutina original en lugar del argumento original, donde se puede llamar tantas veces como sea necesario. Peter Ingerman describió por primera vez los procesadores en referencia al lenguaje de programación ALGOL 60, que admite la evaluación de llamadas por nombre. [4]
Aunque la industria del software estandarizó en gran medida la evaluación de llamada por valor y llamada por referencia , [5] el estudio activo de la llamada por nombre continuó en la comunidad de programación funcional . Esta investigación produjo una serie de lenguajes de programación de evaluación perezosos en los que alguna variante de llamada por nombre es la estrategia de evaluación estándar. Los compiladores de estos lenguajes, como el Glasgow Haskell Compiler , han dependido en gran medida de los procesadores, con la característica adicional de que los procesadores guardan su resultado inicial para evitar volver a calcularlo; [6] esto se conoce como memorización o llamada por necesidad .
Los lenguajes de programación funcionales también han permitido a los programadores generar procesadores explícitamente. Esto se hace en el código fuente envolviendo una expresión de argumento en una función anónima que no tiene parámetros propios. Esto evita que la expresión se evalúe hasta que una función receptora llame a la función anónima, logrando así el mismo efecto que la llamada por nombre. [7] La adopción de funciones anónimas en otros lenguajes de programación ha hecho que esta capacidad esté ampliamente disponible.
La siguiente es una demostración sencilla en JavaScript (ES6):
// 'hypot' es una función binaria const hipot = ( x , y ) => Math . raíz cuadrada ( x * x + y * y ); // 'thunk' es una función que no toma argumentos y, cuando se invoca, realiza una // operación potencialmente costosa (calcular una raíz cuadrada, en este ejemplo) y/o causa algún efecto secundario const thunk = () = > hipot ( 3 , 4 ); // el procesador puede pasarse sin ser evaluado... doSomethingWithThunk ( thunk );// ...o procesador evaluado (); // === 5
Los procesadores son útiles en plataformas de programación orientada a objetos que permiten que una clase herede múltiples interfaces , lo que lleva a situaciones en las que se puede llamar al mismo método a través de cualquiera de varias interfaces. El siguiente código ilustra una situación de este tipo en C++ .
clase A { público : virtual int Access () const { valor de retorno_ ; } privado : int valor_ ; }; clase B { público : virtual int Access () const { valor de retorno_ ; } privado : int valor_ ; }; clase C : público A , público B { público : int Access () const override { return mejor_valor_ ; } privado : int mejor_valor_ ; }; int uso ( B * b ) { return b -> Acceso (); } int main () { // ... B some_b ; uso ( & some_b ); Calgo_c ; uso ( & some_c ); }
En este ejemplo, el código generado para cada una de las clases A, B y C incluirá una tabla de despacho que se puede usar para llamar Access
a un objeto de ese tipo, a través de una referencia que tiene el mismo tipo. La clase C tendrá una tabla de despacho adicional, utilizada para llamar Access
a un objeto de tipo C a través de una referencia de tipo B. La expresión b->Access()
usará la tabla de despacho propia de B o la tabla C adicional, dependiendo del tipo de objeto al que se refiere b. Si se refiere a un objeto de tipo C, el compilador debe asegurarse de que Access
la implementación de C reciba una dirección de instancia para todo el objeto C, en lugar de la parte B heredada de ese objeto. [8]
Como enfoque directo a este problema de ajuste del puntero, el compilador puede incluir un desplazamiento de número entero en cada entrada de la tabla de distribución. Este desplazamiento es la diferencia entre la dirección de referencia y la dirección requerida por la implementación del método. El código generado para cada llamada a través de estas tablas de despacho debe luego recuperar el desplazamiento y usarlo para ajustar la dirección de la instancia antes de llamar al método.
La solución que acabamos de describir tiene problemas similares a la implementación ingenua de llamada por nombre descrita anteriormente: el compilador genera varias copias de código para calcular un argumento (la dirección de instancia), al mismo tiempo que aumenta el tamaño de la tabla de despacho para contener las compensaciones. Como alternativa, el compilador puede generar un procesador ajustador junto con la implementación de C Access
que ajusta la dirección de la instancia en la cantidad requerida y luego llama al método. El procesador puede aparecer en la tabla de despacho de C para B, eliminando así la necesidad de que las personas que llaman ajusten la dirección ellos mismos. [9]
Las rutinas para cálculos como la integración necesitan calcular una expresión en múltiples puntos. La llamada por nombre se utilizó para este propósito en idiomas que no admitían cierres o parámetros de procedimiento .
Los procesadores se han utilizado ampliamente para proporcionar interoperabilidad entre módulos de software cuyas rutinas no pueden llamarse entre sí directamente. Esto puede ocurrir porque las rutinas tienen diferentes convenciones de llamada , se ejecutan en diferentes modos de CPU o espacios de direcciones , o al menos una se ejecuta en una máquina virtual . Un compilador (u otra herramienta) puede resolver este problema generando un procesador que automatice los pasos adicionales necesarios para llamar a la rutina de destino, ya sea transformando argumentos, copiándolos en otra ubicación o cambiando el modo de CPU. Un procesador exitoso minimiza el trabajo adicional que debe realizar la persona que llama en comparación con una llamada normal.
Gran parte de la literatura sobre procesadores de interoperabilidad se relaciona con varias plataformas Wintel , incluidas MS-DOS , OS/2 , [10] Windows [11] [12] [13] [14] y .NET , y con la transición de 16 bits. a direccionamiento de memoria de 32 bits . A medida que los clientes migraron de una plataforma a otra, los procesadores han sido esenciales para admitir el software heredado escrito para las plataformas más antiguas.
La transición del código de 32 bits a 64 bits en x86 también utiliza una forma de procesador ( WoW64 ). Sin embargo, debido a que el espacio de direcciones x86-64 es mayor que el disponible para el código de 32 bits, el antiguo mecanismo de "procesador genérico" no se podía utilizar para llamar a código de 64 bits desde código de 32 bits. [15] El único caso en el que un código de 32 bits llama a un código de 64 bits es cuando WoW64 convierte las API de Windows a 32 bits.
En sistemas que carecen de hardware de memoria virtual automática , los procesadores pueden implementar una forma limitada de memoria virtual conocida como superposiciones . Con las superposiciones, un desarrollador divide el código de un programa en segmentos que se pueden cargar y descargar de forma independiente e identifica los puntos de entrada en cada segmento. Un segmento que llama a otro segmento debe hacerlo indirectamente a través de una tabla de rama . Cuando un segmento está en la memoria, sus entradas de la tabla de ramas saltan al segmento. Cuando se descarga un segmento, sus entradas se reemplazan con "thunks de recarga" que pueden recargarlo a pedido. [16]
De manera similar, los sistemas que vinculan dinámicamente módulos de un programa en tiempo de ejecución pueden usar procesadores para conectar los módulos. Cada módulo puede llamar a los demás a través de una tabla de procesadores que el vinculador completa cuando carga el módulo. De esta manera los módulos pueden interactuar sin conocimiento previo de dónde están ubicados en la memoria. [17]
{{cite book}}
: |work=
ignorado ( ayuda )