Ejecución del programa |
---|
Conceptos generales |
Tipos de código |
Estrategias de compilación |
Tiempos de ejecución notables |
|
Compiladores y cadenas de herramientas notables |
En informática , un intérprete es un programa informático que ejecuta directamente instrucciones escritas en un lenguaje de programación o scripting , sin necesidad de que hayan sido compiladas previamente en un programa en lenguaje máquina . Un intérprete generalmente utiliza una de las siguientes estrategias para la ejecución del programa:
Las primeras versiones del lenguaje de programación Lisp y los dialectos BASIC para minicomputadoras y microcomputadoras serían ejemplos del primer tipo. Perl , Raku , Python , MATLAB y Ruby son ejemplos del segundo, mientras que UCSD Pascal es un ejemplo del tercer tipo. Los programas fuente se compilan con anticipación y se almacenan como código independiente de la máquina, que luego se vincula en tiempo de ejecución y se ejecuta por un intérprete y/o compilador (para sistemas JIT ). Algunos sistemas, como Smalltalk y versiones contemporáneas de BASIC y Java , también pueden combinar dos y tres tipos. [2] También se han construido intérpretes de varios tipos para muchos lenguajes tradicionalmente asociados con la compilación, como Algol , Fortran , Cobol , C y C++ .
Si bien la interpretación y la compilación son los dos medios principales por los que se implementan los lenguajes de programación, no son mutuamente excluyentes, ya que la mayoría de los sistemas de interpretación también realizan algún trabajo de traducción, al igual que los compiladores. Los términos " lenguaje interpretado " o " lenguaje compilado " significan que la implementación canónica de ese lenguaje es un intérprete o un compilador, respectivamente. Un lenguaje de alto nivel es idealmente una abstracción independiente de implementaciones particulares.
Los intérpretes se empezaron a utilizar en 1952 para facilitar la programación dentro de las limitaciones de los ordenadores de la época (por ejemplo, la escasez de espacio de almacenamiento de programas o la falta de soporte nativo para números de coma flotante). Los intérpretes también se utilizaban para traducir entre lenguajes de máquina de bajo nivel, lo que permitía escribir código para máquinas que todavía estaban en construcción y probarlo en ordenadores que ya existían. [3] El primer lenguaje de alto nivel interpretado fue Lisp . Lisp fue implementado por primera vez por Steve Russell en un ordenador IBM 704. Russell había leído el artículo de John McCarthy , "Funciones recursivas de expresiones simbólicas y su cálculo por máquina, parte I", y se dio cuenta (para sorpresa de McCarthy) de que la función eval de Lisp podía implementarse en código máquina. [4] El resultado fue un intérprete de Lisp funcional que podía utilizarse para ejecutar programas de Lisp o, más propiamente, "evaluar expresiones de Lisp".
El desarrollo de los intérpretes de edición estuvo influenciado por la necesidad de computación interactiva. En la década de 1960, la introducción de sistemas de tiempo compartido permitió que varios usuarios accedieran a una computadora simultáneamente, y los intérpretes de edición se volvieron esenciales para administrar y modificar código en tiempo real. Los primeros intérpretes de edición probablemente se desarrollaron para computadoras mainframe, donde se usaban para crear y modificar programas sobre la marcha. Uno de los primeros ejemplos de un intérprete de edición es el sistema EDT (Editor and Debugger for the TECO), que se desarrolló a fines de la década de 1960 para la computadora PDP-1. EDT permitía a los usuarios editar y depurar programas utilizando una combinación de comandos y macros, allanando el camino para los editores de texto modernos y los entornos de desarrollo interactivos. [ cita requerida ]
Un intérprete suele estar formado por un conjunto de comandos conocidos que puede ejecutar y una lista de estos comandos en el orden en que el programador desea ejecutarlos. Cada comando (también conocido como Instrucción ) contiene los datos que el programador desea mutar e información sobre cómo hacerlo. Por ejemplo, un intérprete podría leerlo ADD Books, 5
e interpretarlo como una solicitud para agregar cinco a la Books
variable .
Los intérpretes tienen una amplia variedad de instrucciones que están especializadas para realizar diferentes tareas, pero normalmente encontrará instrucciones de intérprete para operaciones matemáticas básicas , ramificaciones y administración de memoria , lo que hace que la mayoría de los intérpretes sean completos de Turing . Muchos intérpretes también están estrechamente integrados con un recolector de basura y un depurador .
Los programas escritos en un lenguaje de alto nivel son ejecutados directamente por algún tipo de intérprete o convertidos en código máquina por un compilador (y ensamblador y enlazador ) para que la CPU los ejecute.
Aunque los compiladores (y ensambladores) generalmente producen código de máquina directamente ejecutable por hardware de computadora, a menudo pueden (opcionalmente) producir una forma intermedia llamada código objeto . Este es básicamente el mismo código específico de la máquina pero aumentado con una tabla de símbolos con nombres y etiquetas para hacer que los bloques ejecutables (o módulos) sean identificables y reubicables. Los programas compilados generalmente usarán bloques de construcción (funciones) guardados en una biblioteca de dichos módulos de código objeto. Se utiliza un enlazador para combinar archivos de biblioteca (prefabricados) con el archivo o archivos objeto de la aplicación para formar un solo archivo ejecutable. Por lo tanto, los archivos objeto que se usan para generar un archivo ejecutable a menudo se producen en diferentes momentos y, a veces, incluso por diferentes lenguajes (capaces de generar el mismo formato de objeto).
Un intérprete simple escrito en un lenguaje de bajo nivel (por ejemplo, lenguaje ensamblador ) puede tener bloques de código de máquina similares que implementan funciones del lenguaje de alto nivel almacenadas y ejecutadas cuando la entrada de una función en una tabla de búsqueda apunta a ese código. Sin embargo, un intérprete escrito en un lenguaje de alto nivel normalmente utiliza otro enfoque, como generar y luego recorrer un árbol de análisis , o generar y ejecutar instrucciones intermedias definidas por software, o ambas cosas.
Por lo tanto, tanto los compiladores como los intérpretes generalmente convierten el código fuente (archivos de texto) en tokens, ambos pueden (o no) generar un árbol de análisis y ambos pueden generar instrucciones inmediatas (para una máquina de pila , código cuádruple o por otros medios). La diferencia básica es que un sistema compilador, que incluye un enlazador (integrado o separado), genera un programa de código de máquina independiente , mientras que un sistema intérprete, en cambio, realiza las acciones descritas por el programa de alto nivel.
De este modo, un compilador puede realizar casi todas las conversiones desde la semántica del código fuente al nivel de máquina de una vez por todas (es decir, hasta que se deba cambiar el programa), mientras que un intérprete tiene que realizar parte de este trabajo de conversión cada vez que se ejecuta una sentencia o función. Sin embargo, en un intérprete eficiente, gran parte del trabajo de traducción (incluido el análisis de tipos y similares) se descarta y se realiza solo la primera vez que se ejecuta un programa, módulo, función o incluso sentencia, por lo que es bastante similar a cómo funciona un compilador. Sin embargo, un programa compilado aún se ejecuta mucho más rápido, en la mayoría de las circunstancias, en parte porque los compiladores están diseñados para optimizar el código y se les puede dar suficiente tiempo para esto. Esto es especialmente cierto para lenguajes de alto nivel más simples sin (muchas) estructuras de datos dinámicas, verificaciones o comprobación de tipos .
En la compilación tradicional, la salida ejecutable de los enlazadores (archivos .exe o .dll o una biblioteca, ver imagen) es típicamente reubicable cuando se ejecuta bajo un sistema operativo general, de manera muy similar a los módulos de código objeto, pero con la diferencia de que esta reubicación se realiza dinámicamente en tiempo de ejecución, es decir, cuando el programa se carga para su ejecución. Por otro lado, los programas compilados y enlazados para pequeños sistemas embebidos suelen estar asignados estáticamente, a menudo codificados en una memoria flash NOR , ya que a menudo no hay almacenamiento secundario ni sistema operativo en este sentido.
Históricamente, la mayoría de los sistemas de interpretación tenían un editor autónomo integrado. Esto se está volviendo más común también para los compiladores (en aquel entonces llamados IDE ), aunque algunos programadores prefieren usar un editor de su elección y ejecutar el compilador, el enlazador y otras herramientas manualmente. Históricamente, los compiladores son anteriores a los intérpretes porque el hardware en ese momento no podía soportar tanto el intérprete como el código interpretado y el entorno de procesamiento por lotes típico de la época limitaba las ventajas de la interpretación. [5]
Durante el ciclo de desarrollo de software , los programadores realizan cambios frecuentes en el código fuente. Cuando se utiliza un compilador, cada vez que se realiza un cambio en el código fuente, deben esperar a que el compilador traduzca los archivos fuente modificados y vincule todos los archivos de código binario antes de que se pueda ejecutar el programa. Cuanto más grande sea el programa, más larga será la espera. Por el contrario, un programador que utiliza un intérprete espera mucho menos, ya que el intérprete generalmente solo necesita traducir el código en el que se está trabajando a una representación intermedia (o no traducirlo en absoluto), por lo que requiere mucho menos tiempo antes de que se puedan probar los cambios. Los efectos son evidentes al guardar el código fuente y volver a cargar el programa. El código compilado generalmente se depura con menos facilidad, ya que la edición, la compilación y la vinculación son procesos secuenciales que deben realizarse en la secuencia adecuada con un conjunto adecuado de comandos. Por esta razón, muchos compiladores también tienen una ayuda ejecutiva, conocida como Makefile y programa. El Makefile enumera las líneas de comando del compilador y del enlazador y los archivos de código fuente del programa, pero puede tomar una entrada de menú de línea de comando simple (por ejemplo, "Make 3") que selecciona el tercer grupo (conjunto) de instrucciones y luego emite los comandos al compilador y al enlazador que alimenta los archivos de código fuente especificados.
Un compilador convierte el código fuente en instrucciones binarias para la arquitectura de un procesador específico, lo que lo hace menos portable . Esta conversión se realiza solo una vez, en el entorno del desarrollador, y luego el mismo binario se puede distribuir a las máquinas del usuario donde se puede ejecutar sin más traducción. Un compilador cruzado puede generar código binario para la máquina del usuario incluso si tiene un procesador diferente al de la máquina donde se compila el código.
Un programa interpretado puede distribuirse como código fuente. Debe traducirse en cada máquina final, lo que lleva más tiempo pero hace que la distribución del programa sea independiente de la arquitectura de la máquina. Sin embargo, la portabilidad del código fuente interpretado depende de que la máquina de destino tenga un intérprete adecuado. Si el intérprete debe proporcionarse junto con el código fuente, el proceso de instalación general es más complejo que la entrega de un ejecutable monolítico, ya que el intérprete en sí es parte de lo que se debe instalar.
El hecho de que los humanos puedan leer y copiar fácilmente el código interpretado puede ser un motivo de preocupación desde el punto de vista de los derechos de autor . Sin embargo, existen varios sistemas de cifrado y ofuscación . La entrega de código intermedio, como el bytecode, tiene un efecto similar a la ofuscación, pero el bytecode podría decodificarse con un descompilador o desensamblador . [ cita requerida ]
La principal desventaja de los intérpretes es que un programa interpretado normalmente se ejecuta más lentamente que si hubiera sido compilado . La diferencia en velocidades puede ser minúscula o grande; a menudo de un orden de magnitud y, a veces, más. Generalmente, lleva más tiempo ejecutar un programa con un intérprete que ejecutar el código compilado, pero puede llevar menos tiempo interpretarlo que el tiempo total requerido para compilarlo y ejecutarlo. Esto es especialmente importante cuando se crean prototipos y se prueba el código, cuando un ciclo de edición-interpretación-depuración a menudo puede ser mucho más corto que un ciclo de edición-compilación-ejecución-depuración. [6] [7]
La interpretación del código es más lenta que la ejecución del código compilado porque el intérprete debe analizar cada enunciado del programa cada vez que se ejecuta y luego realizar la acción deseada, mientras que el código compilado simplemente realiza la acción dentro de un contexto fijo determinado por la compilación. Este análisis en tiempo de ejecución se conoce como "sobrecarga interpretativa". El acceso a las variables también es más lento en un intérprete porque la asignación de identificadores a ubicaciones de almacenamiento debe realizarse repetidamente en tiempo de ejecución en lugar de en tiempo de compilación . [6]
Existen varios compromisos entre la velocidad de desarrollo cuando se utiliza un intérprete y la velocidad de ejecución cuando se utiliza un compilador. Algunos sistemas (como algunos Lisp ) permiten que el código interpretado y compilado se llamen entre sí y compartan variables. Esto significa que una vez que una rutina se ha probado y depurado bajo el intérprete, se puede compilar y, por lo tanto, beneficiarse de una ejecución más rápida mientras se desarrollan otras rutinas. [ cita requerida ] Muchos intérpretes no ejecutan el código fuente tal como está, sino que lo convierten en una forma interna más compacta. Muchos intérpretes BASIC reemplazan las palabras clave con tokens de un solo byte que se pueden usar para encontrar la instrucción en una tabla de saltos . [6] Algunos intérpretes, como el intérprete PBASIC , logran niveles aún más altos de compactación del programa utilizando una estructura de memoria de programa orientada a bits en lugar de una orientada a bytes, donde los tokens de comandos ocupan quizás 5 bits, las constantes nominalmente de "16 bits" se almacenan en un código de longitud variable que requiere 3, 6, 10 o 18 bits, y los operandos de dirección incluyen un "desplazamiento de bits". Muchos intérpretes BASIC pueden almacenar y leer su propia representación interna tokenizada.
Intérprete de expresiones de Toy C |
---|
// tipos de datos para el árbol de sintaxis abstracta enum _kind { kVar , kConst , kSum , kDiff , kMult , kDiv , kPlus , kMinus , kNot }; struct _variable { int * memoria ; }; struct _constant { int valor ; }; struct _unaryOperation { struct _node * derecha ; }; struct _binaryOperation { struct _node * izquierda , * derecha ; }; struct _node { enum _kind tipo ; unión _expresión { struct _variable variable ; struct _constant constante ; struct _binaryOperation binario ; struct _unaryOperation unario ; } e ; }; // procedimiento intérprete int ejecutarIntExpresión ( const struct _nodo * n ) { int valorIzquierdo , valorDerecho ; cambiar ( n -> tipo ) { caso kVar : devolver * n -> e . variable . memoria ; caso kConst : devolver n -> e . constante . valor ; caso kSum : caso kDiff : caso kMult : caso kDiv : valorIzquierdo = ejecutarIntExpresión ( n -> e . binario . izquierda ); valorDerecho = ejecutarIntExpresión ( n -> e . binario . derecho ); cambiar ( n -> tipo ) { caso kSum : devolver valorIzquierdo + valorDerecho ; caso kDiff : devolver valorIzquierdo - valorDerecho ; caso kMult : devolver valorIzquierdo * valorDerecho ; caso kDiv : si ( valorDerecho == 0 ) excepción ( "división por cero" ); // no retorna return leftValue / rightValue ; } caso kPlus : caso kMinus : caso kNot : rightValue = executeIntExpression ( n - > e.unary.right ) ; switch ( n - > kind ) { caso kPlus : return + rightValue ; caso kMinus : return - rightValue ; caso kNot : return ! rightValue ; } predeterminado : excepción ( "error interno: tipo de expresión ilegal" ); } } |
Un intérprete podría utilizar el mismo analizador léxico y sintáctico que el compilador y luego interpretar el árbol sintáctico abstracto resultante. En el recuadro se muestran ejemplos de definiciones de tipos de datos para este último y un intérprete de juguete para árboles sintácticos obtenidos a partir de expresiones en C.
La interpretación no puede utilizarse como único método de ejecución: aunque un intérprete puede ser interpretado por sí mismo, etc., se necesita un programa ejecutado directamente en algún lugar en la parte inferior de la pila porque el código que se está interpretando no es, por definición, el mismo que el código de máquina que la CPU puede ejecutar. [8] [9]
Existe un espectro de posibilidades entre la interpretación y la compilación, dependiendo de la cantidad de análisis realizado antes de que se ejecute el programa. Por ejemplo, Emacs Lisp se compila a bytecode , que es una representación altamente comprimida y optimizada del código fuente de Lisp, pero no es código máquina (y por lo tanto no está ligado a ningún hardware en particular). Este código "compilado" es luego interpretado por un intérprete de bytecode (escrito en C ). El código compilado en este caso es código máquina para una máquina virtual , que no se implementa en hardware, sino en el intérprete de bytecode. Dichos intérpretes de compilación a veces también se denominan compreters . [10] [11] En un intérprete de bytecode, cada instrucción comienza con un byte y, por lo tanto, los intérpretes de bytecode tienen hasta 256 instrucciones, aunque no se pueden usar todas. Algunos bytecodes pueden ocupar varios bytes y pueden ser arbitrariamente complicados.
Las tablas de control , que no necesariamente necesitan pasar por una fase de compilación, dictan el flujo de control algorítmico apropiado a través de intérpretes personalizados, de manera similar a los intérpretes de código de bytes.
Los intérpretes de código enhebrado son similares a los intérpretes de bytecode, pero en lugar de bytes utilizan punteros. Cada "instrucción" es una palabra que apunta a una función o una secuencia de instrucciones, posiblemente seguida de un parámetro. El intérprete de código enhebrado realiza un bucle para obtener instrucciones y llamar a las funciones a las que apuntan, o bien obtiene la primera instrucción y salta a ella, y cada secuencia de instrucciones termina con una búsqueda y un salto a la siguiente instrucción. A diferencia del bytecode, no hay un límite efectivo en el número de instrucciones diferentes, aparte de la memoria disponible y el espacio de direcciones. El ejemplo clásico de código enhebrado es el código Forth utilizado en los sistemas Open Firmware : el lenguaje fuente se compila en "código F" (un bytecode), que luego es interpretado por una máquina virtual . [ cita requerida ]
En el espectro entre la interpretación y la compilación, otro enfoque es transformar el código fuente en un árbol de sintaxis abstracta (AST) optimizado, luego ejecutar el programa siguiendo esta estructura de árbol, o usarlo para generar código nativo just-in-time . [12] En este enfoque, cada oración necesita ser analizada solo una vez. Como ventaja sobre el bytecode, el AST mantiene la estructura global del programa y las relaciones entre las declaraciones (que se pierden en una representación de bytecode), y cuando se comprime proporciona una representación más compacta. [13] Por lo tanto, el uso de AST se ha propuesto como un mejor formato intermedio para compiladores just-in-time que el bytecode. Además, permite que el sistema realice un mejor análisis durante el tiempo de ejecución.
Sin embargo, para los intérpretes, un AST causa más sobrecarga que un intérprete de bytecode, debido a que los nodos relacionados con la sintaxis no realizan ningún trabajo útil, a que la representación es menos secuencial (requiere atravesar más punteros) y a que hay sobrecarga al visitar el árbol. [14]
Otro aspecto que difumina aún más la distinción entre intérpretes, intérpretes de bytecode y compilación es la compilación just-in-time (JIT), una técnica en la que la representación intermedia se compila en código de máquina nativo en tiempo de ejecución. Esto confiere la eficiencia de ejecutar código nativo, a costa del tiempo de arranque y del mayor uso de memoria cuando se compila por primera vez el bytecode o AST. El primer compilador JIT publicado se atribuye generalmente al trabajo de John McCarthy en LISP en 1960. [15] La optimización adaptativa es una técnica complementaria en la que el intérprete perfila el programa en ejecución y compila sus partes ejecutadas con más frecuencia en código nativo. Esta última técnica tiene algunas décadas de antigüedad, y apareció en lenguajes como Smalltalk en la década de 1980. [16]
La compilación justo a tiempo ha ganado la atención generalizada entre los implementadores de lenguajes en los últimos años, con Java , .NET Framework , la mayoría de las implementaciones modernas de JavaScript y Matlab que ahora incluyen compiladores JIT. [ cita requerida ]
Para hacer que la distinción entre compiladores e intérpretes sea aún más vaga, existe un diseño especial de intérprete conocido como intérprete de plantillas. En lugar de implementar la ejecución de código en virtud de una gran sentencia switch que contiene todos los códigos de bytes posibles, mientras se opera en una pila de software o en un recorrido de árbol, un intérprete de plantillas mantiene una gran matriz de códigos de bytes (o cualquier representación intermedia eficiente) asignada directamente a las instrucciones de máquina nativas correspondientes que se pueden ejecutar en el hardware host como pares clave-valor (o en diseños más eficientes, direcciones directas a las instrucciones nativas), [17] [18] conocidas como una "plantilla". Cuando se ejecuta el segmento de código en particular, el intérprete simplemente carga o salta a la asignación del código de operación en la plantilla y la ejecuta directamente en el hardware. [19] [20] Debido a su diseño, el intérprete de plantillas se parece mucho a un compilador justo a tiempo en lugar de a un intérprete tradicional, sin embargo, técnicamente no es un JIT debido al hecho de que simplemente traduce el código del lenguaje en llamadas nativas, un código de operación a la vez, en lugar de crear secuencias optimizadas de instrucciones ejecutables de CPU a partir de todo el segmento de código. Debido al diseño simple del intérprete de simplemente pasar llamadas directamente al hardware en lugar de implementarlas directamente, es mucho más rápido que cualquier otro tipo, incluso los intérpretes de código de bytes, y hasta cierto punto menos propenso a errores, pero como compensación es más difícil de mantener debido a que el intérprete tiene que admitir la traducción a múltiples arquitecturas diferentes en lugar de una máquina/pila virtual independiente de la plataforma. Hasta la fecha, las únicas implementaciones de intérpretes de plantillas de lenguajes ampliamente conocidos que existen son el intérprete dentro de la implementación de referencia oficial de Java, la Sun HotSpot Java Virtual Machine, [17] y el Ignition Interpreter en el motor de ejecución de javascript Google V8.
Un autointérprete es un intérprete de lenguaje de programación escrito en un lenguaje de programación que puede interpretarse a sí mismo; un ejemplo es un intérprete de BASIC escrito en BASIC. Los autointérpretes están relacionados con los compiladores autoalojados .
Si no existe un compilador para el lenguaje a interpretar, la creación de un intérprete propio requiere la implementación del lenguaje en un lenguaje anfitrión (que puede ser otro lenguaje de programación o ensamblador ). Al disponer de un primer intérprete como este, se arranca el sistema y se pueden desarrollar nuevas versiones del intérprete en el propio lenguaje. Fue de esta forma que Donald Knuth desarrolló el intérprete TANGLE para el lenguaje WEB del sistema de composición tipográfica TeX , estándar de facto .
La definición de un lenguaje informático suele hacerse en relación con una máquina abstracta (la llamada semántica operacional ) o como una función matemática ( semántica denotacional ). Un lenguaje también puede definirse mediante un intérprete en el que se da la semántica del lenguaje anfitrión. La definición de un lenguaje por parte de un autointérprete no está bien fundamentada (no puede definir un lenguaje), pero un autointérprete le informa al lector sobre la expresividad y la elegancia de un lenguaje. También permite al intérprete interpretar su código fuente, el primer paso hacia la interpretación reflexiva.
Una dimensión de diseño importante en la implementación de un intérprete automático es si una característica del lenguaje interpretado se implementa con la misma característica en el lenguaje anfitrión del intérprete. Un ejemplo es si un cierre en un lenguaje tipo Lisp se implementa utilizando cierres en el lenguaje del intérprete o se implementa "manualmente" con una estructura de datos que almacena explícitamente el entorno. Cuantas más características implemente la misma característica en el lenguaje anfitrión, menos control tendrá el programador del intérprete; por ejemplo, no se puede lograr un comportamiento diferente para tratar con desbordamientos de números si las operaciones aritméticas se delegan a operaciones correspondientes en el lenguaje anfitrión.
Algunos lenguajes como Lisp y Prolog tienen elegantes auto-intérpretes. [21] Se han llevado a cabo muchas investigaciones sobre auto-intérpretes (particularmente intérpretes reflexivos) en el lenguaje de programación Scheme , un dialecto de Lisp. En general, sin embargo, cualquier lenguaje Turing-completo permite escribir su propio intérprete. Lisp es un lenguaje de este tipo, porque los programas Lisp son listas de símbolos y otras listas. XSLT es un lenguaje de este tipo, porque los programas XSLT están escritos en XML. Un subdominio de la metaprogramación es la escritura de lenguajes específicos de dominio (DSL).
Clive Gifford introdujo [22] una medida de la calidad de los autointérpretes (el eigenratio), el límite de la relación entre el tiempo de computadora empleado en ejecutar una pila de N autointérpretes y el tiempo empleado en ejecutar una pila de N − 1 autointérpretes a medida que N tiende al infinito. Este valor no depende del programa que se esté ejecutando.
El libro Structure and Interpretation of Computer Programs presenta ejemplos de interpretación metacircular para Scheme y sus dialectos. Otros ejemplos de lenguajes con un intérprete propio son Forth y Pascal .
El microcódigo es una técnica muy utilizada "que impone un intérprete entre el hardware y el nivel arquitectónico de una computadora". [23] Como tal, el microcódigo es una capa de instrucciones a nivel de hardware que implementan instrucciones de código de máquina de nivel superior o secuenciación de máquina de estados interna en muchos elementos de procesamiento digital . El microcódigo se utiliza en unidades centrales de procesamiento de propósito general , así como en procesadores más especializados como microcontroladores , procesadores de señales digitales , controladores de canal , controladores de disco , controladores de interfaz de red , procesadores de red , unidades de procesamiento gráfico y en otro hardware.
El microcódigo suele residir en una memoria especial de alta velocidad y traduce instrucciones de máquina, datos de máquina de estados u otra entrada en secuencias de operaciones detalladas a nivel de circuito. Separa las instrucciones de máquina de la electrónica subyacente para que las instrucciones se puedan diseñar y modificar con mayor libertad. También facilita la creación de instrucciones complejas de varios pasos, al tiempo que reduce la complejidad de los circuitos informáticos. La escritura de microcódigo a menudo se denomina microprogramación y el microcódigo en una implementación de procesador particular a veces se denomina microprograma .
Una microcodificación más extensa permite que microarquitecturas pequeñas y simples emulen arquitecturas más potentes con longitudes de palabra más amplias , más unidades de ejecución , etc., lo cual es una forma relativamente sencilla de lograr compatibilidad de software entre diferentes productos en una familia de procesadores.
Incluso un procesador de computadora sin microcodificación puede considerarse un intérprete de ejecución inmediata escrito en un lenguaje de descripción de hardware de propósito general como VHDL para crear un sistema que analiza las instrucciones del código de máquina y las ejecuta inmediatamente.
Los intérpretes, como los escritos en Java, Perl y Tcl, son necesarios actualmente para una amplia gama de tareas computacionales, incluidas la emulación binaria y las aplicaciones de Internet. El rendimiento de los intérpretes sigue siendo una preocupación a pesar de su adaptabilidad, en particular en sistemas con recursos de hardware limitados. Los enfoques avanzados de instrumentación y seguimiento proporcionan información sobre las implementaciones de los intérpretes y la utilización de los recursos del procesador durante la ejecución mediante evaluaciones de intérpretes adaptados al conjunto de instrucciones MIPS y lenguajes de programación como Tcl, Perl y Java. Las características de rendimiento se ven influidas por la complejidad del intérprete, como lo demuestran las comparaciones con el código compilado. Está claro que el rendimiento del intérprete depende más de los matices y las necesidades de recursos del intérprete que de la aplicación particular que se está interpretando.