Перейти к содержанию

Последнее обновление:2026-10-12

Векторизация C++ SIMD

В последнее время изучаю материалы по векторизации — выглядит довольно пугающе; воспользуюсь случаем, чтобы освоить высокопроизводительное SIMD-программирование.

SIMD расшифровывается как single-instruction multiple-data — одна инструкция, много данных.

В традиционной компьютерной архитектуре CPU за раз обрабатывает только один элемент данных. Однако многие задачи связаны с выполнением одинаковых операций над большими объёмами данных: сложение, умножение или логические операции над всеми элементами массива и т.д. SIMD-программирование предоставляет CPU специализированные наборы инструкций, позволяя ему одновременно выполнять одинаковые операции над несколькими элементами данных.

Такой способ обработки особенно подходит для вычислений над векторами, матрицами, изображениями, аудио и видео.

Сейчас чаще всего используются SSE, SSE2, AVX128, AVX256, AVX512.

В этом разделе мы кратко познакомимся с некоторыми операциями AVX512 — их довольно много, здесь приведём лишь некоторые.

1. Терминология

Первый вопрос: код simd-программирования не очень похож на обычный код — сплошные подчёркивания и названия, ничего непонятно. Как в этом разобраться?

Например:

  • _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.

И тогда нашлась вот эта таблица:

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

Ещё пример:

_mm512_mask_load_ps

_mm512_mask_loadu_ps

u означает unordered — загрузка без упорядочивания: когда данные из памяти загружаются функцией _mm512_mask_loadu_ps, никаких требований к выравниванию адреса памяти не применяется. А _mm512_mask_load_ps требует выравнивания по 64 байта.

Изучая таким образом сверяясь с таблицей, можно очень быстро узнать значение каждого интерфейса.

С соответствующими API можно ознакомиться в Interl Intrinsics Guide.

https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html

2. Практические примеры

2.1 Поиск минимума

В 512 бит мы можем хранить 16 32-битных float.

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));
И мы можем быстро получить:

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

2.2 Быстрое перемешивание порядка данных

Если входные данные

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
то мы можем быстро получить:

16.16 2 14.14 13.13 5 6 7 8 9 10 11 12 13 14 15 16
Соответствующая реализация:

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 Вращение

Для массива:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
мы можем повернуть и получить:

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
то есть rotate down или rotate up.

Здесь это можно реализовать так:

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;
}
И так далее — есть и другие примеры; можно заметить, что с помощью simd мы способны реализовывать весьма интересные алгоритмы и ускорять обработку массивов и больших пакетов данных.

Далее буду продолжать изучать simd — дерзайте вместе со мной~

Примечание: полный пример этого раздела уже выложен в закрытом сообществе, спасибо~

Похожие темы

Комментарии