Dado un sistema informático específico, ¿es posible estimar el tiempo de ejecución preciso real de un código de ensamblaje?

23

este es un fragmento de código de ensamblaje

section .text
    global _start       ;must be declared for using gcc
_start:                     ;tell linker entry point
    mov edx, len    ;message length
    mov ecx, msg    ;message to write
    mov ebx, 1      ;file descriptor (stdout)
    mov eax, 4      ;system call number (sys_write)
    int 0x80        ;call kernel
    mov eax, 1      ;system call number (sys_exit)
    int 0x80        ;call kernel

section .data

msg db  'Hello, world!',0xa ;our dear string
len equ $ - msg         ;length of our dear string

Dado un sistema informático específico, ¿es posible predecir con precisión el tiempo de ejecución real de un código de ensamblaje?

yaojp
fuente
30
¿Es "ejecutar el código en esa computadora y usar un cronómetro" una respuesta válida?
Draconis
44
Sospecho que la mayor parte del tiempo dedicado a ejecutar este código está esperando E / S. El tiempo que lleva ejecutar las instrucciones individuales es algo predecible si conociera la ubicación de la memoria del código y todos los detalles sobre el procesador (que son extremadamente complejos hoy en día), pero la velocidad también se ve afectada por la memoria y el disco. También tendría que conocer una gran cantidad de detalles sobre ellos. Entonces, a menos que tenga en cuenta los fenómenos físicos (que también afectan el tiempo), podría decir que es predecible, pero inimaginablemente difícil hacerlo.
IllidanS4 quiere que Monica regrese el
44
siempre es posible estimar ...
sudo rm -rf slash
3
¿No es esto también imposible debido al problema de detención? Podemos probar para algún código si se detendrá, pero no podemos tener un algoritmo que determine esto para todos los códigos posibles.
kutschkem
2
@Falco Eso sería una propiedad del sistema dado. Algunas implementaciones independientes de C no tienen sistema operativo; todo lo que se está ejecutando es un bucle principal (o ni siquiera un bucle ;-)) que puede o no leer de las direcciones de hardware para la entrada.
Peter - Restablece a Monica el

Respuestas:

47

Solo puedo citar del manual de una CPU bastante primitiva, un procesador 68020 de alrededor de 1986: "Calcular el tiempo de ejecución exacto de una secuencia de instrucciones es difícil, incluso si tienes un conocimiento preciso de la implementación del procesador". Que no tenemos. Y en comparación con un procesador moderno, esa CPU era primitiva .

No puedo predecir el tiempo de ejecución de ese código, y tú tampoco. Pero ni siquiera puede definir qué es el "tiempo de ejecución" de un fragmento de código, cuando un procesador tiene memorias caché masivas y capacidades desordenadas enormes. Un procesador moderno típico puede tener 200 instrucciones "en vuelo", es decir, en varias etapas de ejecución. Entonces, el tiempo desde tratar de leer el primer byte de instrucción hasta retirar la última instrucción puede ser bastante largo. Pero la demora real para todo otro trabajo que el procesador necesita hacer puede ser (y típicamente es) mucho menor.

Por supuesto, hacer dos llamadas al sistema operativo hace que esto sea completamente impredecible. No sabes qué hace realmente "escribir en stdout", por lo que no puedes predecir la hora.

Y no puede saber la velocidad del reloj de la computadora en el momento preciso en que ejecuta el código. Puede estar en algún modo de ahorro de energía, la computadora puede haber reducido la velocidad del reloj porque se calienta, por lo que incluso el mismo número de ciclos de reloj puede tomar diferentes cantidades de tiempo.

En general: totalmente impredecible.

gnasher729
fuente
12
Creo que tus conclusiones son demasiado fuertes. La latencia y el rendimiento son métricas comunes para medir el "tiempo de ejecución" de un programa. También puede simplemente conformarse con una definición adecuada de "tiempo de ejecución". Además, si tiene una instantánea completa del estado del sistema, hw y sw, y un conocimiento perfecto de los componentes internos de la CPU, puede predecir el tiempo de ejecución. En Intel probablemente puedan estimar el tiempo de ejecución, incluso aquí en SO podemos predecir latencias y tputs con precisión de ciclo. En este caso, además de las llamadas al sistema, ni siquiera es tan difícil.
Margaret Bloom
10
@MargaretBloom ni siquiera entonces. Coloco mi teléfono demasiado cerca del horno, la CPU no funciona para controlar la temperatura, su tiempo de ejecución estimado es demasiado bajo. E incluso si cuenta en ciclos y no realiza llamadas al sistema, otros subprocesos y CPU pueden funcionar bien con el contenido de la RAM, o pueden volcar su memoria en el disco duro mientras lo intercambian, en función de circunstancias impredecibles, que van desde el poder Las oleadas ralentizan el disco duro lo suficiente como para que un hilo de la competencia obtenga suficiente memoria a tiempo para destruir el tuyo, todo el camino hasta los hilos tirando dados para ver cuánto tiempo perder.
John Dvorak
66
Además de eso, "el conocimiento completo del estado del sistema, hw y sw" es bastante difícil, creo. Agregue "10 ms por adelantado", y ya está pidiendo lo imposible. Y si la implementación de la CPU de la generación de números aleatorios de hardware utiliza fenómenos cuánticos (probablemente lo haga), y algún hilo en la CPU lo llama, entonces ni siquiera saber el estado completo del universo 3000 km alrededor de la computadora lo salvará. Y en MWI, ni siquiera puedes adivinar bien.
John Dvorak
8
@Nat: Incluso en criptografía, "tiempo constante" en realidad no significa absolutamente constante, solo significa que el tiempo de ejecución no tiene variaciones sistemáticas que dependan de datos secretos y podría correlacionarse estadísticamente con ellos. Y en la práctica, a menudo se supone que si la ruta de código tomada y el patrón de accesos de memoria ejecutados no dependen de datos secretos, y si se evitan instrucciones específicas que se sabe que toman una cantidad de tiempo variable (o sus entradas enmascaradas para ojalá elimine la correlación), probablemente sea lo suficientemente bueno. Más allá de eso, realmente solo tienes que medirlo.
Ilmari Karonen
2
Un 68020 es una bestia compleja ... prueba un MCS51 ....
rackandboneman
30

No puede hacer esto en general, pero en algunos sentidos, puede hacerlo, y ha habido algunos casos históricos en los que realmente tuvo que hacerlo.

El Atari 2600 (o Atari Video Computer System) fue uno de los primeros sistemas de videojuegos domésticos y se lanzó por primera vez en 1978. A diferencia de los sistemas posteriores de la era, Atari no podía permitirse darle al dispositivo un búfer de cuadros, lo que significa que la CPU tenía ejecutar código en cada línea de exploración para determinar qué producir: si este código tomara 17.08 microsegundos para ejecutarse (el intervalo HBlank), los gráficos no se establecerían correctamente antes de que la línea de exploración comenzara a dibujarlos. Peor aún, si el programador quería dibujar contenido más complejo de lo que normalmente permitía Atari, tenía que medir los tiempos exactos para las instrucciones y cambiar los registros gráficos a medida que se dibujaba el haz, con un lapso de 57,29 microsegundos para toda la línea de exploración.

Sin embargo, el Atari 2600, como muchos otros sistemas basados ​​en el 6502, tenía una característica muy importante que permitía la cuidadosa administración del tiempo requerida para este escenario: la CPU, la RAM y la señal de TV se salieron de los relojes basados ​​en el mismo maestro reloj. La señal de TV salió de un reloj de 3.98 MHz, dividiendo las veces anteriores en un número entero de "relojes de color" que administraban la señal de TV, y un ciclo de los relojes de CPU y RAM fue exactamente tres relojes de color, permitiendo que el reloj de la CPU sea Una medida precisa del tiempo en relación con la señal de TV de progreso actual. (Para obtener más información sobre esto, consulte la Guía del programador de Stella , escrita para el emulador Stella Atari 2600 ).

Este entorno operativo, además, significaba que cada instrucción de CPU tenía una cantidad definida de ciclos que tomaría en cada caso, y muchos desarrolladores de 6502 publicaron esta información en tablas de referencia. Por ejemplo, considere esta entrada para la CMPinstrucción (Comparar memoria con acumulador), tomada de esta tabla :

CMP  Compare Memory with Accumulator

     A - M                            N Z C I D V
                                    + + + - - -

     addressing    assembler    opc  bytes  cycles
     --------------------------------------------
     immediate     CMP #oper     C9    2     2
     zeropage      CMP oper      C5    2     3
     zeropage,X    CMP oper,X    D5    2     4
     absolute      CMP oper      CD    3     4
     absolute,X    CMP oper,X    DD    3     4*
     absolute,Y    CMP oper,Y    D9    3     4*
     (indirect,X)  CMP (oper,X)  C1    2     6
     (indirect),Y  CMP (oper),Y  D1    2     5*

*  add 1 to cycles if page boundary is crossed

Utilizando toda esta información, Atari 2600 (y otros desarrolladores de 6502) pudieron determinar exactamente cuánto tiempo tardó en ejecutarse su código y crear rutinas que hicieron lo que necesitaban y cumplían con los requisitos de sincronización de señal de TV de Atari. Y debido a que este tiempo era tan exacto (especialmente para instrucciones que perdían el tiempo como NOP), incluso pudieron usarlo para modificar los gráficos a medida que se dibujaban.


Por supuesto, el 6502 de Atari es un caso muy específico, y todo esto es posible solo porque el sistema tenía todo lo siguiente:

  • Un reloj maestro que funcionaba todo, incluida la RAM. Los sistemas modernos tienen relojes independientes para la CPU y la RAM, con el reloj RAM a menudo más lento y los dos no necesariamente sincronizados.
  • Sin almacenamiento en caché de ningún tipo: el 6502 siempre accedía a DRAM directamente. Los sistemas modernos tienen memorias caché SRAM que hacen que sea más difícil predecir el estado; aunque quizás todavía sea posible predecir el comportamiento de un sistema con una memoria caché, definitivamente es más difícil.
  • No hay otros programas ejecutándose simultáneamente: el programa en el cartucho tenía un control completo del sistema. Los sistemas modernos ejecutan múltiples programas a la vez utilizando algoritmos de programación no deterministas.
  • Una velocidad de reloj lo suficientemente lenta como para que las señales puedan viajar a través del sistema a tiempo. En un sistema moderno con velocidades de reloj de 4 GHz (por ejemplo), se necesita un fotón de luz de 6.67 ciclos de reloj para recorrer la longitud de una placa base de medio metro; nunca podría esperar que un procesador moderno interactúe con otra cosa en la placa en solo un ciclo, ya que se necesita más de un ciclo para que una señal en el tablero llegue incluso al dispositivo.
  • Una velocidad de reloj bien definida que rara vez cambia (1,19 MHz en el caso del Atari): las velocidades de la CPU de los sistemas modernos cambian todo el tiempo, mientras que un Atari no podría hacer esto sin afectar también la señal de TV.
  • Tiempos de ciclo publicados: el x86 no define cuánto tiempo lleva cualquiera de sus instrucciones.

Todas estas cosas se unieron para crear un sistema donde era posible crear conjuntos de instrucciones que tomaron una cantidad exacta de tiempo, y para esta aplicación, eso es exactamente lo que se exigía. La mayoría de los sistemas no tienen este grado de precisión simplemente porque no es necesario: los cálculos se realizan cuando se realizan o si se necesita una cantidad de tiempo exacta, se puede consultar un reloj independiente. Pero si la necesidad es correcta (como en algunos sistemas integrados), todavía puede aparecer y podrá determinar con precisión cuánto tiempo tarda su código en ejecutarse en estos entornos.


Y también debo agregar el gran descargo de responsabilidad masivo de que todo esto solo se aplica a la construcción de un conjunto de instrucciones de ensamblaje que llevará una cantidad exacta de tiempo. Si lo que desea hacer es tomar una pieza arbitraria de ensamblaje, incluso en estos entornos, y preguntar "¿Cuánto tiempo lleva ejecutar esto?", Categóricamente no puede hacer eso, ese es el problema de detención , que se ha demostrado que no tiene solución.


EDITAR 1: En una versión anterior de esta respuesta, dije que el Atari 2600 no tenía forma de informar al procesador de dónde estaba en la señal de TV, lo que lo obligó a mantener todo el programa contado y sincronizado desde el principio. Como se me señaló en los comentarios, esto es cierto para algunos sistemas como el ZX Spectrum, pero no es cierto para el Atari 2600, ya que contiene un registro de hardware que detiene la CPU hasta que se produzca el siguiente intervalo de supresión horizontal, así como una función para comenzar el intervalo de supresión vertical a voluntad. Por lo tanto, el problema de contar los ciclos se limita a cada línea de exploración, y solo se vuelve exacto si el desarrollador desea cambiar el contenido a medida que se dibuja la línea de exploración.

TheHansinator
fuente
44
También se debe tener en cuenta que la mayoría de los juegos no funcionaron a la perfección: se podían ver muchos artefactos en la salida de video debido a una sincronización incorrecta de la señal de video, ya sea debido a un error del programador (estimación incorrecta de la sincronización de la CPU) o simplemente por tener demasiado Trabajo por hacer. También era muy frágil: si necesita corregir un error o agregar nuevas funciones, es muy probable que rompa el tiempo, a veces inevitablemente. Fue divertido, pero también una pesadilla :) Ni siquiera estoy seguro de si la velocidad del reloj siempre fue exactamente correcta, por ejemplo, bajo sobrecalentamiento, interferencia, etc. Pero definitivamente muestra que fue difícil incluso en ese momento.
Luaan
1
Buena respuesta, aunque me gustaría señalar que no tiene que contar el número de ciclos para cada instrucción en el Atari 2600. Tiene dos características para ayudarlo a no tener que hacer eso: un temporizador de cuenta regresiva que inicializa y luego sondee para ver si ha llegado a 0, y un registro que detiene la CPU hasta que comience el próximo blanking horizontal. Muchos otros dispositivos, como el ZX Spectrum, no tienen nada de eso, y en realidad tienes que contar cada ciclo pasado después de la interrupción de supresión vertical para saber en qué parte de la pantalla estás.
Martin Vilcans
1
Yo diría que el problema de detención no se aplica estrictamente a los Atari. Si excluye las capacidades de E / S del Atari y lo restringe a una ROM de cartucho típica, entonces hay una cantidad finita de almacenamiento. En ese momento, tiene una máquina de estados finitos, por lo que cualquier programa debe detenerse o ingresar un estado al que ya ingresó, lo que lleva a un bucle infinito demostrable en tiempo finito.
user1937198
2
@ user1937198 128 bytes de estado (más lo que esté en los registros) es MÁS que suficiente espacio de estado para hacer la diferencia entre eso y la cinta teórica infinita de la máquina Turing, una distinción que solo importa en teoría. Demonios, prácticamente no podemos buscar los 128 BITS de algo así como una clave AES ... El espacio de estado crece de manera aterradora rápidamente a medida que agregas bits. No olvide que el equivalente de 'Desactivar interrumpe; detenerse habría sido casi seguro posible.
Dan Mills
1
"Ese es el problema de detención, que se ha demostrado que no se puede resolver. Si te encuentras con esto, entonces necesitas romper el cronómetro y ejecutar tu código". - esto no tiene sentido. No puede evadir la prueba de Turing "ejecutando" el código en lugar de simularlo. Si se detiene, puede calcular cuánto tiempo tarda en detenerse. Si no se detiene, nunca puede estar seguro (en general) si se detendrá en el futuro o se ejecutará para siempre. Es el mismo problema con un cronómetro real o simulado. Al menos en una simulación, puede inspeccionar más fácilmente el estado interno en busca de signos de bucle.
benrg
15

Hay dos aspectos en juego aquí

Como señala @ gnasher729, si conocemos las instrucciones exactas para ejecutar, aún es difícil estimar el tiempo de ejecución exacto debido a cosas como el almacenamiento en caché, la predicción de ramas, el escalado, etc.

Sin embargo, la situación es aún peor. Dado un trozo de ensamblaje, es imposible saber qué instrucciones se ejecutarán, o incluso saber cuántas instrucciones se ejecutarán. Esto se debe al teorema de Rice: si pudiéramos determinar eso con precisión, entonces podríamos usar esa información para resolver el problema de detención, lo cual es imposible.

El código de ensamblaje puede contener saltos y ramas, que son suficientes para hacer que la traza completa de un programa sea posiblemente infinita. Se ha trabajado en aproximaciones conservadoras del tiempo de ejecución, que da límites superiores en la ejecución, a través de cosas como la semántica de costos o sistemas de tipo anotado. No estoy familiarizado con nada para el ensamblaje específicamente, pero no me sorprendería si existiera algo así.

jmite
fuente
44
Quiero decir, el problema de detención se aplica directamente aquí, ya que si supiéramos el tiempo de ejecución sabríamos si se detiene. También el hecho de que no haya condicionales ni siquiera ayuda aquí, ya que en x86, moves Turing-Complete
BlueRaja - Danny Pflughoeft
77
Rice y el Problema de detención son declaraciones sobre programas arbitrarios (cualquiera), pero el OP aquí ha especificado un código específico en la pregunta. Puede determinar las propiedades semánticas y de detención sobre categorías individuales o limitadas de programas, ¿verdad? Es solo que no hay un procedimiento general que cubra todos los programas.
Daniel R. Collins
2
Podemos saber definitivamente qué instrucción se ejecutará a continuación, lo que no podemos decir es si alguna vez golpeamos a sys_exity, por lo tanto, detenemos el cronómetro. Si restringimos la finalización de programas, lo cual es razonable para una pregunta tan práctica, entonces la respuesta es en realidad sí (siempre que tenga una instantánea perfecta del estado, hw y sw, del sistema justo antes de iniciar el programa).
Margaret Bloom
1
@ BlueRaja-DannyPflughoeft Mov está completo, pero no en el código que tiene el OP aquí. Pero de todos modos eso está fuera del punto: los ints pueden ejecutar código arbitrario, esperar operaciones de E / S arbitrarias, etc.
Luaan
2

¿La elección del "sistema informático" incluiría microcontroladores? Algunos microcontroladores tienen tiempos de ejecución muy predecibles, por ejemplo, la serie PIC de 8 bits tiene cuatro ciclos de reloj por instrucción a menos que la instrucción se bifurque a una dirección diferente, lea desde flash o sea una instrucción especial de dos palabras.

Las interrupciones obviamente interrumpirán este tipo de timimg, pero es posible hacer mucho sin un controlador de interrupciones en una configuración de "metal desnudo".

Usando el ensamblaje y un estilo de codificación especial, es posible escribir código que siempre tomará el mismo tiempo en ejecutarse. No es tan común ahora que la mayoría de las variantes de PIC tienen temporizadores múltiples, pero es posible.

Oliver Broad
fuente
2

En la era de las computadoras de 8 bits, algunos juegos hicieron algo así. Los programadores usarían la cantidad exacta de tiempo necesario para ejecutar las instrucciones, en función de la cantidad de tiempo que tomaron y la velocidad de reloj conocida de la CPU, para sincronizar con los tiempos exactos del hardware de video y audio. En aquellos días, la pantalla era un monitor de tubo de rayos catódicos que recorría cada línea de la pantalla a una velocidad fija y pintaba esa fila de píxeles activando y desactivando el rayo catódico para activar o desactivar los fósforos. Debido a que los programadores necesitaban decirle al hardware de video qué mostrar justo antes de que el rayo llegara a esa parte de la pantalla, y ajustar el resto del código al tiempo restante, llamaron a eso "correr el rayo".

Absolutamente no funcionaría en ninguna computadora moderna, o para un código como su ejemplo.

Por qué no? Aquí hay algunas cosas que arruinarían el tiempo simple y predecible:

La velocidad de la CPU y las recuperaciones de memoria son cuellos de botella en el tiempo de ejecución. Es una pérdida de dinero ejecutar una CPU más rápido de lo que puede obtener instrucciones para ejecutar, o instalar memoria que puede entregar bytes más rápido de lo que la CPU puede aceptarlos. Debido a esto, las computadoras viejas no funcionaban en el mismo reloj. Las CPU modernas funcionan mucho más rápido que la memoria principal. Lo logran al tener instrucciones y cachés de datos. La CPU aún se detendrá si alguna vez necesita esperar bytes que no están en el caché. Por lo tanto, las mismas instrucciones se ejecutarán mucho más rápido si ya están en el caché que si no lo están.

Además, las CPU modernas tienen tuberías largas. Mantienen su alto rendimiento al hacer que otra parte del chip haga un trabajo preliminar en las siguientes instrucciones en la tubería. Esto fallará si la CPU no sabe cuál será la próxima instrucción, lo que puede suceder si hay una rama. Por lo tanto, las CPU intentan predecir saltos condicionales. (No tiene ninguno en este fragmento de código, pero tal vez hubo un salto condicional mal predicho que obstruyó la tubería. Además, una buena excusa para vincular esa respuesta legendaria). Del mismo modo, los sistemas que llaman int 80a atrapar en modo kernel en realidad están utilizando una característica complicada de la CPU, una puerta de interrupción, que introduce un retraso impredecible.

Si su sistema operativo utiliza la multitarea preventiva, el hilo que ejecuta este código podría perder su división de tiempo en cualquier momento.

Competir con la viga también solo funcionó porque el programa se ejecutaba en el metal desnudo y golpeaba directamente en el hardware. Aquí, estás llamando int 80para hacer una llamada al sistema. Eso transfiere el control al sistema operativo, lo que no le brinda garantía de tiempo. Luego le dice que haga E / S en una secuencia arbitraria, que podría haber sido redirigida a cualquier dispositivo. Es demasiado abstracto para usted decir cuánto tiempo lleva la E / S, pero seguramente dominará el tiempo dedicado a ejecutar instrucciones.

Si desea una sincronización exacta en un sistema moderno, debe introducir un ciclo de retardo. Tienes que hacer que las iteraciones más rápidas se ejecuten a la velocidad de la más lenta, ya que no es posible lo contrario. Una de las razones por las que las personas lo hacen en el mundo real es para evitar la filtración de información criptográfica a un atacante que puede tomar el tiempo que las solicitudes tardan más que otras.

Davislor
fuente
1

Esto es algo tangencial, pero el transbordador espacial tenía 4 computadoras redundantes que dependían de estar sincronizadas con precisión, es decir, su tiempo de ejecución coincidía exactamente.

El primer intento de lanzamiento del transbordador espacial se eliminó cuando la computadora Backup Flight Software (BFS) se negó a sincronizarse con las cuatro computadoras del Sistema de software de aviónica primario (PASS). Detalles en "The Bug Heard Round the World" aquí . Lectura fascinante sobre cómo se desarrolló el software para que coincida ciclo por ciclo y podría brindarle información interesante.

Edgar H
fuente
0

Creo que estamos mezclando dos problemas diferentes aquí. (Y sí, sé que otros lo han dicho, pero espero poder expresarlo más claramente).

Primero, necesitamos pasar del código fuente a la secuencia de instrucciones que realmente se ejecuta (que necesita conocer los datos de entrada y el código: ¿cuántas veces da la vuelta a un ciclo? ¿Qué rama se toma después de una prueba? ) Debido al problema de detención, la secuencia de instrucciones puede ser infinita (sin terminación) y no siempre se puede determinar estáticamente, incluso con el conocimiento de los datos de entrada.

Una vez establecida la secuencia de instrucciones a ejecutar, entonces desea determinar el tiempo de ejecución. Eso ciertamente puede estimarse con cierto conocimiento de la arquitectura del sistema. Pero el problema es que en muchas máquinas modernas, el tiempo de ejecución depende en gran medida del almacenamiento en caché de las recuperaciones de memoria, lo que significa que depende tanto de los datos de entrada como de las instrucciones ejecutadas. También depende de adivinar correctamente los destinos de rama condicionales, que nuevamente dependen de los datos. Así que solo será una estimación, no será exacta.

Michael Kay
fuente