Я вычислил восемь точечных продуктов одновременно с AVX. В моем текущем коде я делаю что-то вроде этого (перед разворачиванием):
Ivy-Bridge/Sandy-Bridge
__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {
__m256 breg0 = _mm256_load_ps(&b[8*i]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0);
}
Хасуэлл
__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {
__m256 breg0 = _mm256_load_ps(&b[8*i]);
tmp0 = _mm256_fmadd_ps(arge0, breg0, tmp0);
}
Сколько раз мне нужно развернуть цикл для каждого случая, чтобы обеспечить максимальную пропускную способность?
Для Haswell, использующего FMA3, я думаю, что ответ здесь FLOPS за цикл для песчаного моста и haswell SSE2/AVX/AVX2. Мне нужно развернуть цикл 10 раз.
Для Ivy Bridge я думаю, что это 8. Вот моя логика. У добавления AVX есть латентность 3, а умножение - латентность 5. Ivy Bridge может одновременно использовать одно AVX-умножение и одно добавление AVX с использованием разных портов. Используя обозначение m для умножения, a для сложения и x для отсутствия операции, а также число для указания частичной суммы (например, m5 означает умножение с 5-й частичной суммой), я могу написать:
port0: m1 m2 m3 m4 m5 m6 m7 m8 m1 m2 m3 m4 m5 ...
port1: x x x x x a1 a2 a3 a4 a5 a6 a7 a8 ...
Таким образом, используя 8 частичных сумм после девяти тактовых циклов (четыре из нагрузки и пять из умножения), я могу отправить одну загрузку AVX, одно добавление AVX и одно умножение AVX на каждый такт.
Я предполагаю, что это означает, что невозможно достичь максимальной пропускной способности для этих задач в 32-битном режиме в Ivy Bridge и Haswell, поскольку 32-битный режим имеет только восемь регистров AVX?
Изменить: Что касается щедрости. Мои основные вопросы все еще сохраняются. Я хочу получить максимальную пропускную способность либо Ivy Bridge, либо функций Haswell выше, n
может быть любым значением, большим или равным 64. Я думаю, что это можно сделать только с помощью разворачивания (восемь раз для Ivy Bridge и 10 раз для Haswell). Если вы считаете, что это может быть сделано другим способом, то пусть это увидит. В некотором смысле это вариация Как достичь теоретического максимума из 4 FLOP за цикл?. Но вместо того, чтобы только умножать и добавлять, я ищу одну 256-битную нагрузку (или две 128-разрядные нагрузки), одно умножение AVX и одно добавление AVX каждый такт с мостом Ivy или двумя 256-битными нагрузками и двумя инструкциями FMA3 за такт.
Я также хотел бы знать, сколько регистров необходимо. Для Ivy Bridge я считаю это 10. Один для трансляции, один для загрузки (только один из-за переименования регистра) и восемь для восьми частичных сумм. Поэтому я не думаю, что это можно сделать в 32-битном режиме (и действительно, когда я запускаю в 32-битном режиме, производительность значительно падает).
Я должен указать, что компилятор может дать ошибочные результаты Разница в производительности между MSVC и GCC для высоко оптимизированного кода матричной матрицы
Текущая функция, которую я использую для Ivy Bridge, ниже. Это в основном умножает одну строку матрицы 64x64 a
со всей матрицей 64x64 b
(я запускаю эту функцию по 64 раза в каждой строке a
, чтобы получить полную матрицу в матрице c
).
#include <immintrin.h>
extern "C" void row_m64x64(const float *a, const float *b, float *c) {
const int vec_size = 8;
const int n = 64;
__m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
tmp7 = _mm256_loadu_ps(&c[7*vec_size]);
for(int i=0; i<n; i++) {
__m256 areg0 = _mm256_set1_ps(a[i]);
__m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);
__m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
__m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);
__m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);
__m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);
__m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);
__m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);
__m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);
}
_mm256_storeu_ps(&c[0*vec_size], tmp0);
_mm256_storeu_ps(&c[1*vec_size], tmp1);
_mm256_storeu_ps(&c[2*vec_size], tmp2);
_mm256_storeu_ps(&c[3*vec_size], tmp3);
_mm256_storeu_ps(&c[4*vec_size], tmp4);
_mm256_storeu_ps(&c[5*vec_size], tmp5);
_mm256_storeu_ps(&c[6*vec_size], tmp6);
_mm256_storeu_ps(&c[7*vec_size], tmp7);
}