Optimización con NEON en ARM

La tecnología NEON es una extensión SIMD (Single Instruction, Multiple Data) de 128 bits para procesadores ARM Cortex™-A. Está diseñada para acelerar aplicaciones multimedia, mejorando la experiencia del usuario. Dispone de 32 registros de 64 bits (o 16 registros de 128 bits en vista doble).

Los iPhones actuales y la mayoría de los teléfonos Android soportan NEON. Al desarrollar algoritmos móviles, se puede usar NEON para acelerar operaciones. Con registros de longitud 4, la velocidad puede ser hasta 4 veces mayor que la versión escalar.

Las instrucciones NEON realizan procesamiento SIMD empaquetado:

  • Un registro se trata como un vector de elementos del mismo tipo de dato.
  • Los tipos pueden ser: enteros con/sin signo de 8, 16, 32, 64 bits, y punto flotante de precisión simple de 32 bits.
  • La instrucción ejecuta la misma operación en todos los canales.

A continuación, se explican las estructuras y funciones relacionadas con float32x4_t. float32x4_t es equivalente a un vector de 4 elementos; en general, typexN_t es un vector de N elementos.

En programación NEON, la operación sobre un solo elemento se extiende a todo el registro, reduciendo el número de operaciones. Como ejemplo, se muestra cómo calcular la suma de los elementos de un array usando funciones intrínsecas de NEON, optimizando respecto al código escalar original.

Código escalar original (C++)

#include <iostream>
using namespace std;

float sum_array(float *arr, int len) {
    if (arr == nullptr || len < 1) {
        cout << "input error\n";
        return 0.0f;
    }
    float sum = 0.0f;
    for (int i = 0; i < len; ++i) {
        sum += *arr++;
    }
    return sum;
}

Este algoritmo tiene complejidad O(N).

Versión optimizada con NEON

#include <iostream>
#include <arm_neon.h> // necesario para NEON
using namespace std;

float sum_array_neon(float *arr, int len) {
    if (arr == nullptr || len < 1) {
        cout << "input error\n";
        return 0.0f;
    }

    int groups = len >> 2;         // número de grupos de 4 elementos
    int remainder = len & 3;       // elementos restantes

    // vector de suma inicializado a cero
    float32x4_t sum_vec = vdupq_n_f32(0.0f);

    // procesar grupos de 4 elementos
    for (; groups > 0; --groups, arr += 4) {
        float32x4_t data = vld1q_f32(arr);          // cargar 4 floats
        sum_vec = vaddq_f32(sum_vec, data);          // sumar elemento a elemento
    }

    // sumar los elementos del vector acumulador
    float total = vgetq_lane_f32(sum_vec, 0)
                + vgetq_lane_f32(sum_vec, 1)
                + vgetq_lane_f32(sum_vec, 2)
                + vgetq_lane_f32(sum_vec, 3);

    // procesar los elementos restantes
    for (; remainder > 0; --remainder, ++arr) {
        total += *arr;
    }

    return total;
}

La complejidad se reduce a O(N/4).

Funciones NEON utilizadas

  • float32x4_t vdupq_n_f32(float32_t value): duplica el valor en las 4 posiciones del registro.
  • float32x4_t vld1q_f32(float32_t const *ptr): carga 4 floats consecutivos desde la memoria.
  • void vst1q_f32(float32_t *ptr, float32x4_t val): almacena un rgeistro en memoria.
  • float32x4_t vaddq_f32(float32x4_t a, float32x4_t b): suma elemento a elemento (r = a + b).
  • float32x4_t vsubq_f32(float32x4_t a, float32x4_t b): resta elemento a elemento (r = a - b).
  • float32_t vgetq_lane_f32(float32x4_t v, const int lane): extrae el valor del carril indicado (0-3).
  • float32x4_t vmulq_f32(float32x4_t a, float32x4_t b): multiplicación elemento a elemento (r = a * b).
  • float32x4_t vmlaq_f32(float32x4_t a, float32x4_t b, float32x4_t c): realiza r = a + b * c.
  • float32x4_t vextq_f32(float32x4_t a, float32x4_t b, const int n): concatena a y b y devuelve 4 elementos a partir de la posición n (0 <= n <= 3). Ejemplo: a={1,2,3,4}, b={5,6,7,8}; vextq(a,b,1) -> {2,3,4,5}.
  • vfmaq_laneq_f32: multiplicación-acumulación usando un carril específico de otro vector.

Ejemplo adicional con vfmaq_laneq_f32

float32x4_t sum = vdupq_n_f32(0.0f);
float _a[] = {1,2,3,4}, _b[] = {5,6,7,8};
float32x4_t a = vld1q_f32(_a), b = vld1q_f32(_b);
float32x4_t sum1 = vfmaq_laneq_f32(sum, a, b, 0);
// sum1 = sum + a * b[0] = (0,0,0,0) + (1*5,2*5,3*5,4*5) = (5,10,15,20)
float32x4_t sum2 = vfmaq_laneq_f32(sum1, a, b, 1);
float32x4_t sum3 = vfmaq_laneq_f32(sum2, a, b, 2);

Referencias

NEON es fácil de aprender a nivel básico, pero dominarlo reuqiere práctica y dedicación.

Etiquetas: ARM NEON SIMD float32x4_t optimización

Publicado el 6-19 04:33