Как использовать плавное умножение и добавление в AVX для 16-битных упакованных целых чисел

Я знаю, что в AVX2 можно выполнить умножение и сложение с помощью одной инструкции. Я хочу использовать инструкцию умножения и добавления, в которой каждая 256-битная переменная AVX2 упакована 16 16-битными переменными. Например, рассмотрим пример ниже,

разрешение=a0*b0+a1*b1+a2*b2+a3*b3

здесь каждая из res, a0, a1, a2, a3, b0, b1, b2, b3 — 16-битные переменные. Я внимательно следил за обсуждением . Пожалуйста, найдите мой код ниже, чтобы рассчитать пример, показанный выше,

#include<stdio.h>
#include<stdint.h>
#include <immintrin.h>
#include<time.h>
#include "cpucycles.c"

#pragma STDC FP_CONTRACT ON

#define AVX_LEN 16

inline __m256i mul_add(__m256i a, __m256i b, __m256i c) { 
    return _mm256_add_epi16(_mm256_mullo_epi16(a, b), c);
}

void fill_random(int16_t *a, int32_t len){  //to fill up the random array

    int32_t i;

    for(i=0;i<len;i++){     
        a[i]=(int16_t)rand()&0xffff;
    }
}

void main(){


    int16_t a0[16*AVX_LEN], b0[16*AVX_LEN];
    int16_t a1[16*AVX_LEN], b1[16*AVX_LEN];
    int16_t a2[16*AVX_LEN], b2[16*AVX_LEN];
    int16_t a3[16*AVX_LEN], b3[16*AVX_LEN];
    int16_t res[16*AVX_LEN];


    __m256i a0_avx[AVX_LEN], b0_avx[AVX_LEN];
    __m256i a1_avx[AVX_LEN], b1_avx[AVX_LEN];
    __m256i a2_avx[AVX_LEN], b2_avx[AVX_LEN];
    __m256i a3_avx[AVX_LEN], b3_avx[AVX_LEN];

    __m256i res_avx[AVX_LEN];

    int16_t res_avx_check[16*AVX_LEN];
    int32_t i,j;

    uint64_t mask_ar[4]; //for unloading AVX variables
    mask_ar[0]=~(0UL);mask_ar[1]=~(0UL);mask_ar[2]=~(0UL);mask_ar[3]=~(0UL);
    __m256i mask;
    mask = _mm256_loadu_si256 ((__m256i const *)mask_ar);

    time_t t;
    srand((unsigned) time(&t));


    int32_t repeat=100000;

    uint64_t clock1, clock2, fma_clock;

    clock1=clock2=fma_clock=0;

    for(j=0;j<repeat;j++){
        printf("j : %d\n",j);

        fill_random(a0,16*AVX_LEN);// Genrate random data
        fill_random(a1,16*AVX_LEN);
        fill_random(a2,16*AVX_LEN);
        fill_random(a3,16*AVX_LEN);

        fill_random(b0,16*AVX_LEN);
        fill_random(b1,16*AVX_LEN);
        fill_random(b2,16*AVX_LEN);
        fill_random(b3,16*AVX_LEN);


        for(i=0;i<AVX_LEN;i++){ //Load values in AVX variables

            a0_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&a0[i*16]));
            a1_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&a1[i*16]));
            a2_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&a2[i*16]));
            a3_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&a3[i*16]));

            b0_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&b0[i*16]));
            b1_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&b1[i*16]));
            b2_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&b2[i*16]));
            b3_avx[i] = _mm256_loadu_si256 ((__m256i const *) (&b3[i*16]));
        }

        for(i=0;i<AVX_LEN;i++){
            res_avx[i]= _mm256_set_epi64x(0, 0, 0, 0);
        }

        //to calculate a0*b0 + a1*b1 + a2*b2 + a3*b3

        //----standard calculation----
        for(i=0;i<16*AVX_LEN;i++){
            res[i]=a0[i]*b0[i] + a1[i]*b1[i] + a2[i]*b2[i] + a3[i]*b3[i];
        }


        //-----AVX-----

        clock1=cpucycles();


        for(i=0;i<AVX_LEN;i++){ //simple approach

            a0_avx[i]=_mm256_mullo_epi16(a0_avx[i], b0_avx[i]);
            res_avx[i]=_mm256_add_epi16(a0_avx[i], res_avx[i]);

            a1_avx[i]=_mm256_mullo_epi16(a1_avx[i], b1_avx[i]);
            res_avx[i]=_mm256_add_epi16(a1_avx[i], res_avx[i]);

            a2_avx[i]=_mm256_mullo_epi16(a2_avx[i], b2_avx[i]);
            res_avx[i]=_mm256_add_epi16(a2_avx[i], res_avx[i]);

            a3_avx[i]=_mm256_mullo_epi16(a3_avx[i], b3_avx[i]);
            res_avx[i]=_mm256_add_epi16(a3_avx[i], res_avx[i]);

        }

        /*
        for(i=0;i<AVX_LEN;i++){ //FMA approach

            res_avx[i]=mul_add(a0_avx[i], b0_avx[i], res_avx[i]);

            res_avx[i]=mul_add(a1_avx[i], b1_avx[i], res_avx[i]);
            res_avx[i]=mul_add(a2_avx[i], b2_avx[i], res_avx[i]);

            res_avx[i]=mul_add(a3_avx[i], b3_avx[i], res_avx[i]);

        }
        */

        clock2=cpucycles();
        fma_clock = fma_clock + (clock2-clock1);

        //-----Check----

        for(i=0;i<AVX_LEN;i++){ //store avx results for comparison
            _mm256_maskstore_epi64 (res_avx_check + i*16, mask, res_avx[i]);
        }

        for(i=0;i<16*AVX_LEN;i++){
            if(res[i]!=res_avx_check[i]){

                printf("\n--ERROR--\n");
                return;
            }   

        }
    }


    printf("Total time taken is :%llu\n", fma_clock/repeat);

}

Код cpucycles взят из ECRYPT и приведен ниже.

#include "cpucycles.h"

long long cpucycles(void)
{
  unsigned long long result;
  asm volatile(".byte 15;.byte 49;shlq $32,%%rdx;orq %%rdx,%%rax"
    : "=a" (result) ::  "%rdx");
  return result;
}

Моя версия gcc возвращается,

gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36)

Я использую

 Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz

Когда я запускаю это на своем компьютере, я получаю следующие циклы для подхода fma и простого подхода соответственно.

FMA approach : Total time taken is :109
Simple approach : Total time taken is :141

Как видите, подход FMA немного быстрее, но я ожидал, что он будет еще быстрее. Я понимаю, что в моем примере кода есть много обращений к памяти, которые могут быть причиной ухудшения производительности. Но,

  1. Когда я выгружаю сборку, я вижу почти одинаковые инструкции для обоих подходов. Я не вижу никаких инструкций по fma в версии FMA. Я не понимаю причины. Это из-за инструкций _mm256_mullo_epi16?

  2. Верен ли мой подход?

  3. Не могли бы вы помочь мне исправить это?

Я новичок в программировании AVX2, поэтому вполне возможно, что я сделал что-то не очень стандартное, но я буду рад ответить на что-то непонятное. Я благодарю вас всех за вашу помощь заранее.


person Rick    schedule 31.07.2019    source источник
comment
x86 не имеет целочисленного FMA/MAC (умножение-накопление), кроме горизонтального pmaddubsw/pmaddwd. Опции FP-контракта не имеют ничего общего с целочисленным материалом; целочисленная математика всегда точна.   -  person Peter Cordes    schedule 31.07.2019
comment
@PeterCordes Я не понимаю. Я говорю о векторных инструкциях.   -  person Rick    schedule 31.07.2019
comment
[v]pmaddubsw и [v]pmaddwd — инструкции SIMD SSE/AVX2. Я имел в виду SIMD-целое, конечно, а не скалярное в регистрах GP.   -  person Peter Cordes    schedule 31.07.2019
comment
@PeterCordes Спасибо. Теперь я понимаю. Я не понимаю, однако, что когда я использую подход FMA, почему тактовые циклы уменьшаются   -  person Rick    schedule 31.07.2019
comment
Какие параметры компилятора вы использовали? Вы забыли включить оптимизацию? gcc -O3 -march=native. На самом деле ваш компилятор слишком стар, чтобы знать -march=skylake, поэтому -march=native не будет правильно устанавливать -mtune настройки. Но в любом случае после оптимизации разницы быть не должно.   -  person Peter Cordes    schedule 31.07.2019
comment
@PeterCordes Я использовал -mavx2 -O3 -mfma. После ваших комментариев я также добавил -march=native в gcc-6.5. И как вы сказали, это ничего не меняет.   -  person Rick    schedule 01.08.2019
comment
Ах да, компилятор знает, что ваши массивы выровнены, поэтому стандартная настройка разделения невыровненных 256-битных загрузок/хранилищ здесь не будет проблемой.   -  person Peter Cordes    schedule 01.08.2019


Ответы (1)


x86 не имеет SIMD-целого FMA / MAC (умножение-накопление), кроме горизонтального pmaddubsw / pmaddwd, которые добавляют горизонтальное в более широкие целые числа. (До AVX512IFMA _mm_madd52lo_epu64 или AVX512_4VNNIW _mm512_4dpwssd_epi32(__m512i, __m512ix4, __m128i *)).

FP-контракт и опции -ffast-math не имеют ничего общего с SIMD-целочисленным материалом; целочисленная математика всегда точна.


Я думаю, что ваш «простой» подход медленнее, потому что вы также изменяете входные массивы, и это не оптимизируется, например.

a0_avx[i] = _mm256_mullo_epi16(a0_avx[i], b0_avx[i]);

а также обновление res_avx[i].

Если компилятор не оптимизирует это, эти дополнительные хранилища могут быть именно тем, почему он медленнее, чем ваша функция mul_add. rdtsc без инструкции сериализации даже не нужно ждать выполнения более ранних инструкций, не говоря уже об удалении или фиксации хранилищ в кеше L1d, но дополнительные операции для внешнего интерфейса все еще требуют изучения. При пропускной способности всего 1 магазин за такт это легко может стать новым узким местом.


К вашему сведению, вам не нужно копировать входные данные в массивы __m256i. Обычно вы просто используете загрузку SIMD для обычных данных. Это не медленнее, чем индексирование массивов __m256i. Ваши массивы слишком велики для того, чтобы компилятор мог полностью развернуться и сохранить все в регистрах (как это было бы для скалярных переменных __m256i).

Если бы вы просто использовали __m256i a0 = _mm256_loadu_si256(...) внутри цикла, вы могли бы обновить a0, не замедляя свой код, потому что это была бы просто одна локальная переменная, которую можно хранить в регистре YMM.

Но я считаю хорошим стилем использовать новые именованные переменные tmp для большинства шагов, чтобы сделать код более самодокументируемым. Например, __m256i ab = ... или sum = .... Вы можете повторно использовать один и тот же временный sum для каждого a0+b0 и a1+b1.

Вы также можете использовать временный вектор для результирующего вектора вместо того, чтобы заставлять компилятор оптимизировать обновления памяти в res_avx[i] до последнего.

Вы можете использовать alignas(32) int16_t a0[...];, чтобы сделать простые массивы выровненными для _mm256_load вместо loadu.


В вашей функции cpucycles() RDTSC нет необходимости использовать встроенный ассемблер. Вместо этого используйте __rdtsc().

person Peter Cordes    schedule 01.08.2019
comment
спасибо за иллюстрированный ответ. Вы написали, что также изменяете входные массивы, и это не оптимизируется. Не могли бы вы сказать мне, как лучше всего обновить такой массив AVX2. Я ценю вашу помощь. - person Rick; 03.08.2019
comment
@Rick: Обычно не используйте массивы __m256d в первую очередь, просто используйте несколько локальных переменных __m256d, чтобы их можно было хранить в регистрах. И когда у вас есть массивы (независимо от того, содержат ли они double или __m256d), используйте tmp var вместо их изменения, если вы действительно не хотите этого побочного эффекта. - person Peter Cordes; 03.08.2019