Как достичь теоретического максимума в 4 FLOP/такт на современных процессорах x86-64?
Понимание проблемы: почему ваш код далёк от пиковой производительности
Ваш тест показывает лишь малую часть возможностей процессора. Теоретические 4 FLOP/такт относятся к пиковой производительности при идеальных условиях, когда:
- Данные уже находятся в кэше L1 (нет задержек памяти)
- Используются SIMD-инструкции (AVX, AVX-512)
- Конвейер загружен без простоев (нет зависимостей по данным)
- Выполняются операции умножения и сложения (FMA — fused multiply-add)
Ваш код с c[i] = a[i] + b[i] — это всего 1 FLOP на итерацию (одно сложение). Даже если процессор выполнит его за 1 такт, вы получите 1 FLOP/такт. Для 4 FLOP/такт нужно выполнять 4 независимые операции за такт, что возможно только с FMA-инструкциями (2 умножения + 2 сложения) или двумя AVX-векторами за такт.
Шаг 1: Используйте FMA (fused multiply-add) для удвоения FLOP/такт
Современные процессоры имеют специальные блоки FMA, которые выполняют a = a + b * c за один такт. Это даёт 2 FLOP за одну инструкцию. Чтобы задействовать их, нужно переписать код так, чтобы умножение и сложение были объединены.
// Пример: c[i] = a[i] * b[i] + c[i] (FMA)
for (size_t i = 0; i < N; ++i) {
c[i] = a[i] * b[i] + c[i]; // 2 FLOP
}
Однако компилятор может не всегда автоматически генерировать FMA. Используйте флаги -ffast-math -mfma или intrinsic-функции для гарантии.
Шаг 2: Векторизация с AVX/AVX-512
Одна AVX-инструкция (256-бит) обрабатывает 4 числа double одновременно. Если процессор может выполнять 2 таких инструкции за такт (например, Haswell и новее), то получаем:
- 2 AVX-инструкции × 4 элемента × 2 FLOP (FMA) = 16 FLOP/такт
- Или 2 AVX-инструкции × 4 элемента × 1 FLOP (сложение) = 8 FLOP/такт
Ваш процессор i7-10750H (Comet Lake) поддерживает AVX2 и FMA. Теоретически он может выполнять 2 AVX-инструкции за такт, что даёт до 16 FLOP/такт для FMA (но на практике из-за ограничений по питанию и теплу — около 8-12).
Шаг 3: Пример кода с intrinsics для достижения 4+ FLOP/такт
#include <immintrin.h>
#include <vector>
#include <iostream>
void saxpy_fma_avx(double* c, const double* a, const double* b, size_t n) {
size_t i = 0;
// Обрабатываем по 4 элемента за раз (AVX)
for (; i + 4 <= n; i += 4) {
__m256d va = _mm256_loadu_pd(&a[i]); // загружаем a[i..i+3]
__m256d vb = _mm256_loadu_pd(&b[i]); // загружаем b[i..i+3]
__m256d vc = _mm256_loadu_pd(&c[i]); // загружаем c[i..i+3]
vc = _mm256_fmadd_pd(va, vb, vc); // c = a*b + c (2 FLOP на элемент)
_mm256_storeu_pd(&c[i], vc); // сохраняем
}
// Обработка остатка
for (; i < n; ++i) {
c[i] = a[i] * b[i] + c[i];
}
}
int main() {
const size_t N = 10'000'000;
std::vector<double> a(N, 1.0), b(N, 2.0), c(N, 0.0);
// Прогрев кэша
saxpy_fma_avx(c.data(), a.data(), b.data(), N);
auto start = std::chrono::high_resolution_clock::now();
saxpy_fma_avx(c.data(), a.data(), b.data(), N);
auto end = std::chrono::high_resolution_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count();
double flops = (2.0 * N) / elapsed; // 2 FLOP на элемент (FMA)
std::cout << "GFLOPS: " << flops / 1e9 << std::endl;
return 0;
}
Компилируйте с флагами: -O3 -march=native -ffast-math -funroll-loops. На i7-10750H этот код даст ~8-12 GFLOPS на одном ядре (3-4 FLOP/такт).
Шаг 4: Дополнительные советы для максимума
- Избегайте зависимостей по данным: Используйте несколько аккумуляторов для разрыва цепочек (например, 4 независимых суммы).
- Выравнивание памяти: Используйте
_mm256_load_pd(выровненный доступ) вместо_mm256_loadu_pdдля ускорения на 5-10%. - Разворачивание циклов: Вручную разверните цикл на 4-8 итераций, чтобы загрузить конвейер.
- Потоки: Используйте OpenMP (
#pragma omp parallel for) для задействования всех ядер, но помните, что 4 FLOP/такт — это на одно ядро.
Помните: теоретический максимум — это только пик в идеальных условиях. На практике из-за ограничений по питанию (Turbo Boost), пропускной способности памяти и кэша вы получите 70-80% от пика. Но ваш код уже будет в 4-5 раз быстрее исходного!
<<Альтернативный подход: использование библиотек для автоматической векторизации
Если вы не хотите писать intrinsic-код вручную, можно довериться оптимизирующим библиотекам, которые уже реализуют максимальную производительность. Самые популярные варианты:
- Eigen — библиотека для линейной алгебры. Использует AVX, FMA и многопоточность автоматически. Пример:
c = a.array() * b.array() + c.array(); - Intel MKL (oneMKL) — коммерческая библиотека от Intel с ручной оптимизацией под каждый процессор. Даёт 90-95% от пика.
- OpenBLAS — бесплатная альтернатива MKL.
Пример с Eigen:
#include <Eigen/Dense>
#include <i Ответов пока нет.
Войдите чтобы оставить ответ