En informática , el código enhebrado es una técnica de programación en la que el código tiene una forma que consiste esencialmente en llamadas a subrutinas . A menudo se utiliza en compiladores , que pueden generar código en esa forma o implementarse en esa forma ellos mismos. El código puede ser procesado por un intérprete o puede ser simplemente una secuencia de instrucciones de llamada de código de máquina .
El código enhebrado tiene mejor densidad que el código generado por técnicas de generación alternativas y por convenciones de llamadas alternativas . En arquitecturas en caché, puede ejecutarse un poco más lento. [ cita requerida ] Sin embargo, un programa que es lo suficientemente pequeño como para caber en la caché de un procesador de computadora puede ejecutarse más rápido que un programa más grande que sufre muchas fallas de caché . [1] Los programas pequeños también pueden ser más rápidos en el cambio de hilo, cuando otros programas han llenado el caché.
El código enhebrado es mejor conocido por su uso en muchos compiladores de lenguajes de programación , como Forth , muchas implementaciones de BASIC , algunas implementaciones de COBOL , primeras versiones de B , [2] y otros lenguajes para pequeñas minicomputadoras y satélites de radioaficionados . [ cita requerida ]
Historia
La forma común de hacer programas de computadora es usar un compilador para traducir el código fuente (escrito en algún lenguaje simbólico ) a código de máquina . El ejecutable resultante suele ser rápido pero, debido a que es específico de una plataforma de hardware , no es portátil. Un enfoque diferente es generar instrucciones para una máquina virtual y utilizar un intérprete en cada plataforma de hardware. El intérprete crea una instancia del entorno de la máquina virtual y ejecuta las instrucciones. Por lo tanto, solo se debe compilar el intérprete.
Las primeras computadoras tenían relativamente poca memoria. Por ejemplo, la mayoría de Data General Nova , IBM 1130 y muchas de las primeras microcomputadoras tenían solo 4 kB de RAM instalados. En consecuencia, se dedicó mucho tiempo a tratar de encontrar formas de reducir el tamaño de un programa para que quepa en la memoria disponible.
Una solución es utilizar un intérprete que lea el lenguaje simbólico poco a poco y llame a funciones para realizar las acciones. Como el código fuente suele ser mucho más denso que el código de máquina resultante, esto puede reducir el uso general de la memoria. Esta fue la razón por la que Microsoft BASIC es un intérprete: [a] su propio código tenía que compartir la memoria de 4 kB de máquinas como Altair 8800 con el código fuente del usuario. Un compilador traduce de un lenguaje fuente a un código de máquina, por lo que el compilador, la fuente y la salida deben estar todos en la memoria al mismo tiempo. En un intérprete, no hay salida. El código se crea una línea a la vez, se ejecuta y luego se descarta.
El código enhebrado es un estilo de formato para código compilado que minimiza el uso de memoria. En lugar de escribir cada paso de una operación cada vez que ocurre en el programa, como era común en los ensambladores de macros, por ejemplo, el compilador escribe cada bit de código común en una subrutina. Por lo tanto, cada bit existe en un solo lugar de la memoria (consulte " No se repita "). La aplicación de nivel superior en estos programas puede consistir en nada más que llamadas a subrutinas. Muchas de estas subrutinas, a su vez, también consisten en nada más que llamadas a subrutinas de nivel inferior.
Los mainframes y algunos de los primeros microprocesadores, como el RCA 1802, requerían varias instrucciones para llamar a una subrutina. En la aplicación de nivel superior y en muchas subrutinas, esa secuencia se repite constantemente, y solo la dirección de la subrutina cambia de una llamada a la siguiente. Esto significa que un programa que consta de muchas llamadas a funciones también puede tener cantidades considerables de código repetido.
Para abordar esto, los sistemas de código enhebrado utilizaron pseudocódigo para representar llamadas a funciones en un solo operador. En tiempo de ejecución, un pequeño "intérprete" escaneaba el código de nivel superior, extraía la dirección de la subrutina en la memoria y la llamaba. En otros sistemas, este mismo concepto básico se implementa como una tabla de rama , una tabla de despacho o una tabla de método virtual , todas las cuales consisten en una tabla de direcciones de subrutina.
Durante la década de 1970, los diseñadores de hardware dedicaron un esfuerzo considerable a realizar llamadas de subrutinas de forma más rápida y sencilla. En los diseños mejorados, solo se gasta una única instrucción para llamar a una subrutina, por lo que el uso de una pseudoinstrucción no ahorra espacio. [ cita requerida ] Además, el rendimiento de estas llamadas está casi libre de gastos generales adicionales. Hoy en día, aunque casi todos los lenguajes de programación se centran en aislar el código en subrutinas, lo hacen por claridad y mantenimiento del código, no para ahorrar espacio.
Los sistemas de código enhebrado ahorran espacio al reemplazar esa lista de llamadas a funciones, donde solo la dirección de subrutina cambia de una llamada a la siguiente, con una lista de tokens de ejecución, que son esencialmente llamadas a funciones con los códigos de operación de la llamada eliminados, dejando atrás solo una lista de direcciones. [3] [4] [5] [6] [7]
A lo largo de los años, los programadores han creado muchas variaciones de ese "intérprete" o "pequeño selector". La dirección particular en la lista de direcciones se puede extraer usando un índice, registro de propósito general o puntero . Las direcciones pueden ser directas o indirectas, contiguas o no contiguas (enlazadas por punteros), relativas o absolutas, resueltas en tiempo de compilación o construidas dinámicamente. Ninguna variación es la "mejor" para todas las situaciones.
Desarrollo
Para ahorrar espacio, los programadores comprimieron las listas de llamadas a subrutinas en listas simples de direcciones de subrutinas y utilizaron un pequeño bucle para llamar a cada subrutina por turno. Por ejemplo, el siguiente pseudocódigo usa esta técnica para sumar dos números A y B. En el ejemplo, la lista está etiquetada como hilo y una variable ip (Puntero de instrucción) rastrea nuestro lugar dentro de la lista. Otra variable sp (Stack Pointer) contiene una dirección en otra parte de la memoria que está disponible para contener un valor temporalmente.
start : ip = & thread // apunta a la dirección '& pushA', no a la etiqueta textual 'thread' top : jump * ip ++ // sigue ip a la dirección en el hilo, sigue esa dirección a la subrutina, avanza el hilo ip : & pushA & pushB & add ... pushA : * sp ++ = A // siga sp a la memoria disponible, almacene A allí, avance sp al siguiente salto superior pushB : * sp ++ = B salto superior add : addend1 = * - - sp // Extrae el valor superior de la pila addend2 = * - sp // Extrae el segundo valor de la pila * sp ++ = addend1 + addend2 // Suma los dos valores y almacena el resultado en la parte superior de la pila saltar arriba
El ciclo de llamada en top
es tan simple que se puede repetir en línea al final de cada subrutina. El control ahora salta una vez, desde el final de una subrutina hasta el comienzo de otra, en lugar de saltar dos veces top
. Por ejemplo:
start : ip = & thread // ip apunta a & pushA (que apunta a la primera instrucción de pushA) jump * ip ++ // envía el control a la primera instrucción de pushA y avanza ip a & pushB thread : & pushA & pushB & add . .. pushA : * sp ++ = A // sigue sp a la memoria disponible, almacena A allí, avanza sp al siguiente salto * ip ++ // envía el control donde ip dice to (es decir, pushB) y avanza ip pushB : * sp ++ = B jump * ip ++ add : addend1 = * - sp // Extrae el valor superior de la pila addend2 = * - sp // Extrae el segundo valor de la pila * sp ++ = addend1 + addend2 / / Suma los dos valores y almacena el resultado en la parte superior de la pila jump * ip ++
Esto se denomina código de subproceso directo (DTC). Aunque la técnica es más antigua, el primer uso ampliamente difundido del término "código enhebrado" es probablemente el artículo de 1973 de James R. Bell "Código enhebrado". [8]
En 1970, Charles H. Moore inventó una disposición más compacta, código de subprocesos indirectos (ITC), para su máquina virtual Forth. Moore llegó a este arreglo porque las miniordenadores Nova tenían un bit indirecto en cada dirección, lo que hacía que ITC fuera fácil y rápido. Más tarde, dijo que lo encontró tan conveniente que lo propagó en todos los diseños posteriores de Forth. [9]
Hoy en día, algunos compiladores de Forth generan código con subprocesos directos, mientras que otros generan código con subprocesos indirectos. Los ejecutables actúan de la misma manera de cualquier manera.
Modelos de roscado
Prácticamente todo el código ejecutable con subprocesos utiliza uno u otro de estos métodos para invocar subrutinas (cada método se denomina "modelo de subprocesos").
Enhebrado directo
Las direcciones en el hilo son las direcciones del lenguaje de máquina. Este formulario es simple, pero puede tener gastos generales porque el hilo consta solo de direcciones de máquina, por lo que todos los parámetros adicionales deben cargarse indirectamente desde la memoria. Algunos sistemas Forth producen código directo. En muchas máquinas, el enhebrado directo es más rápido que el enhebrado de subrutina (consulte la referencia a continuación).
Un ejemplo de una máquina apiladora podría ejecutar la secuencia "empujar A, empujar B, agregar". Eso podría traducirse al siguiente hilo y rutinas, donde ip
se inicializa en la dirección etiquetada thread
(es decir, la dirección donde &pushA
se almacena).
#define PUSH (x) (* sp ++ = (x)) #define POP () (* - sp) start : ip = & thread // ip apunta a & pushA (que apunta a la primera instrucción de pushA) jump * ip ++ // enviar control a la primera instrucción de pushA y avanzar ip a & pushB thread : & pushA & pushB & add ... pushA : PUSH ( A ) jump * ip ++ // enviar control donde ip dice (es decir, pushB ) y avanzar ip pushB : PUSH ( B ) saltar * ip ++ añadir : resultado = POP () + POP () PUSH ( resultado ) saltar * ip ++
Alternativamente, se pueden incluir operandos en el hilo. Esto puede eliminar algunas indirecciones necesarias anteriormente, pero hace que el hilo sea más grande:
#define PUSH (x) (* sp ++ = (x)) #define POP () (* - sp) start : ip = & thread jump * ip ++ thread : & push & A // dirección donde se almacena A, no literal A & push & B & add ... push : variable_address = * ip ++ // debe mover ip más allá de la dirección del operando, ya que no es una dirección de subrutina PUSH ( * variable_address ) // Leer el valor de la variable y presionar pila saltar * ip ++ añadir : resultado = POP () + POP () PUSH ( resultado ) saltar * ip ++
Enhebrado indirecto
El subprocesamiento indirecto utiliza punteros a ubicaciones que, a su vez, apuntan al código de máquina. El puntero indirecto puede ir seguido de operandos que se almacenan en el "bloque" indirecto en lugar de almacenarlos repetidamente en el hilo. Por tanto, el código indirecto suele ser más compacto que el código directo. La indirección generalmente lo hace más lento, aunque generalmente aún más rápido que los intérpretes de código de bytes. Cuando los operandos del manejador incluyen tanto valores como tipos, el ahorro de espacio sobre el código de subprocesamiento directo puede ser significativo. Los sistemas FORTH más antiguos suelen producir código de subprocesos indirectos.
Por ejemplo, si el objetivo es ejecutar "empujar A, empujar B, agregar", se podría usar lo siguiente. Aquí, ip
se inicializa para direccionar &thread
, cada fragmento de código ( push
, add
) se encuentra mediante direccionamiento doble indirecto a través ip
y un bloque indirecto; y todos los operandos del fragmento se encuentran en el bloque indirecto que sigue a la dirección del fragmento. Esto requiere mantener la subrutina actualip
, a diferencia de todos los ejemplos anteriores en los que contenía la siguiente subrutina a llamar.
start : ip = & thread // apunta a '& i_pushA' jump * ( * ip ) // sigue los punteros a la primera instrucción de 'push', NO avance ip todavía hilo : & i_pushA & i_pushB & i_add ... i_pushA : & push & A i_pushB : & push & B i_add : & add push : * sp ++ = * ( * ip + 1 ) // mira 1 después del inicio del bloque indirecto para el salto de dirección del operando * ( * ++ ip ) // avanza ip en el hilo, saltar del siguiente bloque indirecto a la siguiente subrutina agregar : addend1 = * - sp addend2 = * - sp * sp ++ = addend1 + addend2 jump * ( * ++ ip )
Enhebrado de subrutinas
El llamado "código subproceso de subrutina" (también "código de subproceso de llamada") consiste en una serie de instrucciones de "llamada" en lenguaje de máquina (o direcciones de funciones para "llamar", en contraposición al uso de subprocesos directos de "saltar" ). Los primeros compiladores de ALGOL , Fortran, Cobol y algunos sistemas Forth a menudo producían código subrutina. El código en muchos de estos sistemas operaba en una pila de operandos de último en entrar, primero en salir (LIFO), para lo cual la teoría del compilador estaba bien desarrollada. La mayoría de los procesadores modernos tienen soporte de hardware especial para instrucciones de "llamada" y "devolución" de subrutinas, por lo que la sobrecarga de una instrucción de máquina adicional por envío se reduce un poco.
Anton Ertl, el co-creador del compilador de Gforth , afirmó que "a diferencia de los mitos populares, el subproceso de subrutinas suele ser más lento que el subproceso directo". [10] Sin embargo, las pruebas más recientes de Ertl [1] muestran que el enhebrado de subrutinas es más rápido que el enhebrado directo en 15 de los 25 casos de prueba. Más específicamente, descubrió que el subprocesamiento directo es el modelo de subprocesamiento más rápido en los procesadores Xeon, Opteron y Athlon, el subproceso indirecto es más rápido en los procesadores Pentium M y el subprocesamiento de subrutinas es más rápido en los procesadores Pentium 4, Pentium III y PPC.
Como ejemplo de subprocesamiento de llamadas para "presionar A, presionar B, agregar":
hilo : llamar pushA llamar pushB llamar add ret pushA : * sp ++ = A ret pushB : * sp ++ = B ret add : addend1 = * - sp addend2 = * - sp * sp ++ = addend1 + addend2 ret
Subprocesamiento de tokens
El código con subprocesos de tokens implementa el subproceso como una lista de índices en una tabla de operaciones; el ancho del índice se elige naturalmente para que sea lo más pequeño posible para la densidad y la eficiencia. 1 byte / 8 bits es la elección natural para facilitar la programación, pero se pueden utilizar tamaños más pequeños como 4 bits, o más grandes como 12 o 16 bits, según la cantidad de operaciones admitidas. Siempre que se elija que el ancho del índice sea más estrecho que el de un puntero de máquina, naturalmente será más compacto que los otros tipos de roscado sin mucho esfuerzo especial por parte del programador. Por lo general, tiene entre la mitad y las tres cuartas partes del tamaño de otros subprocesos, que a su vez son un cuarto o un octavo del tamaño del código sin subprocesos. Los indicadores de la tabla pueden ser indirectos o directos. Algunos compiladores de Forth producen código con subprocesos de tokens. Algunos programadores consideran que el " código p " generado por algunos compiladores de Pascal , así como los códigos de bytes utilizados por .NET , Java , BASIC y algunos compiladores de C , son subprocesos de tokens.
Históricamente, un enfoque común es el código de bytes , que generalmente usa códigos de operación de 8 bits con una máquina virtual basada en pilas. El intérprete de código de bytes arquetípico se conoce como un "intérprete de decodificación y envío" y sigue la forma:
start : vpc = & thread dispatch : addr = decode ( & vpc ) // Convierta la siguiente operación de bytecode en un puntero al código de máquina que lo implementa // Aquí se realizan todas las operaciones entre instrucciones (por ejemplo, actualización del estado global, procesamiento de eventos, etc) jump addr CODE_PTR decode ( BYTE_CODE ** p ) { // En una codificación más compleja, puede haber varias tablas entre las que elegir o las banderas de control / modo return table [ * ( * p ) ++ ]; } hilo : / * Contiene código de bytes, no direcciones de máquina. Por tanto, es más compacto. * / 1 / * pushA * / 2 / * pushB * / 0 / * add * / table : & add / * table [0] = dirección del código de máquina que implementa el bytecode 0 * / & pushA / * table [1]. .. * / & pushB / * table [2] ... * / pushA : * sp ++ = Un envío de salto pushB : * sp ++ = B envío de salto add : addend1 = * - sp addend2 = * - sp * sp ++ = addend1 + addend2 salto despacho
Si la máquina virtual usa solo instrucciones de tamaño de byte, decode()
es simplemente una búsqueda de thread
, pero a menudo hay instrucciones de 1 byte de uso común más algunas instrucciones multibyte menos comunes (ver computadora de conjunto de instrucciones complejas ), en cuyo caso decode()
es más complejo. La decodificación de códigos de operación de un solo byte puede ser manejada de manera muy simple y eficiente mediante una tabla de bifurcaciones usando el código de operación directamente como índice.
Para instrucciones donde las operaciones individuales son simples, como "empujar" y "agregar", la sobrecarga involucrada en decidir qué ejecutar es mayor que el costo de ejecutarlo realmente, por lo que dichos intérpretes suelen ser mucho más lentos que el código de máquina. Sin embargo, para instrucciones más complejas ("compuestas"), el porcentaje de gastos generales es proporcionalmente menos significativo.
Hay momentos en los que el código con subprocesos de tokens a veces puede ejecutarse más rápido que el código de máquina equivalente cuando ese código de máquina termina siendo demasiado grande para caber en la caché de instrucciones L1 de la CPU física. La mayor densidad de código del código con subprocesos, especialmente el código con subprocesos de tokens, puede permitirle que quepa completamente en la caché L1 cuando de otra manera no lo haría, evitando así la eliminación de la caché. Sin embargo, el código enhebrado consume tanto el caché de instrucciones (para la implementación de cada operación) como el caché de datos (para el código de bytes y las tablas) a diferencia del código de máquina que solo consume el caché de instrucciones; esto significa que el código enhebrado consumirá el presupuesto de la cantidad de datos que la CPU puede almacenar para procesar en un momento dado. En cualquier caso, si el problema que se está calculando implica aplicar una gran cantidad de operaciones a una pequeña cantidad de datos, entonces el uso de código enhebrado puede ser una optimización ideal. [4]
Enhebrado de Huffman
El código enhebrado de Huffman consta de listas de tokens almacenados como códigos de Huffman . Un código Huffman es una cadena de bits de longitud variable que identifica un token único. Un intérprete con subprocesos de Huffman ubica subrutinas usando una tabla de índice o un árbol de punteros que pueden ser navegados por el código de Huffman. El código con subprocesos de Huffman es una de las representaciones más compactas conocidas de un programa informático. El índice y los códigos se eligen midiendo la frecuencia de llamadas a cada subrutina del código. Las llamadas frecuentes reciben los códigos más cortos. Las operaciones con frecuencias aproximadamente iguales reciben códigos con longitudes de bits casi iguales. La mayoría de los sistemas con subprocesos de Huffman se han implementado como sistemas Forth con subprocesos directos y se han utilizado para empaquetar grandes cantidades de código de ejecución lenta en microcontroladores pequeños y baratos . La mayoría de los usos publicados [11] han sido en tarjetas inteligentes, juguetes, calculadoras y relojes. El código tokenizado orientado a bits que se utiliza en PBASIC puede verse como una especie de código con subprocesos de Huffman.
Roscado menos utilizado
Un ejemplo es el subproceso de cadenas, en el que las operaciones se identifican mediante cadenas, generalmente buscadas mediante una tabla hash. Esto se utilizó en las primeras implementaciones Forth de Charles H. Moore y en el lenguaje informático experimental interpretado por hardware de la Universidad de Illinois . También se utiliza en Bashforth .
RPL
El RPL de HP , introducido por primera vez en la calculadora HP-18C en 1986, es un tipo de lenguaje híbrido patentado de subprocesos directos e indirectos interpretados por subprocesos que, a diferencia de otros TIL, permite incrustar "objetos" RPL en el "flujo de ejecución " es decir. El flujo de direcciones a través del cual avanza el puntero del intérprete. Un "objeto" RPL puede considerarse como un tipo de datos especial cuya estructura en memoria contiene una dirección a un "prólogo de objeto" al comienzo del objeto, y luego siguen los datos o el código ejecutable. El prólogo del objeto determina cómo se debe ejecutar o procesar el cuerpo del objeto. Utilizando el "bucle interno RPL", [12] que fue inventado y publicado (y patentado [13] ) por William C. Wickes en 1986 y publicado en "Programming Environments", Institute for Applied Forth Research, Inc., 1988, ejecución sigue así:
- Desreferenciar la IP (puntero de instrucción) y almacenarla en O (puntero de objeto actual)
- Incrementar la IP por la longitud de un puntero de dirección
- Desreferenciar O y almacenar su dirección en O_1 (este es el segundo nivel de indirección)
- Transfiera el control al siguiente puntero u objeto incrustado configurando la PC (contador de programa) en O_1 más un puntero de dirección
- Regrese al paso 1
Esto se puede representar de manera más precisa por:
O = [I] Yo = yo + Δ PC = [O] + Δ
Donde arriba, O es el puntero del objeto actual, I es el puntero del intérprete, Δ es la longitud de una palabra de dirección y el operador "[]" significa "desreferencia".
Cuando el control se transfiere a un puntero de objeto o un objeto incrustado, la ejecución continúa de la siguiente manera:
PROLOG -> PROLOG (La dirección del prólogo al comienzo del código del prólogo apunta a sí mismo) SI O + Δ = / = PC LUEGO GOTO INDIRECTO (Prueba para ejecución directa) O = I - Δ (Corrija O para apuntar al inicio del objeto incrustado) I = I + α (Corrija I para señalar después del objeto incrustado donde α es la longitud del objeto) INDIRECTO (resto del prólogo)
En los microprocesadores Saturn de HP que utilizan RPL, existe un tercer nivel de direccionamiento indirecto que es posible gracias a un truco de arquitectura / programación que permite una ejecución más rápida. [12]
Sucursales
En todos los intérpretes, una rama simplemente cambia el puntero del hilo ( ip
arriba). Una rama condicional, para saltar si el valor de la parte superior de la pila es cero, podría codificarse de la siguiente manera. Tenga en cuenta que &thread[123]
es la ubicación a la que saltar, no la dirección de un controlador. Por lo tanto, debe omitirse ( ip++
) independientemente de si se toma la rama.
hilo : ... & brz & hilo [ 123 ] ... brz : tmp = ip ++ if ( * sp ++ == 0 ) ip = tmp salto * ip ++
Servicios comunes
La separación de las pilas de datos y devoluciones en una máquina elimina una gran cantidad de código de gestión de la pila, lo que reduce sustancialmente el tamaño del código enhebrado. El principio de doble pila se originó tres veces de forma independiente: para los sistemas grandes de Burroughs , Forth y PostScript . Se utiliza en algunas máquinas virtuales Java .
A menudo, hay tres registros presentes en una máquina virtual con subprocesos. Existe otro para pasar datos entre subrutinas ('palabras'). Estos son:
- ip o i ( puntero de instrucción ) de la máquina virtual (no confundir con el contador de programa del hardware subyacente que implementa la VM)
- w (puntero de trabajo)
- rp or r ( puntero de pila de retorno )
- sp o s ( puntero de pila de parámetros para pasar parámetros entre palabras)
A menudo, las máquinas virtuales con subprocesos , como las implementaciones de Forth, tienen una máquina virtual simple en el corazón, que consta de tres primitivas . Esos son:
- nido , también llamado docol
- unnest , o semi_s (; s)
- Siguiente
En una máquina virtual de subprocesos indirectos, la que se proporciona aquí, las operaciones son:
siguiente : * ip ++ -> w salto ** w ++ nest : ip -> * rp ++ w -> ip siguiente unnest : * - rp -> ip siguiente
Esta es quizás [ cita requerida ] el intérprete o máquina virtual más simple y rápida.
Ver también
- Estilo de paso de continuación , que reemplaza la variable global
ip
con un parámetro de función - Recopilación justo a tiempo
- Programación orientada al retorno : el redescubrimiento de código enhebrado para explotar sistemas vulnerables remotos.
- Recursión de cola
Notas
- ^ Dartmouth BASIC , en el que se basa en última instancia MS, era un compilador que se ejecutaba en máquinas mainframe.
Referencias
- ^ a b "Velocidad de varias técnicas de envío de intérpretes V2" .
- ^ Dennis M. Ritchie, "El desarrollo del lenguaje C" , 1993. Cita: "El compilador B en el PDP-7 no generó instrucciones de máquina, sino 'código enhebrado' ..."
- ^ David Frech. "Muforth readme" . sección "Compilador nativo simple y recursivo en cola".
- ^ a b Steve Heller. "Programación C / C ++ eficiente: más pequeño, más rápido, mejor" . 2014. Capítulo 5: "¿Necesita un intérprete?" pag. 195.
- ^ Jean-Paul Tremblay; PG Sorenson. "La teoría y práctica de la escritura de compiladores" . 1985. p. 527
- ^ "Mundo inalámbrico: electrónica, radio, televisión, volumen 89" . pag. 73.
- ^ "Byte, volumen 5" . 1980. p. 212
- ^ Bell, James R. (1973). "Código enhebrado". Comunicaciones de la ACM . 16 (6): 370–372. doi : 10.1145 / 362248.362270 .
- ^ Moore, Charles H., publicó comentarios en el número cuarto de la revista Byte
- ^ Ertl, Anton. "¿Qué es el código enhebrado?" .
- ^ Latendresse, Mario; Feeley, Marc. Generación de intérpretes rápidos para código de bytes comprimido con Huffman . Elsevier. CiteSeerX 10.1.1.156.2546 .
- ^ a b Busby, Jonathan. "Explicación del bucle interno de RPL" , "The Museum of HP Calculators" , 7 de septiembre de 2018, consultado el 27 de diciembre de 2019
- ^ Wickes, William C. (30 de mayo de 1986). "Sistema y método de procesamiento de datos para la ejecución directa e indirecta de tipos de objetos estructurados uniformemente" . uspto.gov . Consultado el 27 de diciembre de 2019 .
enlaces externos
- Página explicativa de Anton Ertl ¿Qué es el código enhebrado? describe diferentes técnicas de subprocesamiento y proporciona más referencias.
- El desarrollo del lenguaje C por Dennis M. Ritchie describe B (un precursor de C) como implementado usando "código enhebrado".
- Thinking Forth Project incluye el libro fundamental (pero agotado) Thinking Forth de Leo Brodie publicado en 1984.
- Iniciando FORTH versión en línea del libro Starting FORTH de Leo Brodie publicado en 1981.
- Moving FORTH de Brad Rodriguez : Parte 1: Decisiones de diseño en Forth Kernel cubre las técnicas de subprocesamiento en profundidad.
- Historia de las CPU de propósito general
- Extensiones GCC. Etiquetas como valores
- Horn, Joseph K. "¿Qué es RPL?" . Archivado desde el original el 17 de septiembre de 2017 . Consultado el 17 de septiembre de 2017 .(NB. Breve descripción general de los idiomas enhebrados, RPL del sistema y del usuario, utilizados en las calculadoras HP como la HP 48 ).