Última atualização:2026-10-12
Vetorização SIMD em C++¶
Recentemente andei estudando conteúdo relacionado a vetorização e, de início, parece meio assustador. Aproveitando a oportunidade, vamos aprender programação de alta performance com SIMD.
SIMD significa single-instruction multiple-data, ou seja, instrução única, múltiplos dados.
Na arquitetura de computadores tradicional, a CPU processa apenas um elemento de dados por vez. No entanto, muitas tarefas envolvem executar a mesma operação sobre uma grande quantidade de dados, como somar, multiplicar ou aplicar operações lógicas a todos os elementos de um array. A programação SIMD fornece à CPU conjuntos de instruções dedicados que permitem executar a mesma operação sobre vários elementos de dados simultaneamente.
Esse modo de processamento é especialmente adequado para cálculos que envolvem vetores, matrizes, imagens, áudio e vídeo.
Os mais usados atualmente são SSE, SSE2, AVX128, AVX256 e AVX512.
Nesta seção, aprenderemos brevemente algumas operações do AVX512; há muitas operações e aqui introduzimos apenas algumas.
1.Terminologia¶
A primeira questão é que o código de programação SIMD não se parece muito com o código que escrevemos normalmente: tantos underscores e nomes que parecem incompreensíveis. Como entendê-los?
Por exemplo:
- _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.
Assim, encontrei a seguinte tabela:
| 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 |
Outro exemplo:
_mm512_mask_load_ps
_mm512_mask_loadu_ps
O u significa unordered, indicando um carregamento sem alinhamento: ao usar a função _mm512_mask_loadu_ps para carregar dados da memória, nenhum requisito de alinhamento de endereço de memória é aplicado. Já _mm512_mask_load_ps exige o alinhamento de 64 bytes.
Estudando com essa tabela de referência, é possível entender muito rapidamente o significado de cada interface.
Para as APIs relacionadas, consulte o Intel Intrinsics Guide.
https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
2.Exemplos práticos¶
2.1 Encontrar o valor mínimo¶
Com 512 bits, podemos armazenar 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));
2.2 Embaralhar rapidamente a ordem dos dados¶
Para os dados de entrada
podemos obter rapidamente: Implementação correspondente: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 Rotação¶
Para um array:
podemos rotacionar e obter: or Ou seja, rotate down ou rotate up.Aqui, podemos implementar assim:
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;
}
Continuarei estudando SIMD por aí; vamos nessa juntos~
Nota: o exemplo completo desta seção foi publicado na planeta, obrigado~
Tópicos relacionados¶
- Guia de C++ Concurrency in Action | C++那些事
- Depuração de Deadlocks em C++ | C++那些事
- STL Paralela do C++17 | C++那些事
- False Sharing e Linhas de Cache em C++ | C++那些事
- Depuração All-Stop multi-thread com GDB | C++那些事
- Detecção de Erros de Concorrência com Helgrind | C++那些事
- Princípio do event loop do libev | C++那些事
- Implementação interna de std::thread em C++ | C++那些事
- C++ std::thread e jthread | C++那些事
- Introdução a multi-thread em C++ | C++那些事