Otimização de programas baseados em OpenCV sistema operacional Linux incorporado

Otimização de programas baseados em OpenCV sistema operacional Linux incorporado

Estou construindo meu próprio sistema operacional Linux embarcado para Raspberry PI3 usando Buildroot. Este SO será utilizado para lidar com diversas aplicações, uma delas realiza detecção de objetos baseada em OpenCV (v3.3.0).

Comecei com Raspbian Jessy + Python, mas descobri que leva muito tempo para executar um exemplo simples. Então decidi projetar meu próprio RTOS com recursos otimizados + desenvolvimento em C++ em vez de Python.

Achei que com essas otimizações os 4 núcleos do RPI + os 1GB de RAM dariam conta de tais aplicações. O problema é que mesmo com essas coisas, os programas mais simples de visão computacional levam muito tempo.

Comparação entre PC e Raspberry PI3

Este é um programa simples que escrevi para ter uma ideia da ordem de grandeza do tempo de execução de cada parte do 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 da execução em um PC i7 insira a descrição da imagem aqui

Resultados da execução no Raspberry PI (sistema operacional gerado do Buildroot) insira a descrição da imagem aqui

Como você pode ver, há uma enorme diferença. O que preciso é otimizar cada detalhe para que este exemploem processamentoetapa ocorre em tempo "quase" real emem um tempo máximo de 15ms.

Minha pergunta é sobre:

  • Como posso otimizar meu sistema operacional para que ele possa lidar com aplicações de cálculos intensivos e como posso controlar as prioridades de cada parte?
  • Como posso usar totalmente os 4 núcleos do RPI3 para cumprir os requisitos?
  • Existem outras possibilidades em vez do OpenCV?
  • Devo usar C em vez de C++?
  • Alguma melhoria de hardware que você recomenda?

Responder1

Em ordem:

Como posso otimizar meu sistema operacional para que ele possa lidar com aplicações de cálculos intensivos e como posso controlar as prioridades de cada parte?

Para otimização geral, não há muito que você possa fazer no lado do sistema operacional além do normal, como garantir que você tenha apenas o que realmente precisa em execução em segundo plano. No Pi original, você poderia acelerar memmove()funções semelhantes usando LD_PRELOADuma biblioteca chamada 'cofi' que fornecia versões otimizadas para montagem dessas funções, mas não tenho certeza se isso ajudará em um Pi 3.

Para priorização, isso é realmente algo para se olhar nas páginas de manual, mas geralmente você não pode fazer isso a menos que paralelize as coisas (no seu caso, parece que a solução óbvia é executar cada etapa conforme o processo é ganho e usar o IPC (provavelmente memória compartilhada por motivos de desempenho) para mover os dados entre eles).

Considerando os resultados que você citou do seu programa de teste, observe em particular que as etapas de processamento e salvamento são cerca de 10 vezes mais lentas no Pi, enquanto a etapa de acesso é apenas cerca de 5 vezes mais lenta, e esses números correspondem a um estimativa aproximada do que eu esperaria ao comparar um Pi 3 a um PC genérico com menos de um ano. A CPU no Pi é quase certamente significativamente mais lenta do que aquela em que você executou o teste do PC (e se você não paralelizou as coisas, a lacuna aumenta ainda mais, já que a maioria das CPUs x86 modernas podem executar um único núcleo por si só em carga total muito mais rápido do que eles podem executar todos os seus núcleos com carga total), e isso terá um impacto. O ARM ISA também é significativamente diferente do x86 ISA (o ARM tende a fazer menos por ciclo em comparação com o x86, mas geralmente não precisa acessar a RAM com tanta frequência e geralmente não torna as falhas de previsão de ramificação tão caras quanto o x86) , portanto, qualquer código otimizado para como o GCC organiza as coisas em um PC não será tão ideal em um Pi.

Também não sei qual câmera você está usando, mas espero que você consiga tempos melhores cortando a resolução das imagens que está processando e provavelmente poderá reduzir o tempo de aquisição se evitar usar formatos compactados (e não usar compactação com perdas significa que a resolução não importará tanto).

Como posso usar totalmente os 4 núcleos do RPI3 para cumprir os requisitos?

Paralelização em seu próprio código. Você só precisa ter certeza de que o SMP está habilitado em seu kernel (e se você estiver usando a configuração oficial da RPi Foundation, deveria estar) e então tentar executar as coisas em paralelo. Não tenho certeza de quanto o OpenCV faz para paralelizar as coisas em si, mas você pode querer dar uma olhada no OpenMP também (ele fornece uma maneira razoavelmente fácil de paralelizar iterações em loops que não são interdependentes).

Existem outras possibilidades em vez do OpenCV?

Pode haver, mas todos são padronizados no OpenCV, então eu sugeriria usá-lo (será mais fácil obter ajuda técnica para implementar coisas porque todo mundo usa).

Devo usar C em vez de C++?

Isso depende de como você está usando as coisas. Embora seja muito mais fácil escrever código lento em C++ do que em C, não é mais difícil escrever código rápido em qualquer uma das linguagens. Muitas das técnicas de otimização são bastante semelhantes em ambas as linguagens (por exemplo, pré-alocar tudo na inicialização para que você não chame malloc()seções críticas ou evite chamar stat()). No caso específico do C++, evite std::stringcomo uma praga, ele chama malloc()em todos os lugares e, como resultado, é incrivelmente lento (vi conversões que mudam de std::stringstrings no estilo C melhoram o desempenho em mais de 40% em alguns casos ).

Alguma melhoria de hardware que você recomenda?

Supondo que você esteja tentando manter os custos de hardware baixos e tenha espaço limitado (daí a escolha do Raspberry Pi), não consigo pensar em nenhum. O Pi (em todas as suas iterações) usa um SoC que é especialmente adequado para trabalhos de visão computacional nessa faixa de preço. Se você estiver disposto a optar por algo um pouco maior e um pouco mais caro, posso sugerir uma placa NVIDIA Jetson (eles usam um SoC Tegra que possui uma GPU equivalente a Quadro integrada com 192 núcleos CUDA, então provavelmente poderia executar seu processamento carga de trabalho muito mais rápida), mas fazer o Buildroot funcionar lá é significativamente mais complicado do que em um Pi.

Edições em resposta aos comentários:

A paralelização no nível do processo não é a mesma coisa que multithreading, é drasticamente diferente (a maior diferença está em como os recursos são compartilhados, por padrão os threads compartilham tudo, o processo não compartilha nada). Em geral, quando há muito processamento envolvido, (geralmente) é melhor usar a paralelização baseada em processos, pois é mais fácil escrever código eficiente sem ter que se preocupar com a segurança do thread.

No que diz respeito às opções, as duas que você mencionou podem ter um grande impacto no desempenho do sistema, mas ambas acabam sendo uma compensação entre taxa de transferência e latência. O modelo de preempção controla como as coisas executadas no modo kernel (como syscalls) podem ser reprogramadas. As três opções que existem são:

  1. Sem preempção: isso significa que qualquer coisa executada no modo kernel não pode ser interrompida. Ele corresponde ao comportamento do SVR4 e 4.4BSD, bem como ao funcionamento da maioria dos outros sistemas UNIX mais antigos. É muito bom para o rendimento, mas muito ruim para a latência, por isso geralmente só é usado em grandes servidores com muitas CPUs (mais CPUs significa que é mais provável que alguém esteja executando algo que pode ser antecipado).
  2. Preempção voluntária: permite que cada função no kernel defina locais onde pode ser reprogramada. Essa é a configuração usada pela maioria das distribuições Linux voltadas para desktop, pois oferece um bom equilíbrio entre taxa de transferência e latência.
  3. Preempção total: Isso significa que (quase) qualquer código no kernel pode ser interrompido (quase) a qualquer momento. Isto é útil para sistemas que necessitam de latência muito baixa em relação a eventos externos e de entrada, como sistemas usados ​​para trabalho multimídia em tempo real. É absolutamente horrível para o rendimento, mas você não consegue superar a latência.

A frequência do temporizador, por outro lado, é muito mais fácil de explicar. Ele controla o maior período de tempo que algo pode funcionar ininterruptamente se houver algo esperando para ser executado. Valores mais altos resultam em um período de tempo mais curto (menor latência e menor rendimento), valores mais baixos em um período mais longo (maior latência e maior rendimento). Para um início geral, sugiro definir o modelo de preempção como voluntário e a frequência do temporizador para 300 Hz e, em seguida, começar a experimentar primeiro alterar a frequência do temporizador (pois isso geralmente terá um impacto mais visível).

Quanto ao Movidius NCS, vale a pena ou não, depende de quantos dados você precisa trabalhar, porque a largura de banda será limitada pela conexão USB (o Pi tem apenas um único controlador USB 2.0, então você não está limitado apenas a menos de um décimo da largura de banda para a qual o Movidius foi projetado, você também terá que compartilhar o barramento com pelo menos o adaptador Ethernet, o que prejudicará sua latência e sua taxa de transferência). Se você estiver fazendo apenas quadros únicos de 1920x1080 com cores de 32 bits em uma taxa baixa, isso pode ser viável, mas se você precisar fazer o processamento de streaming do mesmo vídeo em taxas de quadros completas, provavelmente encontrará latência problemas. Se você decidir usar um, certifique-se de obter um hub alimentado para ele (caso contrário, você pode ter problemas ao tentar extrair mais energia do que o Pi pode fornecer).

informação relacionada