Последнее обновление: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 |
Ещё пример:
_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));
2.2 Быстрое перемешивание порядка данных¶
Если входные данные
то мы можем быстро получить: Соответствующая реализация: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 Вращение¶
Для массива:
мы можем повернуть и получить: or то есть 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 — дерзайте вместе со мной~
Примечание: полный пример этого раздела уже выложен в закрытом сообществе, спасибо~
Похожие темы¶
- Путеводитель по C++ Concurrency in Action | C++那些事
- Поиск взаимоблокировок в C++ | C++那些事
- Параллельный STL в C++17 | C++那些事
- C++ False Sharing и строка кэша | C++那些事
- Отладка многопоточности GDB в режиме All-Stop | C++那些事
- Обнаружение ошибок конкурентности с помощью Helgrind | C++那些事
- Принцип событийного цикла libev | C++那些事
- Внутренняя реализация C++ std::thread | C++那些事
- C++ std::thread и jthread | C++那些事
- Введение в многопоточность C++ | C++那些事