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.
- 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 bloqueunsafe. - Registros SIMD: Se utilizan los tipos
__m256, que representan registros de 256 bits capaces de contener 8 valoresf32(32 bits cada uno) simultáneamente. - Carga de Datos: La función
_mm256_loadu_psrealiza una carga “unaligned” (no alineada). Se usa la varianteu(loadu) para evitar errores de segmentación si los punterosa.as_ptr().add(i)no están alineados a 32 bytes en memoria. - Operación Vectorial:
_mm256_add_pses 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. - Almacenamiento:
_mm256_storeu_pstraslada los 8 resultados calculados desde el registrovreshacia la dirección de memoria del sliceresulten el índicei. - Gestión del Residuo: Debido a que
iaumenta de 8 en 8, si el tamaño del vector es20, el buclewhile i + 8 <= lense ejecutará parai = 0, 8, 16. Los últimos 4 elementos (i=16a19) se procesan en el segundo buclewhile i < len, asegurando la integridad de los datos sin leer fuera de los límites del array.
N° 118