Ir para o conteúdo

Ú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

https://stackoverflow.com/questions/70911872/what-are-the-names-and-meanings-of-the-intrinsic-vector-element-types-like-epi6

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));
Assim, podemos obter rapidamente:

1 2 3 4.4 5 6.6 7.7 8 9 10 11 12 13 14 15 16

2.2 Embaralhar rapidamente a ordem dos dados

Para os dados de entrada

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
podemos obter rapidamente:

16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
podemos rotacionar e obter:

4 5 6 7 8 9 10 11 12 13 14 15 16 1 2 3
or

14 15 16 1 2 3 4 5 6 7 8 9 10 11 12 13
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;
}
E há muitos outros exemplos: dá para perceber que, usando SIMD, conseguimos implementar algoritmos bem interessantes e acelerar o processamento de arrays e de dados em lote.

Continuarei estudando SIMD por aí; vamos nessa juntos~

Nota: o exemplo completo desta seção foi publicado na planeta, obrigado~

Tópicos relacionados

Comentários