То есть каждый элемент вашего результирующего вектора является замаскированной суммой входного вектора? И эти маски происходят из столбцов матрицы, поэтому они не являются непрерывными битами.
Маскированная сумма с использованием непрерывного растрового изображения тривиальна для AVX512 (просто используйте добавление с маскированием слиянием или загрузки с нулевой маской). В SSE / AVX2 вы бы использовали . Есть ли обратная инструкция к команде movemask в intel avx2? + _mm256_and_ps
. Или некоторое изменение в том, что оптимизирует по векторам маски, например, с 32-битной широковещательной нагрузкой, а затем сдвигает это для следующего шага. Вместо того, чтобы делать еще одну передачу невыровненного слова для каждого байта.
Но с вашими битами маски не смежными у вас есть выбор:
- Делать каждый элемент вектора вывода отдельнос горизонтальной суммой в конце. Требуется собрать биты и сделать векторную маску. Вероятно, трудно, за исключением случая M = 32, когда шаг битов уже выстраивает их в ряд с непрерывными 32-разрядными числами с плавающей запятой.
- накапливает вектор из 4 или 8 выходных элементов , используя смежные группы4 или 8 битов маски. Таким образом, вы векторизуете внешний цикл, выполняя широковещательную загрузку во внутреннем цикле над входным вектором. ИСПОЛЬЗУЙТЕ ЭТО. Вы должны развернуть с несколькими векторными суммами, чтобы скрыть задержку добавления FP.
Трансляционные нагрузки типа __m256 v = _mm256_set1_ps(invec[i])
в основном бесплатны (vbroadcastss
- это чистая загрузка, без ALU shuffle uop). Вам не нужны никакие другие перетасовки поплавков, просто чистая вертикальная SIMD, даже в конце цикла: вы просто _mm256_storeu_ps
в выходной вектор.
И вы используете непрерывные группы битов маскипоэтому полезны обычные вопросы и ответы по обратной маске.
// untested, rough outline of what it might look like
uint8_t matrix[rows * cols]; // bit matrix in chunks of 8 bits
float invec[N], outvec[N]; // A normal function will just take pointer inputs.
constexpr int unroll = 4;
for(int outpos = 0 ; outpos < M-8*unroll+1 ; outpos += 8 * unroll) {
__m256 sum0, sum1, sum2, sum3; //optionally use an array of accumulators, sums[unroll];
sum0 = sum1 = sum2 = sum3 = _mm256_setzero_ps();
// optionally peel the first inner iteration to just load+mask without adding to 0.0
for (int inpos = 0 ; in < N ; in++ ){
__m256 inv = _mm256_set1_ps(invec[inpos]);
__m256 mask0 = inverse_movemask(matrix[outpos*stride + inpos + 0]); // 8 bits -> 8 vector elements
__m256 mask1 = inverse_movemask(matrix[outpos*stride + inpos + 1]);
...
sum0 = _mm256_add_ps(sum0, _mm256_and_ps(inv, mask0) ); // add in[i] or 0.0 according to mask
sum1 = _mm256_add_ps(sum1, _mm256_and_ps(inv, mask1) );
...
}
__m256_storeu_ps(&outvec[outpos + 0*8], sum0);
__m256_storeu_ps(&outvec[outpos + 1*8], sum1);
__m256_storeu_ps(&outvec[outpos + 2*8], sum2);
...
}
not-unrolled __m256 and/or __m128 cleanup for M % (8*unroll) != 0
cleanup for M % 4 != 0 using __m128 broadcast loads
for the last 1..3 rows of masks
maybe use a masked store (AVX2 vmaskmov) or pad your output vector
Каждая итерация внутреннего цикла маскирует один метод с плавающей запятой 8 * unroll
различными способами и накапливается в соответствующие 8 * unroll
различные промежуточные суммы. (Через unroll
векторов по 8 чисел с плавающей запятой.)
Это также хорошо для пропускной способности памяти
Каждый битовый бит читается только один раз в продукте vec * mat, новектор ввода эффективно используется M
раз. Зацикливание на смежных строках растрового изображения дает хорошую локальность, не требуя загрузки какой-либо из этих строк кэша более одного раза.
Даже с AVX512 и 2x _mm512_mask_add_ps
за такт, добавленный 1 бит на элемент FP не так уж много пропускной способностидля растровых загрузок.
Однако вы перебираете свой входной вектор M/(8*unroll)
раз. Маскированные добавления для каждого вектора суммы используют разные биты маски, но один и тот же широковещательный вход float
. Поскольку матричные элементы в 32 раза меньше, чем векторные элементы, это неплохо.
Один метод с плавающей запятой, загружаемый за 4x или 8x vaddps
инструкций, является очень хорошей вычислительной интенсивностью. Особенно без AVX512, где битовая карта -> векторная маска будет стоить циклов.
Чтобы еще больше помочь с пропускной способностью кеша / памяти, блокировка кеша / циклическое разбиение для размера кэш-памяти L2 (256 кБ)может быть возможно помочь с повторным использованием входных векторных элементов. Но я не уверен, что вы можете эффективно блокировать как ввод, так и вывод. В отличие от продукта mat * mat, нужно выполнить только O (n ^ 2). Перечитывание ввода и просто запись одного выходного потока, вероятно, хорошо, но вы можете найти золотую середину, которая добавляет частичные результаты в частичные порции выходного вектора. Но тогда мы больше не читаем битовую матрицу в одном непрерывном потоке. Пока вы останавливаетесь на границах строк кэша, это, вероятно, нормально.
Если ваша матрица NxM
имеет M = 32
, то это точно соответствует размеру float
и _mm256_loadu_si256
получит вектор с битами маски для outvec[0]
в младшем бите каждого элемента. И биты маски для outvec[31]
в старшем бите. Вы можете использовать _mm256_blendv_ps
, чтобы применить их к вводу суммы, и сдвиг влево на 1, чтобы переместить следующий бит вверх в верхнюю позицию. (Альтернативой vblendvps
является psrad
на 31 + andps
: арифметический сдвиг вправо для передачи старшего бита на все позиции).
Но это может быть не лучше, чем в другом случае, даже для этого особого случая. Вы можете развернуть несколько выходных элементов в разных векторах, чтобы несколько раз использовать вектор с плавающей точкой.
С AVX512F вы можете просто использовать строки матрицы в качестве __mmask16
значений для маскированныхдобавьте как _mm512_mask_add_ps
.
sum = _mm512_mask_add_ps(sum, matrix[col*rowstride + row], sum, invec[i]);
, если matrix
является массивом uint16_t
.
Или с AVX512BW, kmovq
64 бита маски в регистр k
и kshift
вниз, чтобы сопоставить с развернутыми более 4-х векторных аккумуляторов. К сожалению, kmov k, [mem]
- это 2 мопа на Skylake-X: нагрузка + порт 5, а не просто моп нагрузки, который может записывать в регистры маски. Таким образом, одна загрузка 3x распаковки с kshift
- это чистый выигрыш против 4x kmovw k1, [mem]
/ kmovw k2, [mem+2]
и т. Д. Невозможно получить каждые 16 бит данных маски в нижней части k
регистра без мопа port5 для каждогоодин. Таким образом, он конкурирует с 512-битной пропускной способностью FMA / add / mul на ядрах SKX, которые имеют 2 блока FMA, в противном случае это просто пропускная способность внешнего интерфейса.