Определите минимум для SIMD-дорожек значения __m256

Я понимаю, что обычно следует избегать операций по линиям SIMD. Однако иногда это необходимо.

Я использую встроенные функции AVX2 и имею 8 значений с плавающей запятой в __m256.

Я хочу узнать наименьшее значение в этом векторе и усложнить ситуацию: также, в каком слоте это было.

Мое текущее решение совершает круговой обход памяти, что мне не нравится:

float closestvals[8];
_mm256_store_ps( closestvals, closest8 );

float closest  = closestvals[0];
int closestidx = 0;
for ( int k=1; k<8; ++k )
{
    if ( closestvals[k] < closest )
    {
        closest = closestvals[ k ];
        closestidx = k;
    }
}

Что было бы хорошим способом сделать это без перехода в / из памяти?


person Bram    schedule 07.04.2017    source источник
comment
См. этот вопрос для получения информации о методах, которые работают с SSE и 32-битными целыми числами - вы должны иметь возможность повторно использовать некоторые методы в отвечает, хотя разделенная природа AVX делает его немного сложнее, чем SSE.   -  person Paul R    schedule 07.04.2017
comment
@PaulR: Для упакованных синглов я бы предпочел перестановки вместо palignr-s, которые вы используете для 32-битных целых чисел, но идея, конечно же, остается той же. Смотрите мой ответ ниже.   -  person wim    schedule 07.04.2017
comment
@wim: да, хороший момент - я заметил, что clang даже изменяет некоторые palignr инструкции на перестановку / перемешивание на лету (что может немного запутать отладку / профилирование!).   -  person Paul R    schedule 07.04.2017
comment
Это AVX, а не AVX2   -  person Martin    schedule 08.04.2017


Ответы (1)


Вы можете попробовать это:

#include <stdio.h>
#include <x86intrin.h>
#include <math.h>
/*  gcc -O3 -Wall -m64 -march=haswell hor_min.c   */
int print_vec_ps(__m256 x);

int main() {
    float x[8]={1.2f, 3.6f, 2.1f, 9.4f,   4.0f, 0.1f, 8.9f, 3.3f};

    /* Note that the results are not useful if one of the inputs is a 'not a number'. The input below leads to indx = 32 (!)     */
//    float x[8]={1.2f, 3.6f, 2.1f, NAN,  4.0f, 2.0f , 8.9f, 3.3f};

    __m256 v0    = _mm256_load_ps(x);                /* _mm256_shuffle_ps instead of _mm256_permute_ps is also possible, see Peter Cordes' comments */
    __m256 v1    = _mm256_permute_ps(v0,0b10110001); /* swap floats: 0<->1, 2<->3, 4<->5, 6<->7                         */    
    __m256 v2    = _mm256_min_ps(v0,v1);
    __m256 v3    = _mm256_permute_ps(v2,0b01001110); /* swap floats                                                     */    
    __m256 v4    = _mm256_min_ps(v2,v3);
    __m256 v5    = _mm256_castpd_ps(_mm256_permute4x64_pd(_mm256_castps_pd(v4),0b01001110)); /* swap 128-bit lanes      */
    __m256 v_min = _mm256_min_ps(v4,v5);
    __m256 mask  = _mm256_cmp_ps(v0,v_min,0);
    int    indx  = _tzcnt_u32(_mm256_movemask_ps(mask));


   printf("             7      6      5      4      3      2      1      0\n");
   printf("v0     = ");print_vec_ps(v0    );
   printf("v1     = ");print_vec_ps(v1    );
   printf("v2     = ");print_vec_ps(v2    );
   printf("\nv3     = ");print_vec_ps(v3    );
   printf("v4     = ");print_vec_ps(v4    );
   printf("\nv5     = ");print_vec_ps(v5    );
   printf("v_min  = ");print_vec_ps(v_min );
   printf("mask   = ");print_vec_ps(mask  );
   printf("indx   = ");printf("%d\n",indx);

   return 0;
}


int print_vec_ps(__m256 x){
   float v[8];
   _mm256_storeu_ps(v,x);
   printf("%5.2f  %5.2f  %5.2f  %5.2f  %5.2f  %5.2f  %5.2f  %5.2f\n",
          v[7],v[6],v[5],v[4],v[3],v[2],v[1],v[0]);
   return 0;
}

Выход:

./a.out
             7      6      5      4      3      2      1      0
v0     =  3.30   8.90   0.10   4.00   9.40   2.10   3.60   1.20
v1     =  8.90   3.30   4.00   0.10   2.10   9.40   1.20   3.60
v2     =  3.30   3.30   0.10   0.10   2.10   2.10   1.20   1.20

v3     =  0.10   0.10   3.30   3.30   1.20   1.20   2.10   2.10
v4     =  0.10   0.10   0.10   0.10   1.20   1.20   1.20   1.20

v5     =  1.20   1.20   1.20   1.20   0.10   0.10   0.10   0.10
v_min  =  0.10   0.10   0.10   0.10   0.10   0.10   0.10   0.10
mask   =  0.00   0.00   -nan   0.00   0.00   0.00   0.00   0.00
indx   = 5

В предыдущей версии этого ответа 128-битные полосы были заменены на _mm256_permute2f128_ps. В этом обновленном ответе _mm256_permute2f128_ps заменено на _mm256_permute4x64_pd, что быстрее на процессорах AMD и на Intel KNL, см. Комментарии @Peter Cordes. Но обратите внимание, что _mm256_permute4x64_pd требует AVX2, а AVX достаточно для _mm256_permute2f128_ps.

Также обратите внимание, что результаты этого кода бесполезны, если одно из входных значений - «не число» (NAN).

person wim    schedule 07.04.2017
comment
Хорошее решение - я предполагал 10 или более инструкций для такого решения, но вы сделали это менее чем за 10. - person Paul R; 07.04.2017
comment
@PaulR: Хорошо, это 9 инструкций. Но, по крайней мере, это должно быть намного быстрее, чем цикл for :-). - person wim; 07.04.2017
comment
Спасибо, отличный материал. Я жал его, и он быстрее. (не уверен, насколько сильно, поскольку я использовал более крупную область.) Самая медленная часть - это tzcnt, который компилятор переводит в условный переход и bsf. - person Bram; 07.04.2017
comment
@Bram: Вы использовали правильные флаги компилятора? Только с gcc -O3 -mavx2 инструкция tzcnt не активна. С gcc -O3 -march=haswell набор инструкций BMI (и tzcnt) включен. Процессоры Intel с поддержкой AVX2, такие как Intel Haswell, Broadwell или Skylake, также имеют поддержку BMI, см. Также wikipedia < / а>. - person wim; 07.04.2017
comment
@Bram: Во всяком случае, приятно слышать, что это работает! Какие параметры процессора, компилятора и компилятора вы используете для вычислений? - person wim; 07.04.2017
comment
@Wim Clang 3.8.0-2ubuntu4 и -march = haswell сделали свое дело с моим i5-4570. На данный момент использую -O2. - person Bram; 07.04.2017
comment
Для процессоров AMD и KNL избегайте использования vperm2f128 с одним и тем же исходным вектором дважды. vpermpd всегда может заменить его на равную или лучшую производительность на каждом ЦП, при этом по-прежнему используется элемент управления imm8 . (например, вдвое больше пропускной способности и на 1/4 меньше числа операций на Ryzen.) __m256d _mm256_permute4x64_pd(__m256d a, int control) ;. Связано: vshufps может быть на 1 байт меньше размера кода, чем vpermilps, но медленнее на KNL. - person Peter Cordes; 13.07.2017
comment
Кроме того, если вы сначала выполните vpermilps инструкции, в него может сложиться груз. (На самом деле vpermpd имеет такое же преимущество). Кроме того, выполнение перетасовки линий с меньшей задержкой вначале может иметь небольшое преимущество для выполнения вне очереди, позволяя большему количеству инсайнов выйти из строя раньше. - person Peter Cordes; 13.07.2017
comment
@PeterCordes Хорошая идея использовать vpermpd вместо vperm2f128. Я только что обновил ответ. - person wim; 04.08.2017
comment
Кстати, vpermpd предназначен только для AVX2. vperm2f128 - это AVX1. Наверное, неплохо было бы добавить примечание об этом. Это отстой для семейства AMD Bulldozer (до Excavator), потому что они поддерживают только AVX1. Я понял это только после своего предыдущего комментария. - person Peter Cordes; 04.08.2017