Воспроизвести _mm256_sllv_epi16 и _mm256_sllv_epi8 в avx2

Я был удивлен, увидев, что _mm256_sllv_epi16/8(__m256i v1, __m256i v2) и _mm256_srlv_epi16/8(__m256i v1, __m256i v2) не были в Руководство Intel по внутренним функциям, и я не нашел никакого решения для воссоздания этого внутреннего AVX512 только с AVX2.

Эта функция сдвигает влево все 16/8-битные упакованные int на значение счетчика соответствующих элементов данных в v2.

Пример для epi16:

__m256i v1 = _mm256_set1_epi16(0b1111111111111111);
__m256i v2 = _mm256_setr_epi16(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
v1 = _mm256_sllv_epi16(v1, v2);

Тогда v1 равно -> (1111111111111111, 1111111111111110, 1111111111111100, г. 1111111111111000, г. ................, 1000000000000000);

@ 1201ProgramAlarm: true, но OP хочет имитировать их с помощью AVX2, поэтому их код может работать на Haswell / Ryzen, а не только на AVX512BW (SKX). И ни у одного процессора нет _mm256_sllv_epi8 / vpsllvb, потому что его нет, даже в AVX512VBMI2. Я удалил тег avx512, потому что это не вопрос avx512.

Peter Cordes 11.08.2018 02:37
3
1
670
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Странно, что они это упустили, хотя кажется, что многие целочисленные инструкции AVX доступны только для 32/64-битной ширины. В AVX512BW было добавлено как минимум 16-битное (хотя я до сих пор не понимаю, почему Intel отказывается добавлять 8-битные сдвиги).

Мы можем эмулировать 16-битные сдвиги переменных, используя только AVX2, используя 32-битные сдвиги переменных с некоторым маскированием и смешиванием.

Нам нужен счетчик сдвига вправо в нижней части 32-битного элемента, содержащего каждый 16-битный элемент, что мы можем сделать с помощью И (для нижнего элемента) и немедленного сдвига для верхней половины. (В отличие от скалярных сдвигов, векторные сдвиги x86 насыщают свое количество вместо обертывания / маскирования).

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

__m256i _mm256_sllv_epi16(__m256i a, __m256i count) {
    const __m256i mask = _mm256_set1_epi32(0xffff0000);
    __m256i low_half = _mm256_sllv_epi32(
        a,
        _mm256_andnot_si256(mask, count)
    );
    __m256i high_half = _mm256_sllv_epi32(
        _mm256_and_si256(mask, a),
        _mm256_srli_epi32(count, 16)
    );
    return _mm256_blend_epi16(low_half, high_half, 0xaa);
}
__m256i _mm256_sllv_epi16(__m256i a, __m256i count) {
    const __m256i mask = _mm256_set1_epi32(0xffff0000); // alternating low/high words of a dword
    // shift low word of each dword: low_half = (a << (count & 0xffff)) [for each 32b element]
    // note that, because `a` isn't being masked here, we may get some "junk" bits, but these will get eliminated by the blend below
    __m256i low_half = _mm256_sllv_epi32(
        a,
        _mm256_andnot_si256(mask, count)
    );
    // shift high word of each dword: high_half = ((a & 0xffff0000) << (count >> 16)) [for each 32b element]
    __m256i high_half = _mm256_sllv_epi32(
        _mm256_and_si256(mask, a),     // make sure we shift in zeros
        _mm256_srli_epi32(count, 16)   // need the high-16 count at the bottom of a 32-bit element
    );
    // combine low and high words
    return _mm256_blend_epi16(low_half, high_half, 0xaa);
}

__m256i _mm256_srlv_epi16(__m256i a, __m256i count) {
    const __m256i mask = _mm256_set1_epi32(0x0000ffff);
    __m256i low_half = _mm256_srlv_epi32(
        _mm256_and_si256(mask, a),
        _mm256_and_si256(mask, count)
    );
    __m256i high_half = _mm256_srlv_epi32(
        a,
        _mm256_srli_epi32(count, 16)
    );
    return _mm256_blend_epi16(low_half, high_half, 0xaa);
}

GCC 8.2 компилирует это примерно так, как вы ожидаете:

_mm256_srlv_epi16(long long __vector(4), long long __vector(4)):
        vmovdqa       ymm3, YMMWORD PTR .LC0[rip]
        vpand   ymm2, ymm0, ymm3
        vpand   ymm3, ymm1, ymm3
        vpsrld  ymm1, ymm1, 16
        vpsrlvd ymm2, ymm2, ymm3
        vpsrlvd ymm0, ymm0, ymm1
        vpblendw        ymm0, ymm2, ymm0, 170
        ret
_mm256_sllv_epi16(long long __vector(4), long long __vector(4)):
        vmovdqa       ymm3, YMMWORD PTR .LC1[rip]
        vpandn  ymm2, ymm3, ymm1
        vpsrld  ymm1, ymm1, 16
        vpsllvd ymm2, ymm0, ymm2
        vpand   ymm0, ymm0, ymm3
        vpsllvd ymm0, ymm0, ymm1
        vpblendw        ymm0, ymm2, ymm0, 170
        ret

Это означает, что эмуляция приводит к 1x загрузке + 2x AND / ANDN + 2x переменному сдвигу + 1x правому сдвигу + 1x смешиванию.

Clang 6.0 делает кое-что интересное - устраняет нагрузку на память (и соответствующее маскирование) с помощью блендов:

_mm256_sllv_epi16(long long __vector(4), long long __vector(4)):
        vpxor   xmm2, xmm2, xmm2
        vpblendw        ymm3, ymm1, ymm2, 170
        vpsllvd ymm3, ymm0, ymm3
        vpsrld  ymm1, ymm1, 16
        vpblendw        ymm0, ymm2, ymm0, 170
        vpsllvd ymm0, ymm0, ymm1
        vpblendw        ymm0, ymm3, ymm0, 170
        ret
_mm256_srlv_epi16(long long __vector(4), long long __vector(4)):
        vpxor   xmm2, xmm2, xmm2
        vpblendw        ymm3, ymm0, ymm2, 170
        vpblendw        ymm2, ymm1, ymm2, 170
        vpsrlvd ymm2, ymm3, ymm2
        vpsrld  ymm1, ymm1, 16
        vpsrlvd ymm0, ymm0, ymm1
        vpblendw        ymm0, ymm2, ymm0, 170
        ret

В результате получается: 1x очистка + 3x смешение + 2x переменный сдвиг + 1x сдвиг вправо.

Я не тестировал, какой из подходов быстрее, но подозреваю, что это может зависеть от ЦП, в частности, от стоимости PBLENDW на ЦП.

Конечно, если ваш вариант использования немного более ограничен, приведенное выше можно упростить, например если все ваши значения сдвига являются константами, вы можете удалить маскировку / сдвиг, необходимую для того, чтобы это работало (при условии, что компилятор не делает это автоматически за вас). Для сдвига влево, если величины сдвига постоянны, вы можете вместо этого использовать _mm256_mullo_epi16, преобразовывая суммы сдвига во что-то, что можно умножить, например для примера, который вы дали:

__m256i v1 = _mm256_set1_epi16(0b1111111111111111);
__m256i v2 = _mm256_setr_epi16(1<<0,1<<1,1<<2,1<<3,1<<4,1<<5,1<<6,1<<7,1<<8,1<<9,1<<10,1<<11,1<<12,1<<13,1<<14,1<<15);
v1 = _mm256_mullo_epi16(v1, v2);

Обновление: Питер упоминает (см. Комментарий ниже), что сдвиг вправо также может быть реализован с помощью _mm256_mulhi_epi16 (например, для выполнения v>>1 умножьте v на 1<<15 и возьмите старшее слово).


Для 8-битных сдвигов переменных этого также нет в AVX512 (опять же, я не знаю, почему у Intel нет 8-битных сдвигов SIMD). Если доступен AVX512BW является, вы можете использовать трюк, аналогичный приведенному выше, используя _mm256_sllv_epi16. Для AVX2 я не могу придумать особенно лучшего подхода, чем применение эмуляции для 16-битного сигнала во второй раз, поскольку в конечном итоге вам придется сделать сдвиг в 4 раза больше того, что дает 32-битный сдвиг. См. Ответ @ wim для хорошего решения для 8-битной версии AVX2.

Вот что я придумал (в основном 16-битная версия, принятая для 8-битной на AVX512):

__m256i _mm256_sllv_epi8(__m256i a, __m256i count) {
    const __m256i mask = _mm256_set1_epi16(0xff00);
    __m256i low_half = _mm256_sllv_epi16(
        a,
        _mm256_andnot_si256(mask, count)
    );
    __m256i high_half = _mm256_sllv_epi16(
        _mm256_and_si256(mask, a),
        _mm256_srli_epi16(count, 8)
    );
    return _mm256_blendv_epi8(low_half, high_half, _mm256_set1_epi16(0xff00));
}

__m256i _mm256_srlv_epi8(__m256i a, __m256i count) {
    const __m256i mask = _mm256_set1_epi16(0x00ff);
    __m256i low_half = _mm256_srlv_epi16(
        _mm256_and_si256(mask, a),
        _mm256_and_si256(mask, count)
    );
    __m256i high_half = _mm256_srlv_epi16(
        a,
        _mm256_srli_epi16(count, 8)
    );
    return _mm256_blendv_epi8(low_half, high_half, _mm256_set1_epi16(0xff00));
}

(Питер Кордес упоминает ниже, что _mm256_blendv_epi8(low_half, high_half, _mm256_set1_epi16(0xff00)) можно заменить на _mm256_mask_blend_epi8(0xaaaaaaaa, low_half, high_half) в чистой реализации AVX512BW (+ VL), что, вероятно, быстрее)

vmovdqa64: вы скомпилировали с включенным AVX512. Это нормально, потому что не похоже, что вы использовали какие-либо встроенные функции, требующие AVX512. Если вы собираетесь показывать вывод asm, неплохо включить постоянную ссылку на код на godbolt.org, чтобы люди могли сами поиграть с ним. (Используйте полная ссылка, чтобы предотвратить любую ссылку гниль, а не короткую ссылку). например Как преобразовать 32-битное число с плавающей запятой в 8-битный знаковый символ?.
Peter Cordes 12.08.2018 07:17

Если вектор счетчика сдвигов используется много раз, вы можете предварительно вычислить 1<< count_vec и использовать vpmullw для умножения на соответствующую степень 2. Для сдвигов вправо вы можете сделать что-то подобное с vpmulhw.

Peter Cordes 12.08.2018 07:20

Если вы используете AVX512BW для 16-битного переменного сдвига, используйте vpblendmb для байтовых смесей (1 мкоп) с регистром маски с чередованием 0 и 1 бит. Он более эффективен, чем AVX2 vpblendvb (2 мупа). Смотрите мои комментарии к reviews.llvm.org/D50074. Надеюсь, что в какой-то момент LLVM оптимизирует _mm256_blendv_epi8 до vpblendmb во время компиляции, особенно с постоянной маской.

Peter Cordes 12.08.2018 07:41

Снова маскирование: вам нужна только одна маска: set1_epi32(0x000000ff), в которой вы используете сдвиг после вместо предыдущего. (Хм, вы можете получить больше ILP, имея другую маску, так что AND может работать параллельно с первым сдвигом вектора счета. Но не более двух векторов маски кажутся хорошей идеей.)

Peter Cordes 12.08.2018 07:44

re: стоимость PBLENDW: это задержка единичная / 1c на всех процессорах AVX2 (agner.org/optimize). Но процессоры Intel запускают его только на одном порту (p5), поэтому это может быть узким местом в пропускной способности (снова см. Мои комментарии к этому обзору LLVM). На самом деле на процессорах AMD это 2 мупа для 256-битной версии, потому что они, как обычно, разделяют 256-битные операции. А у семейства Bulldozer задержка 2с даже для самых дешевых векторных мопов. Но у AMD хорошая пропускная способность для pblendw.

Peter Cordes 12.08.2018 08:29

Я не уверен, почему GCC использовал vmovdqa64, моя ошибка, спасибо (хотя я думаю, что vmovdqa короче, чем vmovdqa64, поэтому выбор все еще странный). Хороший момент по поводу сдвига вправо - я не учел этого.

Nyan 12.08.2018 08:33

Да, использование EVEX для movdqa - это глупая упущенная оптимизация в gcc. Думаю, я сообщил об этом, но, возможно, упомянул об этом только как часть других отчетов об ошибках в пропущенной оптимизации. По крайней мере, он избегает использования vpxord и других версий EVEX других инструкций.

Peter Cordes 12.08.2018 08:34

Добавил примечание по поводу vpblendmb, на этом спасибо. Я хотел в основном придерживаться AVX2, так как это был исходный вопрос.

Nyan 12.08.2018 08:37

Хм, мне кажется, я не понимаю, о какой именно части кода вы говорите. Во второй реализации _mm256_sllv_epi8 порядок сдвига / и в переменной count является произвольным и может быть переупорядочен до того, как он попадет в _mm256_sllv_epi32. Что касается _mm256_sllv_epi16, я не понимаю, как можно устранить and, потому что a<<count != (a&0xffff0000)<<count != (a<<count)&0xffff0000. Может быть, если бы вы могли опубликовать какой-нибудь код, чтобы прояснить, что вы здесь имеете в виду?

Nyan 12.08.2018 08:52

О, я неправильно прочитал ваш код, я думал, что вы маскируете счетчик в обе стороны, и я забыл, что вам нужно замаскировать a. Вот откуда взялось «лишнее И», и оно необходимо. Я не думаю, что в ваших epi16 или epi8 есть какие-то потраченные впустую инструкции. Вы просто используете _mm256_srli_epi32(count, 16) без лишних операторов AND. Удалены мои предыдущие комментарии. Было бы неплохо добавить в исходный код несколько комментариев о том, какие биты куда перемещаются. Возможно, будет сложно следить за людьми, которые не смогли сами ответить на этот вопрос / не продумали все подводные камни.

Peter Cordes 12.08.2018 09:23

О, я вижу. Я не уверен, где именно добавлять комментарии, поскольку это кажется довольно простым для мне (при условии, что вы знаете, что делают встроенные функции), но тогда это является мой собственный код, так что это не удивительно. Возможно, если бы кто-то мог добавить комментарии к частям, которые им трудно понять, я мог бы включить их в ответ.

Nyan 12.08.2018 11:45

Я считаю хорошей идеей быть очень многословной в ответах SO (и неплохая идея в реальном SIMD-коде, хотя мои комментарии в реальном коде, как правило, также касаются компромиссов производительности). Я добавил текст и пару комментариев в ваш ответ.

Peter Cordes 12.08.2018 11:59

И, кстати, да, _mm256_shuffle_epi8, вероятно, хороший вариант для смены epi8. Перетасовка на линии - это здорово. Они действительно конкурируют за порт 5 с vpblendw на Intel, но, вероятно, это того стоит за два средних элемента. (Верхний и нижний байты могут и должны быть выполнены с помощью одного сдвига или одного оператора AND, особенно в Skylake, где векторные сдвиги имеют пропускную способность 2 за такт, по сравнению с 1 за такт в HSW / BDW. Ryzen также имеет два переключение портов, но перемешивание выполняется на тех же портах.)

Peter Cordes 12.08.2018 12:02
Ответ принят как подходящий

В случае _mm256_sllv_epi8 нетрудно заменить сдвиги умножением, используя инструкцию pshufb в качестве крошечной таблицы поиска. Также возможно эмулировать сдвиг вправо _mm256_srlv_epi8 с умножением и многими другими инструкциями, см. Код ниже. Я ожидал, что по крайней мере _mm256_sllv_epi8 более эффективен, чем решение Nyan.


Более или менее та же идея может быть использована для эмуляции _mm256_sllv_epi16, но в этом случае выбор правильного множителя менее тривиален (см. Также код ниже).

Приведенное ниже решение _mm256_sllv_epi16_emu не обязательно ни быстрее, ни лучше, чем решение Nyan. Производительность зависит от окружающего кода и от используемого процессора. Тем не менее, решение здесь может быть интересно, по крайней мере, на старых компьютерных системах. Например, инструкция vpsllvd используется дважды в решении Nyan. Эта инструкция работает быстро на системах Intel Skylake или новее. На Intel Broadwell или Haswell эта инструкция работает медленно, потому что она декодирует до 3 микроопераций. Решение здесь позволяет избежать этой медленной инструкции.

Можно пропустить две строки кода с mask_lt_15, если известно, что количество сдвигов меньше или равно 15.

Отсутствие внутреннего _mm256_srlv_epi16 оставлено читателю в качестве упражнения.


/*     gcc -O3 -m64 -Wall -mavx2 -march=broadwell shift_v_epi8.c     */
#include <immintrin.h>
#include <stdio.h>
int print_epi8(__m256i  a);
int print_epi16(__m256i  a);

__m256i _mm256_sllv_epi8(__m256i a, __m256i count) {
    __m256i mask_hi        = _mm256_set1_epi32(0xFF00FF00);
    __m256i multiplier_lut = _mm256_set_epi8(0,0,0,0, 0,0,0,0, 128,64,32,16, 8,4,2,1, 0,0,0,0, 0,0,0,0, 128,64,32,16, 8,4,2,1);

    __m256i count_sat      = _mm256_min_epu8(count, _mm256_set1_epi8(8));     /* AVX shift counts are not masked. So a_i << n_i = 0 for n_i >= 8. count_sat is always less than 9.*/ 
    __m256i multiplier     = _mm256_shuffle_epi8(multiplier_lut, count_sat);  /* Select the right multiplication factor in the lookup table.                                      */
    __m256i x_lo           = _mm256_mullo_epi16(a, multiplier);               /* Unfortunately _mm256_mullo_epi8 doesn't exist. Split the 16 bit elements in a high and low part. */

    __m256i multiplier_hi  = _mm256_srli_epi16(multiplier, 8);                /* The multiplier of the high bits.                                                                 */
    __m256i a_hi           = _mm256_and_si256(a, mask_hi);                    /* Mask off the low bits.                                                                           */
    __m256i x_hi           = _mm256_mullo_epi16(a_hi, multiplier_hi);
    __m256i x              = _mm256_blendv_epi8(x_lo, x_hi, mask_hi);         /* Merge the high and low part.                                                                     */
            return x;
}


__m256i _mm256_srlv_epi8(__m256i a, __m256i count) {
    __m256i mask_hi        = _mm256_set1_epi32(0xFF00FF00);
    __m256i multiplier_lut = _mm256_set_epi8(0,0,0,0, 0,0,0,0, 1,2,4,8, 16,32,64,128, 0,0,0,0, 0,0,0,0, 1,2,4,8, 16,32,64,128);

    __m256i count_sat      = _mm256_min_epu8(count, _mm256_set1_epi8(8));     /* AVX shift counts are not masked. So a_i >> n_i = 0 for n_i >= 8. count_sat is always less than 9.*/ 
    __m256i multiplier     = _mm256_shuffle_epi8(multiplier_lut, count_sat);  /* Select the right multiplication factor in the lookup table.                                      */
    __m256i a_lo           = _mm256_andnot_si256(mask_hi, a);                 /* Mask off the high bits.                                                                          */
    __m256i multiplier_lo  = _mm256_andnot_si256(mask_hi, multiplier);        /* The multiplier of the low bits.                                                                  */
    __m256i x_lo           = _mm256_mullo_epi16(a_lo, multiplier_lo);         /* Shift left a_lo by multiplying.                                                                  */
            x_lo           = _mm256_srli_epi16(x_lo, 7);                      /* Shift right by 7 to get the low bits at the right position.                                      */

    __m256i multiplier_hi  = _mm256_and_si256(mask_hi, multiplier);           /* The multiplier of the high bits.                                                                 */
    __m256i x_hi           = _mm256_mulhi_epu16(a, multiplier_hi);            /* Variable shift left a_hi by multiplying. Use a instead of a_hi because the a_lo bits don't interfere */
            x_hi           = _mm256_slli_epi16(x_hi, 1);                      /* Shift left by 1 to get the high bits at the right position.                                      */
    __m256i x              = _mm256_blendv_epi8(x_lo, x_hi, mask_hi);         /* Merge the high and low part.                                                                     */
            return x;
}


__m256i _mm256_sllv_epi16_emu(__m256i a, __m256i count) {
    __m256i multiplier_lut = _mm256_set_epi8(0,0,0,0, 0,0,0,0, 128,64,32,16, 8,4,2,1, 0,0,0,0, 0,0,0,0, 128,64,32,16, 8,4,2,1);
    __m256i byte_shuf_mask = _mm256_set_epi8(14,14,12,12, 10,10,8,8, 6,6,4,4, 2,2,0,0, 14,14,12,12, 10,10,8,8, 6,6,4,4, 2,2,0,0);

    __m256i mask_lt_15     = _mm256_cmpgt_epi16(_mm256_set1_epi16(16), count);
            a              = _mm256_and_si256(mask_lt_15, a);                    /* Set a to zero if count > 15.                                                                      */
            count          = _mm256_shuffle_epi8(count, byte_shuf_mask);         /* Duplicate bytes from the even postions to bytes at the even and odd positions.                    */
            count          = _mm256_sub_epi8(count,_mm256_set1_epi16(0x0800));   /* Subtract 8 at the even byte positions. Note that the vpshufb instruction selects a zero byte if the shuffle control mask is negative.     */
    __m256i multiplier     = _mm256_shuffle_epi8(multiplier_lut, count);         /* Select the right multiplication factor in the lookup table. Within the 16 bit elements, only the upper byte or the lower byte is nonzero. */
    __m256i x              = _mm256_mullo_epi16(a, multiplier);                  
            return x;
}


int main(){

    printf("Emulating _mm256_sllv_epi8:\n");
    __m256i a     = _mm256_set_epi8(32,31,30,29, 28,27,26,25, 24,23,22,21, 20,19,18,17, 16,15,14,13, 12,11,10,9, 8,7,6,5, 4,3,2,1);
    __m256i count = _mm256_set_epi8(7,6,5,4, 3,2,1,0,  11,10,9,8, 7,6,5,4, 3,2,1,0,  11,10,9,8, 7,6,5,4, 3,2,1,0);
    __m256i x     = _mm256_sllv_epi8(a, count);
    printf("a     = \n"); print_epi8(a    );
    printf("count = \n"); print_epi8(count);
    printf("x     = \n"); print_epi8(x    );
    printf("\n\n"); 


    printf("Emulating _mm256_srlv_epi8:\n");
            a     = _mm256_set_epi8(223,224,225,226, 227,228,229,230, 231,232,233,234, 235,236,237,238, 239,240,241,242, 243,244,245,246, 247,248,249,250, 251,252,253,254);
            count = _mm256_set_epi8(7,6,5,4, 3,2,1,0,  11,10,9,8, 7,6,5,4, 3,2,1,0,  11,10,9,8, 7,6,5,4, 3,2,1,0);
            x     = _mm256_srlv_epi8(a, count);
    printf("a     = \n"); print_epi8(a    );
    printf("count = \n"); print_epi8(count);
    printf("x     = \n"); print_epi8(x    );
    printf("\n\n"); 



    printf("Emulating _mm256_sllv_epi16:\n");
            a     = _mm256_set_epi16(1601,1501,1401,1301, 1200,1100,1000,900, 800,700,600,500, 400,300,200,100);
            count = _mm256_set_epi16(17,16,15,13,  11,10,9,8, 7,6,5,4, 3,2,1,0);
            x     = _mm256_sllv_epi16_emu(a, count);
    printf("a     = \n"); print_epi16(a    );
    printf("count = \n"); print_epi16(count);
    printf("x     = \n"); print_epi16(x    );
    printf("\n\n"); 

    return 0;
}


int print_epi8(__m256i  a){
  char v[32];
  int i;
  _mm256_storeu_si256((__m256i *)v,a);
  for (i = 0; i<32; i++) printf("%4hhu",v[i]);
  printf("\n");
  return 0;
}

int print_epi16(__m256i  a){
  unsigned short int  v[16];
  int i;
  _mm256_storeu_si256((__m256i *)v,a);
  for (i = 0; i<16; i++) printf("%6hu",v[i]);
  printf("\n");
  return 0;
}

Результат:

Emulating _mm256_sllv_epi8:
a     = 
   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32
count = 
   0   1   2   3   4   5   6   7   8   9  10  11   0   1   2   3   4   5   6   7   8   9  10  11   0   1   2   3   4   5   6   7
x     = 
   1   4  12  32  80 192 192   0   0   0   0   0  13  28  60 128  16  64 192   0   0   0   0   0  25  52 108 224 208 192 192   0


Emulating _mm256_srlv_epi8:
a     = 
 254 253 252 251 250 249 248 247 246 245 244 243 242 241 240 239 238 237 236 235 234 233 232 231 230 229 228 227 226 225 224 223
count = 
   0   1   2   3   4   5   6   7   8   9  10  11   0   1   2   3   4   5   6   7   8   9  10  11   0   1   2   3   4   5   6   7
x     = 
 254 126  63  31  15   7   3   1   0   0   0   0 242 120  60  29  14   7   3   1   0   0   0   0 230 114  57  28  14   7   3   1


Emulating _mm256_sllv_epi16:
a     = 
   100   200   300   400   500   600   700   800   900  1000  1100  1200  1301  1401  1501  1601
count = 
     0     1     2     3     4     5     6     7     8     9    10    11    13    15    16    17
x     = 
   100   400  1200  3200  8000 19200 44800 36864 33792 53248 12288 32768 40960 32768     0     0

Действительно, некоторые инструкции AVX2 отсутствуют. Однако обратите внимание, что не всегда рекомендуется заполнять эти пробелы путем эмуляции «отсутствующих» инструкций AVX2. Иногда бывает более эффективно перепроектировать код таким образом, чтобы избежать этих эмулированных инструкций. Например, работая с более широким вектором элементы (_epi32 вместо _epi16) с нативной поддержкой.

Можем ли мы получить какую-то пользу от vpmaddubsw, чтобы сделать для нас некоторую маскировку? Нет, нам нужно замаскировать результат vpshufb обоими способами, чтобы создать 0 в каждом другом элементе. И для старшей половины каждой пары нам понадобится множитель 256 * n, но он не поместится в байте.

Peter Cordes 14.08.2018 15:45

@PeterCordes Я думаю, vpmaddubsw - хорошая идея, спасибо! Вероятно, с помощью этой инструкции можно улучшить вычисление x_lo в _mm256_srlv_epi8 (избавиться от одного andnot).

wim 14.08.2018 16:20

О да, я не думал о сдвиге вправо, но расширение четных и нечетных элементов в нижнюю часть 16-битной пары совсем неплохо. С немедленным vpsllw, чтобы вернуть старший байт наверх, мы действительно выводим по крайней мере одну инструкцию вперед, верно? Но будьте осторожны, Haswell использует сдвиг и умножение только на p0, поэтому они конкурируют. Или, может быть, мы можем использовать vpslldq, если давление сдвига / множителя хуже, чем давление перетасовки.

Peter Cordes 14.08.2018 16:33

@PeterCordes Да, но хотя бы один из других байтовых элементов должен быть в любом случае замаскирован при использовании vpmaddubsw, так что это не всегда выигрыш. Я вернусь к этому позже ...

wim 14.08.2018 17:00

Может быть, vpmaddubsw для нижней половины, а vpmulhuw для верхней? Да, я думаю, это простая замена, которая спасает один andnot

Peter Cordes 14.08.2018 17:04

@PeterCordes Как-то vpmaddubsw для младшей половины не работал. Я не знаю точно почему, но в некоторых случаях результаты были неверными. Обратите внимание, что умножение vpmaddubsw немного странно: 8-битное без знака * 8-битное со знаком = насыщенное 16-битное со знаком. После обмена аргументами одни результаты оказались верными, а другие - нет.

wim 14.08.2018 20:15

Да ладно, мы не можем его использовать, потому что он не может умножаться на +128. Я думал, что мы могли бы использовать подписанный ввод как счетчик сдвига (с беззнаковым вводом как данные, которые нужно сдвинуть). Подписанная насыщенность - проблема, она имеет значение только для добавления. 255*127 - это только 0x7e81, а -128 * 255 = -32640, который также подходит для подписанного 16-битного.

Peter Cordes 14.08.2018 22:49

Если мы используем данные как вход со знаком, это будет похоже на расширение знак до 16 бит перед сдвигом, так что это не сработает, если мы хотим, чтобы старший байт был частью сдвига вправо.

Peter Cordes 14.08.2018 22:53

@PeterCordes Спасибо за объяснение!

wim 14.08.2018 23:02

Другие вопросы по теме