ARM NEON com Raspberry Pi 2 e Beaglebone Black

NEON

ARM NEON é uma extensão da arquitetura ARM que conta com um set de instruções exclusivo para realização de rotinas otimizadas com foco principal no processamento de imagens e sinais em geral. Está presente nos cores ARM Cortex-A7 (Raspberry Pi 2), ARM Cortex-A8 (Beaglebone Black) e também em todo o resto da família ARM Cortex-A. Bibliotecas famosas como o OpenCV e a libjpeg-turbo já vêem implementando otimizações com ARM NEON e conseguindo uma aceleração de até 4x em determinados algoritmos.

O motor do ARM NEON trabalha com a paralelização na execução de instruções, manipulando dessa forma até 16 dados por registrador de uma só vez. E não é coincidência caso você esteja achando esse recurso parecido com o SIMD descrito no post Explorando algumas instruções SIMD dos microcontroladores ARM Cortex M4 do Felipe Neves, pois a ideia por trás desses recursos é a mesma, e inclusive o nome não “marketizado” do NEON é justamente Advanced SIMD.

Os registradores utilizados pelo NEON são de 64 ou 128 bits e podem ser trabalhados da maneira que quisermos utilizando variáveis padrões de 8, 16, 32 ou 64 bits, podendo inclusive até ser com ponto flutuante. É possível por exemplo trabalhar com 4 variáveis de 32 bits por registrador de uma vez só utilizando um registrador de 128, como demonstrado na ilustração abaixo.

Exemplo de instrução NEON
Figura 1 – Exemplo de instrução NEON

As instruções do NEON se classificam quanto a suas funcionalidades que podem ser de lógica e comparação, processamento geral de dados, aritmética geral, multiplicação, rotacionamento, load/store e as de interação com a unidade de VFP (Vector Floating Point).

Utilização

Para usarmos os recursos oferecidos pelo NEON em linguagem C/C++ temos duas opções. A primeira é utilizarmos vectorizing compiler com as flags-mfpu=neon” e “-ftree-vectorize” para o GCC, colocando a palavra chave “__restrict” nos ponteiros que queremos que o compilador tente otimizar.

A segunda maneira, e mais interessante, é assumirmos as rédeas da situação fazendo uso das funções NEON Intrinsics com o arquivo header arm_neon.h e apenas com a flag-mfpu=neon“. Utilizaremos esse último método para nossos exemplos.

O primeiro programa com NEON

O nosso primeiro exemplo será idêntico tanto para rodar na Raspberry Pi 2 quanto na Beaglebone Black. O objetivo será somar os elementos de mesmo índice entre dois vetores, e armazenar o resultado em um terceiro vetor.

O programa se fosse feito de maneira convencional seria assim:

#include <stdio.h>
#include <stdint.h>

#define SIZE 64 // Arrays size

int main(void){
  uint32_t i;
  uint8_t arr0[SIZE];
  uint8_t arr1[SIZE];
  uint8_t arr2[SIZE];

  for(i=0; i<SIZE; i++){ // Populates the input arrays
    arr0[i] = i;
    arr1[i] = i;
  }

  for(i=0; i<SIZE; i++){ 
    arr2[i] = arr1[i] + arr0[i];
  }

  for(i=0; i<SIZE; i++){ // Prints the output array
    printf("Index[%2u] %2u + %2u = %3u\n", i, arr0[i], arr1[i], arr2[i]);
  }

  return 0;
}

Agora vamos passo-a-passo adotando a NEON Intrinsics para otimizar o código. Primeiro adicionamos o include para utilizarmos o header do NEON Intrinsics:

#include <arm_neon.h>

Depois declaramos duas estruturas de 128 bits que serão utilizadas em nossas operações. Cada uma será organizada em 16 lanes de unsigned int de 8 bits.

  uint8x16_t arr0_;
  uint8x16_t arr1_;

Agora a última e principal mudança no nosso loop responsável por executar as somas:

for(i=0; i<SIZE; i+=16){
    arr0_ = vld1q_u8(arr0 + i); // arr0_ = arr0
    arr1_ = vld1q_u8(arr1 + i); // arr1_ = arr1
    arr1_ = vaddq_u8(arr1_, arr0_); // arr1_ = arr1_ + arr0_
    vst1q_u8((arr2+i), arr1_); // arr2 = arr1_
 }
  • Podemos notar que agora nosso loop incrementa a variável i de 16 em 16 e não mais de 1 em 1. Isso porque em cada iteração, atuamos em 16 posições de cada vetor de uma vez só;
  • A função vldlq_u8 é utilizada para carregar 16 bytes de um determinado vetor para uma estrutura de 128 bits que foi declarada anteriormente;
  • A vaddq_u8 serve para efetuar a soma entre as duas estruturas de 128 bits, tratando individualmente cada byte como uint8_t;
  • E por último vstlq_u8 é utilizado para armazenar a estrutura com a resposta em um vetor convencional.

O código final ficará assim:

#include <stdio.h>
#include <arm_neon.h>

#define SIZE 64 // Arrays size

int main(void){
 uint32_t i;
 uint8_t arr0[SIZE];
 uint8_t arr1[SIZE];
 uint8_t arr2[SIZE];

 for(i=0; i<SIZE; i++){ // Populates the input arrays
   arr0[i] = i;
   arr1[i] = i;
 }

 uint8x16_t arr0_;
 uint8x16_t arr1_;

 for(i=0; i<SIZE; i+=16){
   arr0_ = vld1q_u8(arr0 + i); // arr0_ = arr0
   arr1_ = vld1q_u8(arr1 + i); // arr1_ = arr1
   arr1_ = vaddq_u8(arr1_, arr0_); // arr1_ = arr1_ + arr0_
   vst1q_u8((arr2+i), arr1_); // arr2 = arr1_
 }

 for(i=0; i<SIZE; i++){ // Prints the output array
   printf("Index[%2u] %2u + %2u = %3u\n", i, arr0[i], arr1[i], arr2[i]);
 }

 return 0;
}

Para compilar e rodar o código na sua placa basta executar:

gcc main.c -o neon_example.out -O2 -mfpu=neon
./neon_example.out

Lembrando que estamos fazendo uma compilação embarcada no próprio target, no caso em nossas Raspberry Pi 2Beaglebone Black. Também é possível ser feita uma cross-compilação com NEON sem nenhuma dificuldade, bastando que a toolchain também tenha suporte ao NEON.

Elaborando um pouco mais com NEON

Para o segundo exemplo, vamos fazer algo um pouco mais elaborado, um programa para fazer a média dos elementos de mesmo índice entre dois vetores, e armazenar o resultado em um terceiro vetor.

E para termos noção do efeito prático dessa otimização vamos rodar o código com vetores gigantes, e também marcar o tempo decorrido na nossa rotina convencional e na otimizada para comparação.

Vou deixar o programa em um repositório no GitHub para o post não ficar muito poluído, e vou mostrar aqui apenas o trecho importante, onde faço a média entre cada par de elemento utilizando NEON.

void neon_routine(uint8_t *arr2, uint8_t *arr1, uint8_t *arr0){

 uint32_t i;
 uint8x16_t arr0_;
 uint8x16_t arr1_;

 for(i=0; i<SIZE; i+=16){
   arr0_ = vld1q_u8(arr0 + i); // arr0_ = arr0
   arr0_ = vshrq_n_u8(arr0_, 1); // arr0_ = arr0_ >> 1

   arr1_ = vld1q_u8(arr1 + i); // arr1_ = arr1
   arr0_ = vsraq_n_u8(arr0_, arr1_, 1); // arr0_ = arr0_ + (arr1_ >> 1)

   vst1q_u8((arr2+i), arr0_); // arr2 = arr0_
 }
}

Para baixar e executar nosso código na sua placa, basta seguir os comandos abaixo:

git clone https://github.com/igorTavares/neon_test.git
cd neon_test
./build
./neon_test.out

Obs: É importante que para execução do teste você tenha o mínimo de processos rodando no sistema, a fim de que eles não interfiram muito no nosso experimento.

Resultado na Raspberry Pi 2

Para a realização desse teste foi utilizada uma Raspberry Pi 2 Model B com Raspbian Wheezey.

Para aprender como gravar uma imagem na sua Raspberry Pi 2 vou deixar um link aqui para um outro post do site que trata especificamente disso.

Starting the NEON test...
Starting the test routine w/o NEON.
Elapsed time: 1465 ms
Starting the test routine with NEON.
Elapsed time: 483 ms

Resultado na Beaglebone Black

Para a realização desse teste foi utilizada uma Beaglebone Black Revisão C com Debian 7.5 e rodando a 1GHz.

Para aprender como gravar uma imagem na sua Beaglebone Black vou deixar um link aqui para um outro post do site que trata especificamente disso.

Starting the NEON test...
Starting the test routine w/o NEON.
Elapsed time: 1541 ms
Starting the test routine with NEON.
Elapsed time: 385 ms

Conclusões

  • Comprovamos a eficácia da extensão NEON em nosso programa com uma aceleração de 3 a 4 vezes na nossa rotina;
  • Com uma análise simples dos nossos códigos, conseguimos deduzir que quanto maior forem as variáveis trabalhadas menos o NEON será eficiente, visto que menos operações paralelas serão executadas de cada vez;
  • Também é possível observar que quanto mais os dados forem trabalhados após serem carregados nos registradores do NEON mais eficiente o algoritmo ficará, visto que as instruções de load/store ficaram com um tempo cada vez menos considerável dentro da rotina.

Referências

  1. ARM® C Language Extensions Release 1.1
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Hardware » Placas de desenvolvimento » ARM NEON com Raspberry Pi 2 e Beaglebone Black

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: