SIMD (Single Instruction, Multiple Data) es una técnica de computación paralela a nivel de instrucciones que permite aplicar una única operación aritmética a múltiples elementos de datos simultáneamente. Mientras que el auto-vectorizador del compilador (mediante opt-level = 3) es capaz de transformar bucles simples en instrucciones vectoriales, en algoritmos complejos o con patrones de acceso a memoria no triviales, el compilador suele fallar. Es aquí donde std::arch cobra importancia, permitiendo al programador utilizar intrínsecos: funciones que mapean directamente a instrucciones específicas del procesador (como AVX2, AVX-512 o NEON).

El uso de std::arch requiere el uso de bloques unsafe, ya que el programador asume la responsabilidad de garantizar que las instrucciones no provoquen un error de ejecución (como un SIGILL) si la CPU objetivo no soporta dichas extensiones. Para una implementación segura y portable, es imperativo utilizar is_x86_feature_detected!("feature") en tiempo de ejecución para verificar el soporte del hardware antes de ejecutar el código optimizado.

El error más común al trabajar con SIMD es ignorar la alineación de memoria o la longitud de los buffers. Si intentamos cargar un vector de 256 bits (8 floats) en una posición de memoria que no sea múltiplo de 32 bytes usando instrucciones de carga alineada, el programa fallará. Además, es crítico gestionar el “tail” o residuo: si el tamaño del arreglo no es un múltiplo exacto del ancho del registro SIMD, se debe procesar el resto de los elementos mediante un bucle escalar convencional para evitar desbordamientos de memoria.

#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;

/// Suma dos vectores de f32 usando instrucciones AVX2.
/// Retorna true si la operación fue exitosa, false si el hardware no soporta AVX2.
#[cfg(target_arch = "x86_64")]
pub fn sum_vectors_avx2(a: &[f32], b: &[f32], result: &mut [f32]) -> bool {
    // Verificación en runtime de la extensión AVX2
    if !is_x86_feature_detected!("avx2") {
        return false;
    }

    assert_eq!(a.len(), b.len());
    assert_eq!(a.len(), result.len());

    let len = a.len();
    let mut i = 0;

    // Procesar en bloques de 8 floats (256 bits cada uno)
    unsafe {
        while i + 8 <= len {
            // Carga datos de memoria no alineada (unaligned load)
            let va = _mm256_loadu_ps(a.as_ptr().add(i) as *const __m256);
            let vb = _mm256_loadu_ps(b.as_ptr().add(i) as *const __m256);

            // Suma vectorial: va + vb
            let vres = _mm256_add_ps(va, vb);

            // Almacena el resultado de vuelta en la memoria
            _mm256_storeu_ps(result.as_mut_ptr().add(i) as *mut __m256, vres);

            i += 8;
        }
    }

    // Bucle escalar para procesar los elementos restantes (el "tail")
    while i < len {
        result[i] = a[i] + b[i];
        i += 1;
    }

    true
}

fn main() {
    let size = 20;
    let vec_a = vec![1.0f32; size];
    let vec_b = vec![2.0f32; size];
    let mut vec_res = vec![0.0f32; size];

    #[cfg(target_arch = "x86_64")]
    {
        if sum_vectors_avx2(&vec_a, &vec_b, &mut vec_res) {
            println!("Suma SIMD exitosa: {:?}", vec_res);
        } else {
            println!("AVX2 no soportado. Usando fallback escalar.");
            for i in 0..size {
                vec_res[i] = vec_a[i] + vec_b[i];
            }
            println!("Resultado fallback: {:?}", vec_res);
        }
    }
}

Explicación del Código

El código utiliza la arquitectura x86_64 para implementar una suma de vectores optimizada mediante AVX2.

  1. Verificación de Hardware: La función is_x86_feature_detected!("avx2") es crucial para la seguridad; verifica si el procesador actual tiene las instrucciones AVX2 disponibles antes de entrar en el bloque unsafe.
  2. Registros SIMD: Se utilizan los tipos __m256, que representan registros de 256 bits capaces de contener 8 valores f32 (32 bits cada uno) simultáneamente.
  3. Carga de Datos: La función _mm256_loadu_ps realiza una carga “unaligned” (no alineada). Se usa la variante u (loadu) para evitar errores de segmentación si los punteros a.as_ptr().add(i) no están alineados a 32 bytes en memoria.
  4. Operación Vectorial: _mm256_add_ps es el intrínseco que ejecuta la instrucción de suma de punto flotante en todos los elementos del registro en un solo ciclo de instrucción de CPU.
  5. Almacenamiento: _mm256_storeu_ps traslada los 8 resultados calculados desde el registro vres hacia la dirección de memoria del slice result en el índice i.
  6. Gestión del Residuo: Debido a que i aumenta de 8 en 8, si el tamaño del vector es 20, el bucle while i + 8 <= len se ejecutará para i = 0, 8, 16. Los últimos 4 elementos (i=16 a 19) se procesan en el segundo bucle while i < len, asegurando la integridad de los datos sin leer fuera de los límites del array.

118