Использование встроенных функций sse и avx для добавления набора упакованных одиночных чисел в одно значение

У меня есть код, который я пытаюсь ускорить. Во-первых, я использовал встроенные функции SSE и увидел значительный выигрыш. Теперь я пытаюсь понять, могу ли я сделать то же самое с внутренними функциями AVX. Код, по сути, берет два массива, складывает или вычитает их по мере необходимости, возводит результат в квадрат и затем суммирует все эти квадраты.

Ниже приведена несколько упрощенная версия кода с использованием встроенных функций sse:

float chiList[4] __attribute__((aligned(16)));
float chi = 0.0;
__m128 res;
__m128 nres;
__m128 del;
__m128 chiInter2;
__m128 chiInter;
while(runNum<boundary)
{
    chiInter = _mm_setzero_ps();
    for(int i=0; i<maxPts; i+=4)
    {
        //load the first batch of residuals and deltas
        res = _mm_load_ps(resids+i);
        del = _mm_load_ps(residDeltas[param]+i);
        //subtract them
        nres = _mm_sub_ps(res,del);
        //load them back into memory
        _mm_store_ps(resids+i,nres);
        //square them and add them back to chi with the fused
        //multiply and add instructions
        chiInter = _mm_fmadd_ps(nres, nres, chiInter);
    }
    //add the 4 intermediate this way because testing 
    //shows it is faster than the commented out way below
    //so chiInter2 has chiInter reversed
    chiInter2 = _mm_shuffle_ps(chiInter,chiInter,_MM_SHUFFLE(0,1,2,3));
    //add the two
    _mm_store_ps(chiList,_mm_add_ps(chiInter,chiInter2));
    //add again
    chi=chiList[0]+chiList[1];
    //now do stuff with the chi^2
    //alternatively, the slow way
    //_mm_store_ps(chiList,chiInter);
    //chi=chiList[0]+chiList[1]+chiList[2]+chiList[3];
}

Это подводит меня к моему первому вопросу: Есть ли способ сделать последнюю часть (где я беру 4 числа с плавающей запятой в chiInter и суммирую их в одно число с плавающей запятой) более элегантно?

В любом случае, теперь я пытаюсь реализовать это с помощью встроенных функций avx, большая часть этого процесса довольно проста, к сожалению, я задерживаюсь, пытаясь сделать последний бит, пытаясь сжать 8 промежуточных значений chi в одно значение.

Ниже приведен аналогично упрощенный фрагмент кода для встроенных функций avx:

float chiList[8] __attribute__((aligned(32)));
__m256 res;
__m256 del;
__m256 nres;
__m256 chiInter;
while(runNum<boundary)
{
    chiInter = _mm256_setzero_ps();
    for(int i=0; i<maxPts; i+=8)
    {
        //load the first batch of residuals and deltas
        res = _mm256_load_ps(resids+i);
        del = _mm256_load_ps(residDeltas[param]+i);
        //subtract them
        nres = _mm256_sub_ps(res,del);
        //load them back into memory
        _mm256_store_ps(resids+i,nres);
        //square them and add them back to chi with the fused
        //multiply and add instructions
        chiInter = _mm256_fmadd_ps(nres, nres, chiInter);
    }
    _mm256_store_ps(chiList,chiInter);
    chi=chiList[0]+chiList[1]+chiList[2]+chiList[3]+
        chiList[4]+chiList[5]+chiList[6]+chiList[7];
}

Мой второй вопрос: Есть ли какой-нибудь метод, подобный тому, который я использовал с SSE выше, который позволит мне выполнить это последнее добавление быстрее? или, если есть лучший способ сделать то, что я сделал во встроенных функциях SSE, есть ли у него эквивалент для встроенных функций AVX?


person James Matta    schedule 28.03.2014    source источник
comment
Не беспокойтесь слишком об эффективности окончательной суммы - если maxPts достаточно велико, то общее время будет зависеть от того, что происходит внутри цикла for, и любой код преамбулы / постамбулы не будет иметь значения с точки зрения производительности.   -  person Paul R    schedule 29.03.2014
comment
@PaulR, К сожалению, maxPts невелик, обычно не больше 32. И да, несмотря на крошечный размер, я вижу огромные выгоды при использовании sse по сравнению с наивным циклом, т.е. 144 нс / итерация - ›14 нс / итерация.   -  person James Matta    schedule 29.03.2014
comment
См. Соответствующее: stackoverflow.com/q/9775538/1918193. Я удивлен, что вы не пробовали использовать hasdps. Ключевые слова для поиска: горизонтальное сложение / сумма.   -  person Marc Glisse    schedule 29.03.2014
comment
@MarcGlisse, я не знал, потому что не знал, на что смотрю, когда проходил мимо. Большое спасибо за информацию. Если вы напишете это как ответ, я с радостью приму это.   -  person James Matta    schedule 29.03.2014
comment
Если maxPts большой, вы можете проверить показатель степени вашего окончательного накопленного квадрата ошибок в chi, числа с плавающей запятой (с плавающей запятой одинарной точности) имеют только 24 бита значимости en.wikipedia.org/wiki/Single-precision_floating-point_format по мере роста накопленной ошибки вы потеряете точность, и она может дойти до точки, в которой вы прекратите накапливать дальше (промежуточный квадрат дельты остатков может быть в 2 ^ 24 раза меньше, чем ваш текущий накопленный chi, и когда ЦП нормализуется для сложения, они обращаются к нулю.   -  person amdn    schedule 29.03.2014
comment
@amdn, Как я сказал PaulR, maxPts довольно маленький, почти никогда не больше 32. Кроме того, я использую этот код для поиска в пространстве параметров из нескольких триллионов точек, чтобы найти минимумы, потеря точности меня не волнует. числа будут иметь больший показатель степени, что позволит исключить их при выводе на диск.   -  person James Matta    schedule 29.03.2014
comment
@JamesMatta хорошо звучит   -  person amdn    schedule 29.03.2014


Ответы (1)


Эта операция называется горизонтальной суммой. Допустим, у вас есть вектор v={x0,x1,x2,x3,x4,x5,x6,x7}. Сначала извлеките высокие / низкие части, чтобы у вас было w1={x0,x1,x2,x3} и w2={x4,x5,x6,x7}. Теперь вызовите _mm_hadd_ps(w1, w2), который даст: tmp1={x0+x1,x2+x3,x4+x5,x6+x7}. И снова _mm_hadd_ps(tmp1,tmp1) дает tmp2={x0+x1+x2+x3,x4+x5+x6+x7,...}. В последний раз _mm_hadd_ps(tmp2,tmp2) дает tmp3={x0+x1+x2+x3+x4+x5+x6+x7,...}. Вы также можете заменить первый _mm_hadd_ps простым _mm_add_ps.

Это все непроверено и написано из документа. И по скорости никаких обещаний ...

Кто-то на форуме Intel показывает другой вариант (ищите HsumAvxFlt).

Мы также можем посмотреть, что предлагает gcc, скомпилировав этот код с gcc test.c -Ofast -mavx2 -S

float f(float*t){
  t=(float*)__builtin_assume_aligned(t,32);
  float r=0;
  for(int i=0;i<8;i++)
    r+=t[i];
  return r;
}

Сгенерированный test.s содержит:

vhaddps %ymm0, %ymm0, %ymm0
vhaddps %ymm0, %ymm0, %ymm1
vperm2f128  $1, %ymm1, %ymm1, %ymm0
vaddps  %ymm1, %ymm0, %ymm0

Я немного удивлен, что последняя инструкция не vaddss, но думаю, это не имеет большого значения.

person Marc Glisse    schedule 28.03.2014
comment
Вау, это очень помогает. Большое тебе спасибо. Я писал свои улучшения через поиск и тестирование и иногда натыкался на вещи. Встроенные функции спасли меня, поскольку я действительно не хотел писать встроенную сборку, но я ничего о них не знал до недели назад. - person James Matta; 29.03.2014