Sobre la cuestión de la velocidad y medirla en Arduino



Este problema surgió en el estudio del rendimiento de Arduino al ejecutar varios comandos (más sobre eso en una publicación separada). En el curso del estudio, surgieron dudas sobre la constancia del tiempo de trabajo de los comandos individuales cuando el valor de los operandos cambió (como resultó más tarde, no es irrazonable) y se tomó la decisión de tratar de estimar el tiempo de ejecución de un comando separado. Para esto, se escribió un pequeño programa (quien dijo que el boceto es abandonar la clase), que, a primera vista, confirmó la hipótesis. En la conclusión, puede observar los valores 16 y 20, pero a veces se encuentran 28 e incluso 32 microsegundos. Si multiplicamos los datos recibidos por 16 (frecuencia de reloj MK), obtenemos el tiempo de ejecución en ciclos MK (de 256 a 512). Desafortunadamente, una ejecución repetida del ciclo del programa principal (con los mismos datos iniciales), mientras mantiene la imagen general, ya proporciona una distribución diferente del tiempo de ejecución, por lo que las variaciones de tiempo reales no están relacionadas con los datos iniciales. La hipótesis original es refutada, pero se vuelve interesante, y qué se asocia exactamente con una dispersión tan significativa.

Nota necesaria: entiendo muy bien que se deben usar programas más sofisticados para medir el tiempo de ejecución de los comandos, pero para una estimación aproximada, uno que se demostrará más adelante es suficiente.

Entonces, el tiempo está cambiando y, muy significativamente, estamos buscando las causas de este fenómeno. En primer lugar, prestamos atención a la multiplicidad de los valores obtenidos, miramos la descripción de la biblioteca de trabajo a lo largo del tiempo y vemos que 4 µseg es un cuanto de medición, por lo que es mejor ir a cuantos y entender que obtenemos 4 o 5 (muy a menudo) y 6 o 7 o 8 unidades (muy raras). Con la primera mitad, todo es fácil: si el valor medido se encuentra entre 4 y 5 unidades, entonces la dispersión se vuelve inevitable. Además, considerando que las muestras son independientes, podemos aumentar la precisión de la medición mediante métodos estadísticos, que es lo que hacemos, obteniendo resultados aceptables.

Pero con la segunda mitad (6,7,8) las cosas son peores. Descubrimos que la dispersión no se correlaciona con los datos de origen, lo que significa que esto es una manifestación de otros procesos que afectan el tiempo de ejecución de los comandos. Cabe señalar que las emisiones son bastante raras y no muestran un efecto significativo sobre el valor promedio calculado. Sería posible descuidarlos en absoluto, pero este no es nuestro estilo. En general, a lo largo de los años de trabajo en ingeniería, me di cuenta de que no puedes dejar malentendidos, sin importar cuán insignificantes parezcan, ya que tienen una capacidad repugnante para golpear en la espalda (bueno, o dónde más llegan) en el momento más inoportuno.

Comenzamos a presentar la hipótesis 1 , la más conveniente (en conveniencia y versatilidad, solo superada por la intervención directa del Creador): fallas de software, por supuesto, no mías, mis programas nunca fallan, sino bibliotecas conectadas (compilador, sistema operativo, navegador, etc. . - sustituir lo necesario). Además, dado que ejecuto el programa en el emulador en www.tinkercad.com , aún puede consultar los errores del emulador y cerrar el tema, porque las fuentes no están disponibles para nosotros. Contras de esta hipótesis:

  1. De ciclo a ciclo, la ubicación de las desviaciones cambia, lo que sugiere.
  2. Este sitio todavía admite AutoDesk, aunque el argumento es bastante débil.
  3. "Aceptamos el postulado de que lo que está sucediendo no es una alucinación, de lo contrario sería simplemente poco interesante".

El siguiente supuesto es la influencia de algunos procesos en segundo plano en el resultado de la medición. Parece que no estamos haciendo nada más que creer, aunque ... estamos generando los resultados en serie. Surge la hipótesis 2 : el tiempo de salida a veces (aunque sea extraño ... pero sucede) se agrega al tiempo de ejecución del comando. Aunque es dudoso cuánto hay esa salida, pero de todos modos, agregar Flush no ayudó, agregar un retraso para completar la salida y no ayudó, generalmente sacando la salida del bucle, de todos modos, el tiempo salta, esto definitivamente no es Serial.

Bueno, lo que queda es la organización del ciclo en sí mismo (del cual es un susto cambiar su duración, no está claro) y eso es todo ... aunque los micros () permanecieron. Quise decir que el tiempo de ejecución de la primera llamada de esta función y la segunda es el mismo y al restar estos dos valores obtengo cero, pero si esta suposición es incorrecta.

Hipótesis 3: a veces la segunda llamada del recuento de tiempo tarda más que la primera o las acciones asociadas con el recuento de tiempo a veces afectan el resultado. Observamos el código fuente de la función de trabajar con el tiempo (arduino-1.8.4 \ hardware \ arduino \ avr \ cores \ arduino \ cable.c - He expresado repetidamente mi actitud ante tales cosas, no me repetiré) y vemos que 1 vez de 256 Los ciclos de aumento de hardware de la parte más joven del contador se interrumpen para incrementar la parte más antigua del contador.

El tiempo de ejecución de nuestro ciclo es de 4 a 5, por lo que podemos esperar 170 * (4..5) / 256 = de tres a cuatro valores anómalos en un segmento de 170 mediciones. Nos vemos - se ve muy similar, realmente hay 4 de ellos. Para separar las razones primera y segunda, hacemos cálculos por la sección crítica con interrupciones prohibidas. El resultado no cambia mucho, las emisiones todavía tienen un lugar para estar, lo que significa que micros () aporta tiempo adicional. Aquí no podemos hacer nada, aunque el código fuente está disponible, no podemos cambiarlo: las bibliotecas se incluyen en los archivos binarios. Por supuesto, podemos escribir nuestras propias funciones de trabajar con el tiempo y observar su comportamiento, pero hay una manera más simple.

Dado que el procesamiento "largo" de la interrupción es una posible razón para el aumento de la duración, excluimos la posibilidad de que ocurra durante el proceso de medición. Para hacer esto, espere su manifestación y solo entonces llevaremos a cabo un ciclo de medición. Dado que la interrupción ocurre con mucha menos frecuencia de lo que dura nuestro ciclo de medición, podemos garantizar su ausencia. Escribimos el fragmento correspondiente del programa (utilizando hacks sucios con información extraída del código fuente) y, "esto es una magia callejera", todo se vuelve normal: medimos el tiempo de ejecución de 4 y 5 cuantos con un valor promedio del tiempo de ejecución de la operación de adición con PT de 166 ciclos de reloj, que corresponde al valor medido previamente. La hipótesis puede considerarse confirmada.

Queda una pregunta más, y qué demora tanto en las interrupciones, qué se necesita
(7.8) - (5) ~ 2 quanta = * 4 = 8 mseg * 16 = 128 ciclos de procesador? Pasamos al código fuente (es decir, al código ensamblador generado por el compilador en godbolt.com) y vemos que la interrupción misma se ejecuta aproximadamente 70 ciclos, 60 de ellos constantemente, y al leer hay costos adicionales de 10 ciclos, un total de 70 cuando se pulsa interrupción: menos de lo recibido, pero lo suficientemente cerca. Atribuimos la diferencia a la diferencia entre compiladores o modos de su uso.

Bueno, ahora podemos medir el tiempo de ejecución real del comando de adición de PT con varios argumentos y asegurarnos de que realmente cambie mucho al cambiar los argumentos: de 136 medidas para 0.0 a 190 para 0.63 (número mágico), y esto es solo 162 para 10.63. Con una probabilidad del 99.9%, esto se debe a la necesidad de alineación y las características de su implementación en esta biblioteca en particular, pero este estudio claramente va más allá del alcance del problema en consideración.

Apéndice - texto del programa:
void setup() { Serial.begin(9600); } volatile float t; //   void loop() { int d[170]; unsigned long time,time1; float dt=1/170.; for (int i=0; i<170; ++i) { { //       time1=micros(); long time2; do { time2=micros(); } while ((time2 & ~0xFF) == (time1 & ~0xFF)); }; /**/ time1=micros(); //   /* cli(); //       -   */ t=10.63; //     t=t+dt; //   /* sei(); //    */ time = micros(); //   time1=time-time1; d[i]=time1/4; /* Serial.print(time1); //      Serial.flush(); //     Delay(20); //    */ }; //   ,     float sum=0; for (int i=0; i<170; ++i) { sum+=d[i]; Serial.println(d[i]); }; Serial.println((sum/170-2.11)*4*16); //2.11Serial.flush(); //    ,     } 

Source: https://habr.com/ru/post/es426941/


All Articles