En programación informática , un thunk es una subrutina que se utiliza para inyectar un cálculo en otra subrutina. Los thunks 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 de compilador y en la programación modular .
El término se originó como una forma irregular caprichosa del verbo think (pensar) . Se refiere al uso original de thunks en los compiladores ALGOL 60 , que requerían un análisis especial (pensamiento) para determinar qué tipo de rutina generar. [1] [2]
Los primeros años de investigación de compiladores fueron testigos de una amplia experimentación con diferentes estrategias de evaluación . Una cuestión clave era 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 " llamada por nombre ", la subrutina recibe la expresión del argumento no evaluado y debe evaluarla.
Una implementación simple de "llamada 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 thunk , que calcula el valor del argumento. La dirección y el entorno [a] de esta subrutina auxiliar se pasan entonces 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 thunks en referencia al lenguaje de programación ALGOL 60, que admite la evaluación de llamada 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 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 perezosa en los que alguna variante de llamada por nombre es la estrategia de evaluación estándar. Los compiladores para estos lenguajes, como Glasgow Haskell Compiler , se han basado en gran medida en thunks, con la característica adicional de que los thunks guardan su resultado inicial para poder evitar recalcularlo; [6] esto se conoce como memorización o llamada por necesidad .
Los lenguajes de programación funcional también han permitido a los programadores generar thunks 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 hypot = ( x , y ) => Math . sqrt ( 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 provoca que se produzca algún efecto secundario const thunk = () => hypot ( 3 , 4 ); // el thunk puede luego pasarse sin ser evaluado... doSomethingWithThunk ( thunk );// ...o thunk evaluado (); // === 5
Los thunks son útiles en plataformas de programación orientada a objetos que permiten que una clase herede múltiples interfaces , lo que genera 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 better_value_ ; } privado : int mejor_valor_ ; }; int use ( B * b ) { return b -> Acceso (); } int main () { // ... B algún_b ; uso ( & algún_b ); C algún_c ; uso ( & algún_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 utilizar para llamar Access
a un objeto de ese tipo, a través de una referencia que tenga el mismo tipo. La clase C tendrá una tabla de despacho adicional, que se utiliza para llamar Access
a un objeto de tipo C a través de una referencia de tipo B. La expresión b->Access()
utilizará la propia tabla de despacho de B o la tabla adicional de C, dependiendo del tipo de objeto al que b se refiere. Si se refiere a un objeto de tipo C, el compilador debe asegurarse de que la Access
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 método directo para solucionar este problema de ajuste de punteros, el compilador puede incluir un desplazamiento entero en cada entrada de la tabla de despacho. Este desplazamiento es la diferencia entre la dirección de la 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 recuperar el desplazamiento y utilizarlo para ajustar la dirección de la instancia antes de llamar al método.
La solución que se acaba 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 la instancia), al mismo tiempo que aumenta el tamaño de la tabla de despacho para contener los desplazamientos. Como alternativa, el compilador puede generar un procesador de ajuste 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 los llamadores ajusten la dirección ellos mismos. [9]
Las rutinas para cálculos como la integración necesitan calcular una expresión en varios puntos. La llamada por nombre se utilizó para este propósito en lenguajes que no admitían cierres o parámetros de procedimiento .
Los thunks 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 thunk que automatice los pasos adicionales necesarios para llamar a la rutina de destino, ya sea transformar argumentos, copiarlos a otra ubicación o cambiar el modo de CPU. Un thunk exitoso minimiza el trabajo adicional que debe realizar el llamador en comparación con una llamada normal.
Gran parte de la literatura sobre los 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 del direccionamiento de memoria de 16 bits al de 32 bits . A medida que los clientes han migrado de una plataforma a otra, los procesadores han sido esenciales para dar soporte al software heredado escrito para las plataformas más antiguas.
La transición de código de 32 bits a código de 64 bits en x86 también utiliza una forma de thunk ( 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 "thunk 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 de código de 32 bits que llama a código de 64 bits es en el thunk de WoW64 de 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 a cada segmento. Un segmento que llama a otro segmento debe hacerlo indirectamente a través de una tabla de ramificación . Cuando un segmento está en la memoria, sus entradas de la tabla de ramificación saltan al segmento. Cuando se descarga un segmento, sus entradas se reemplazan con "procesadores de recarga" que pueden volver a cargarlo a pedido. [16]
De manera similar, los sistemas que vinculan dinámicamente módulos de un programa entre sí 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 enlazador completa cuando carga el módulo. De esta manera, los módulos pueden interactuar sin conocimiento previo de dónde se encuentran ubicados en la memoria. [17]
{{cite book}}
: |work=
ignorado ( ayuda )