Векторные инструкции и SIMD¶
Что такое SIMD¶
SIMD (Single Instruction, Multiple Data) — архитектурный принцип, при котором одна инструкция обрабатывает сразу несколько элементов данных. В отличие от скалярных операций, обрабатывающих один элемент, SIMD-инструкции работают с «векторами» данных:
Скалярная инструкция: c = a + b (одна пара чисел)
Векторная инструкция: c[0:7] = a[0:7] + b[0:7] (8 пар одновременно)
Это позволяет достичь теоретического ускорения в N раз, где N — ширина вектора. На практике ускорение составляет 4–8x для compute-bound задач.
Расширения x86-64 для SIMD¶
В архитектуре x86-64 SIMD реализован через несколько расширений, появлявшихся постепенно:
| Расширение | Год | Ширина регистра | Элементы (float) | Примеры функций |
|---|---|---|---|---|
| SSE | 1999 | 128 бит | 4 × float32 | _mm_add_ps |
| SSE2 | 2001 | 128 бит | 2 × float64 | _mm_add_pd |
| AVX | 2011 | 256 бит | 8 × float32 | _mm256_add_ps |
| AVX2 | 2013 | 256 бит | 8 × int32 | _mm256_add_epi32 |
| AVX-512 | 2017 | 512 бит | 16 × float32 | _mm512_add_ps |
Intel intrinsics — функции на C/C++, которые напрямую соответствуют SIMD-инструкциям процессора и позволяют использовать их без написания ассемблерного кода.
Векторные регистры¶
SIMD-инструкции используют отдельный набор регистров:
- XMM0–XMM15 (128 бит) — для SSE/SSE2; расширяются до XMM0–XMM31 при использовании EVEX-кодировки AVX-512;
- YMM0–YMM15 (256 бит) — для AVX (расширение XMM, нижние 128 бит = XMM); расширяются до YMM0–YMM31 с AVX-512;
- ZMM0–ZMM31 (512 бит) — для AVX-512 (расширение YMM).
Запись в XMM0 обнуляет верхние биты YMM0 (в режиме AVX). Запись в YMM0 обнуляет верхние биты ZMM0.
Использование AVX через intrinsics¶
Для использования intrinsics нужно подключить заголовочный файл:
Пример: вычисление result[i] = a[i] * b[i] + c[i] для 8 элементов:
/* Без AVX: 8 итераций цикла */
float result[8];
for (int i = 0; i < 8; i++)
result[i] = a[i] * b[i] + c[i];
/* С AVX FMA: 1 инструкция = 8 умножений + 8 сложений */
__m256 va = _mm256_loadu_ps(a); /* загрузить 8 float */
__m256 vb = _mm256_loadu_ps(b);
__m256 vc = _mm256_loadu_ps(c);
__m256 vres = _mm256_fmadd_ps(va, vb, vc); /* va*vb + vc */
_mm256_storeu_ps(result, vres); /* сохранить 8 float */
Пример: скалярное произведение с AVX¶
#include <immintrin.h>
/* Обычная версия */
float dot_product(const float *a, const float *b, int n) {
float sum = 0.0f;
for (int i = 0; i < n; i++)
sum += a[i] * b[i];
return sum;
}
/* AVX-версия (n должно быть кратно 8) */
float dot_product_avx(const float *a, const float *b, int n) {
__m256 acc = _mm256_setzero_ps(); /* acc = [0,0,0,0,0,0,0,0] */
for (int i = 0; i < n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 prod = _mm256_mul_ps(va, vb);
acc = _mm256_add_ps(acc, prod);
}
/* Горизонтальная редукция: сложить 8 элементов acc в один скаляр */
__m128 lo = _mm256_castps256_ps128(acc);
__m128 hi = _mm256_extractf128_ps(acc, 1);
__m128 sum = _mm_add_ps(lo, hi);
sum = _mm_hadd_ps(sum, sum);
sum = _mm_hadd_ps(sum, sum);
return _mm_cvtss_f32(sum);
}
Автовекторизация компилятором¶
Современные компиляторы умеют автоматически преобразовывать обычные циклы в SIMD-инструкции при наличии соответствующих флагов:
Флаг -fopt-info-vec позволяет увидеть, какие циклы были векторизованы:
Для успешной автовекторизации цикл должен: - не иметь зависимостей между итерациями; - иметь простой предсказуемый паттерн доступа к памяти; - работать с данными, выровненными по размеру вектора (желательно).
Выравнивание данных для SIMD¶
SIMD-инструкции работают быстрее, если данные выровнены по ширине вектора:
- 16 байт (SSE):
alignas(16)или_mm_load_psвместо_mm_loadu_ps - 32 байта (AVX):
alignas(32)или_mm256_load_psвместо_mm256_loadu_ps
/* Выровненный массив — допускает использование load (не loadu) */
float arr[8] __attribute__((aligned(32)));
__m256 v = _mm256_load_ps(arr); /* требует выравнивания по 32 байтам */
/* vs */
__m256 v = _mm256_loadu_ps(arr); /* не требует, но может быть чуть медленнее */
На современных процессорах (Haswell и новее) разница между выровненной и невыровненной загрузкой минимальна, если данные не пересекают границу страницы.
Проверка поддержки AVX¶
Не все процессоры поддерживают AVX-512. AVX2 поддерживается большинством x86-64 процессоров, выпущенных после 2013 года.
Связанные темы¶
- Параллелизм на уровне инструкций — ILP, суперскалярность, OoOE
- Кэши процессора — выравнивание данных и кэш-линии
- Встроенный ассемблер в GCC — измерение производительности через rdtsc
Источники¶
- Intel Intrinsics Guide (интерактивный справочник всех SIMD intrinsics): https://www.intel.com/content/www/us/en/docs/intrinsics-guide/
man 1 gcc— флаги-mavx2,-msse4.2,-ftree-vectorize- Agner Fog, "Optimizing software in C++", Chapter 13 — SIMD: https://agner.org/optimize/
grep avx /proc/cpuinfo— поддержка расширений на конкретном процессоре