Última actualización:2026-10-12
Vectorización SIMD en C++¶
Últimamente estoy viendo contenido relacionado con la vectorización y resulta un poco abrumador; aprovecho la ocasión para aprender programación SIMD de alto rendimiento.
SIMD es el acrónimo de single-instruction multiple-data: una sola instrucción, múltiples datos.
En la arquitectura tradicional de los ordenadores, la CPU solo puede procesar un elemento de datos cada vez. Sin embargo, muchas tareas implican ejecutar la misma operación sobre gran cantidad de datos, como sumar, multiplicar o aplicar operaciones lógicas a todos los elementos de un array. La programación SIMD dota a la CPU de juegos de instrucciones especializados que le permiten ejecutar la misma operación sobre varios elementos de datos a la vez.
Esta forma de procesamiento es especialmente adecuada para cálculos con datos como vectores, matrices, imágenes, audio y vídeo.
Los más usados actualmente son SSE, SSE2, AVX128, AVX256 y AVX512.
En esta sección aprenderemos de forma sencilla algunas operaciones de AVX512; hay muchas, así que aquí solo introducimos algunas.
1. Terminología¶
El primer problema es que el código de programación simd no se parece mucho al que solemos escribir: tantos guiones bajos y nombres resultan incomprensibles. ¿Cómo entenderlos?
Por ejemplo:
- _mm512_set1_ps
Broadcast single-precision (32-bit) floating-point value a to all elements of dst.
- _mm512_set1_epi32
Broadcast 32-bit integer a to all elements of dst.
Así que encontré la siguiente tabla:
| Abbreviation | Full Name | C/C++ Equivalent |
|---|---|---|
| ps | packed single-precision | float |
| ph | packed half-precision | None* |
| pd | packed double-precision | double |
| pch | packed half-precision complex | None* |
| pi8 | packed 8-bit integer | int8_t |
| pi16 | packed 16-bit integer | int16_t |
| pi32 | packed 32-bit integer | int32_t |
| epi8 | extended packed 8-bit integer | int8_t |
| epi16 | extended packed 16-bit integer | int16_t |
| epi32 | extended packed 32-bit integer | int32_t |
| epi64 | extended packed 64-bit integer | int64_t |
| epi64x | extended packed 64-bit integer | int64_t |
Otro ejemplo:
_mm512_mask_load_ps
_mm512_mask_loadu_ps
La u significa unordered e indica una carga sin orden: al usar la función _mm512_mask_loadu_ps para cargar datos de la memoria no se aplica ningún requisito de alineación de la dirección de memoria, mientras que _mm512_mask_load_ps exige que se cumpla un alineamiento de 64 bytes.
Estudiando con esta tabla a mano, se entiende muy rápido el significado de cada interfaz.
Puedes consultar las API relacionadas en la Intel Intrinsics Guide.
https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
2. Ejemplos prácticos¶
2.1 Obtener el mínimo¶
Con 512 bits podemos almacenar 16 floats de 32 bits.
static inline rf_512 load(float* ptr) { return _mm512_loadu_ps(ptr); }
float a[width] = {1.0, 2.0, 3.0, 22.0, 5.0, 17.0, 9.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};
float b[width] = {3.3, 6.2, 5.3, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 10.10, 21.11, 12.12, 13.13, 14.14, 15.0, 16.16};
rf_512 data = minimum(load(a), load(b));
Así podemos obtener rápidamente:
2.2 Barajar rápidamente el orden de los datos¶
Si los datos de entrada son
podemos obtener rápidamente:
Implementación correspondiente:
static inline rf_512 permutexvar(ri_512 idx, rf_512 src) {
return _mm512_permutexvar_ps(idx, src);
}
/*
raw data: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
shuffle data: 16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
*/
void print_permutexvar_mask() {
float a[width] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0};
float b[width] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8,
9.9, 10.10, 11.11, 12.12, 13.13, 14.14, 15.0, 16.16};
mask_type mask = make_bit_mask<1, 0, 1, 1, 0>();
int idx_array[width] = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array);
rf_512 result = permutexvar_mask(load(a), mask, idx, load(b));
float result_arr[width];
store(result, result_arr);
std::cout << "raw data: ";
for (int i = 0; i < width; i++) {
std::cout << a[i] << " ";
}
std::cout << std::endl;
std::cout << "shuffle data: ";
for (int i = 0; i < width; i++) {
std::cout << result_arr[i] << " ";
}
std::cout << std::endl;
}
2.3 Rotación¶
Para un array:
podemos obtener por rotación:
or
Es decir, rotate down o rotate up.
Aquí podemos implementarlo así:
rf_512 rotateGeneral(float* arr, int s) {
int idx_array[width];
for (int i = 0; i < width; i++) {
idx_array[i] = (i + s) % width;
}
ri_512 idx = _mm512_loadu_si512((__m512i*)idx_array);
rf_512 result = permutexvar(idx, load(arr));
return result;
}
Etcétera; hay más ejemplos. Se puede comprobar que, usando simd, podemos implementar algoritmos muy interesantes y acelerar el procesamiento de arrays y de datos por lotes.
Seguiré aprendiendo simd más adelante; ¡esforcémonos juntos~
Nota: el ejemplo completo de esta sección ya está publicado en la comunidad 星球, gracias~
Temas relacionados¶
- Guía de lectura de C++ Concurrency in Action | C++那些事
- Localización de interbloqueos en C++ | C++那些事
- STL paralelo de C++17 | C++那些事
- False Sharing y líneas de caché en C++ | C++那些事
- Depuración multihilo All-Stop con GDB | C++那些事
- Detección de errores de concurrencia con Helgrind | C++那些事
- Principios del bucle de eventos de libev | C++那些事
- Implementación interna de std::thread en C++ | C++那些事
- std::thread y jthread en C++ | C++那些事
- Introducción al multihilo en C++ | C++那些事