
Estoy construyendo mi propio sistema operativo Linux integrado para Raspberry PI3 usando Buildroot. Este SO se utilizará para manejar varias aplicaciones, una de ellas realiza la detección de objetos basada en OpenCV (v3.3.0).
Comencé con Raspbian Jessy + Python pero resultó que lleva mucho tiempo ejecutar un ejemplo simple, así que decidí diseñar mi propio RTOS con funciones optimizadas + desarrollo en C++ en lugar de Python.
Pensé que con estas optimizaciones los 4 núcleos de RPI + 1 GB de RAM manejarían este tipo de aplicaciones. El problema es que incluso con estas cosas, los programas de visión por computadora más simples requieren mucho tiempo.
Comparación entre PC y Raspberry PI3
Este es un programa simple que escribí para tener una idea del orden de magnitud del tiempo de ejecución de cada parte del programa.
#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <time.h> /* clock_t, clock, CLOCKS_PER_SEC */
using namespace cv;
using namespace std;
int main()
{
setUseOptimized(true);
clock_t t_access, t_proc, t_save, t_total;
// Access time.
t_access = clock();
Mat img0 = imread("img0.jpg", IMREAD_COLOR);// takes ~90ms
t_access = clock() - t_access;
// Processing time
t_proc = clock();
cvtColor(img0, img0, CV_BGR2GRAY);
blur(img0, img0, Size(9,9));// takes ~18ms
t_proc = clock() - t_proc;
// Saving time
t_save = clock();
imwrite("img1.jpg", img0);
t_save = clock() - t_save;
t_total = t_access + t_proc + t_save;
//printf("CLOCKS_PER_SEC = %d\n\n", CLOCKS_PER_SEC);
printf("(TEST 0) Total execution time\t %d cycles \t= %f ms!\n", t_total,((float)t_total)*1000./CLOCKS_PER_SEC);
printf("---->> Accessing in\t %d cycles \t= %f ms.\n", t_access,((float)t_access)*1000./CLOCKS_PER_SEC);
printf("---->> Processing in\t %d cycles \t= %f ms.\n", t_proc,((float)t_proc)*1000./CLOCKS_PER_SEC);
printf("---->> Saving in\t %d cycles \t= %f ms.\n", t_save,((float)t_save)*1000./CLOCKS_PER_SEC);
return 0;
}
Resultados de ejecución en una PC i7
Resultados de ejecución en Raspberry PI (SO generado desde Buildroot)
Como puedes ver hay una gran diferencia. Lo que necesito es optimizar cada detalle para que este ejemploProcesandoEl paso ocurre "casi" en tiempo real enen un tiempo máximo de 15ms.
Mi pregunta es sobre:
- ¿Cómo puedo optimizar mi sistema operativo para que pueda manejar aplicaciones de cálculos intensivos y cómo puedo controlar las prioridades de cada parte?
- ¿Cómo puedo utilizar completamente los 4 núcleos de RPI3 para cumplir con los requisitos?
- ¿Existen otras posibilidades en lugar de OpenCV?
- ¿Debería utilizar C en lugar de C++?
- ¿Alguna mejora de hardware que recomiendes?
Respuesta1
En orden:
¿Cómo puedo optimizar mi sistema operativo para que pueda manejar aplicaciones de cálculos intensivos y cómo puedo controlar las prioridades de cada parte?
Para la optimización general, no hay mucho que pueda hacer en el lado del sistema operativo más allá de las cosas normales, como asegurarse de tener solo lo que realmente necesita ejecutándose en segundo plano. En el Pi original, se podían acelerar memmove()
funciones similares mediante LD_PRELOAD
una biblioteca llamada 'cofi' que proporcionaba versiones optimizadas para el ensamblaje de esas funciones, pero no estoy seguro de si ayudará en un Pi 3.
Para la priorización, eso es realmente algo para buscar en las páginas de manual, pero generalmente no puede hacerlo a menos que paralelice las cosas (en su caso, parece que la solución obvia es ejecutar cada paso a medida que se gana el proceso y usar IPC (probablemente memoria compartida por motivos de rendimiento) para mover los datos entre ellos).
En cuanto a los resultados que citó de su programa de prueba, observe en particular que los pasos de procesamiento y guardado son aproximadamente 10 veces más lentos en el Pi, mientras que el paso de acceso es solo aproximadamente 5 veces más lento, y esos números coinciden con un estimación aproximada de lo que esperaría al comparar un Pi 3 con una PC genérica que tiene menos de un año. Es casi seguro que la CPU en el Pi es significativamente más lenta que en la que ejecutó la prueba de PC (y si no paralelizó las cosas en absoluto, entonces la brecha se amplía aún más, ya que la mayoría de las CPU x86 modernas pueden ejecutar un solo núcleo por sí sola a carga completa mucho más rápido de lo que pueden ejecutar todos sus núcleos a carga completa), y eso tendrá un impacto. El ISA de ARM también es significativamente diferente del ISA de x86 (ARM tiende a hacer menos por ciclo en comparación con x86, pero generalmente no necesita acceder a la RAM con tanta frecuencia y generalmente no hace que los errores de predicción de bifurcaciones sean tan costosos como lo hace x86) , por lo que cualquier código optimizado para cómo GCC organiza las cosas en una PC no será tan óptimo en una Pi.
Tampoco sé qué cámara estás usando, pero espero que puedas obtener mejores tiempos al reducir la resolución de las imágenes que estás procesando, y probablemente puedas reducir el tiempo de adquisición si evitas usar formatos comprimidos (y no usar compresión con pérdida significa que la resolución no importará tanto).
¿Cómo puedo utilizar completamente los 4 núcleos de RPI3 para cumplir con los requisitos?
Paralelización en su propio código. Sólo necesita asegurarse de que SMP esté habilitado en su kernel (y si está usando la configuración oficial de RPi Foundation, debería estarlo) y luego intentar ejecutar todo en paralelo. No estoy seguro de cuánto hace OpenCV para paralelizar las cosas en sí, pero es posible que también quieras mirar OpenMP (proporciona una manera razonablemente fácil de paralelizar iteraciones en bucles que no son interdependientes).
¿Existen otras posibilidades en lugar de OpenCV?
Puede que lo haya, pero todos están estandarizados en OpenCV, por lo que sugeriría usarlo (le resultará más fácil obtener ayuda técnica para implementar cosas porque todos lo usan).
¿Debería utilizar C en lugar de C++?
Eso depende de cómo estés usando las cosas. Si bien es mucho más fácil escribir código lento en C++ que en C, no es más difícil escribir código rápido en ninguno de los dos idiomas. Muchas de las técnicas de optimización son bastante similares en ambos lenguajes (por ejemplo, preasignar todo al inicio para no llamar malloc()
en secciones críticas o evitar llamar stat()
). Sin embargo, en el caso de C++ específicamente, evítelo std::string
como la peste, llama malloc()
por todas partes y, como resultado, es increíblemente lento (he visto conversiones que cambian de std::string
cadenas de estilo C mejoran el rendimiento en más del 40% en algunos casos). ).
¿Alguna mejora de hardware que recomiendes?
Bajo el supuesto de que está tratando de mantener bajos los costos de hardware y tiene limitaciones de espacio (de ahí la elección de Raspberry Pi), en realidad no se me ocurre ninguno. El Pi (en todas sus iteraciones) utiliza un SoC que es bastante adecuado para el trabajo de visión por computadora en ese rango de precios. Si está dispuesto a optar por algo un poco más grande y algo más caro, podría sugerirle una placa NVIDIA Jetson (utilizan un SoC Tegra que tiene una GPU equivalente a Quadro integrada con 192 núcleos CUDA, por lo que probablemente podría ejecutar su procesamiento). carga de trabajo mucho más rápido), pero hacer que Buildroot funcione allí es mucho más complicado que en un Pi.
Editaciones en respuesta a comentarios:
La paralelización a nivel de proceso no es lo mismo que el subproceso múltiple, es drásticamente diferente (la mayor diferencia está en cómo se comparten los recursos; de forma predeterminada, los subprocesos comparten todo, los procesos no comparten nada). En general, cuando hay mucho procesamiento involucrado, (normalmente) es mejor utilizar la paralelización basada en procesos, ya que es más fácil escribir código eficiente sin tener que preocuparse por la seguridad de los subprocesos.
En cuanto a las opciones, las dos que mencionaste pueden tener un gran impacto en el rendimiento del sistema, pero ambas terminan siendo compensaciones entre rendimiento y latencia. El modelo de preferencia controla cómo se pueden reprogramar las cosas que se ejecutan en modo kernel (como las llamadas al sistema). Las tres opciones que existen son:
- Sin preferencia: esto significa que cualquier cosa que se ejecute en modo kernel no se puede interrumpir. Coincide con el comportamiento de SVR4 y 4.4BSD, así como con el funcionamiento de la mayoría de los sistemas UNIX más antiguos. Es muy bueno para el rendimiento, pero muy malo para la latencia, por lo que generalmente solo se usa en servidores grandes con muchas CPU (más CPU significa que es más probable que uno esté ejecutando algo que se puede adelantar).
- Prelación voluntaria: esto permite que cada función en el kernel defina ubicaciones en las que puede reprogramarse. Esta es la configuración que utilizan la mayoría de las distribuciones de Linux orientadas al escritorio, ya que ofrece un buen equilibrio entre rendimiento y latencia.
- Prelación total: esto significa que (casi) cualquier código en el kernel puede interrumpirse en (casi) cualquier momento. Esto es útil para sistemas que necesitan una latencia muy baja con respecto a eventos externos y de entrada, como los sistemas utilizados para trabajo multimedia en tiempo real. Es absolutamente horrible en términos de rendimiento, pero la latencia es inmejorable.
Por el contrario, la frecuencia del temporizador es mucho más fácil de explicar. Controla el período de tiempo más largo que algo puede ejecutarse ininterrumpidamente si hay algo más esperando para ejecutarse. Los valores más altos dan como resultado un período de tiempo más corto (menor latencia y menor rendimiento), los valores más bajos dan como resultado un período más largo (mayor latencia y mayor rendimiento). Para un comienzo general, sugeriría configurar el modelo de preferencia en voluntario y la frecuencia del temporizador en 300 Hz, y luego comenzar a experimentar cambiando primero la frecuencia del temporizador (ya que eso generalmente tendrá un impacto más visible).
En cuanto al Movidius NCS, si vale la pena o no depende de la cantidad de datos con los que necesites trabajar porque el ancho de banda estará limitado por la conexión USB (el Pi solo tiene un controlador USB 2.0, por lo que no solo estás limitado a menos de una décima parte del ancho de banda para el que está diseñado Movidius, también debe compartir el bus con al menos el adaptador Ethernet, lo que afectará tanto su latencia como su rendimiento). Si solo está haciendo fotogramas individuales de 1920x1080 con color de 32 bits a una velocidad baja, entonces podría ser viable, pero si necesita realizar un procesamiento de transmisión de ese mismo vídeo a velocidades de fotogramas completas, entonces probablemente se encontrará con latencia. asuntos. Si elige usar uno, asegúrese de obtener un concentrador con alimentación (de lo contrario, puede tener problemas al intentar consumir más energía de la que el Pi puede proporcionar).