AVX2 каков наиболее эффективный способ упаковки левого на основе маски?
Если у вас есть входной массив и выходной массив, но вы хотите написать только те элементы, которые передают определенное условие, каков был бы наиболее эффективный способ сделать это в AVX2?
Я видел в SSE, где это было сделано так: (From:https://deplinenoise.files.wordpress.com/2015/03/gdc2015_afredriksson_simd.pdf)
__m128i LeftPack_SSSE3(__m128 mask, __m128 val)
{
 // Move 4 sign bits of mask to 4-bit integer value.
 int mask = _mm_movemask_ps(mask);
 // Select shuffle control data
 __m128i shuf_ctrl = _mm_load_si128(&shufmasks[mask]);
 // Permute to move valid values to front of SIMD register
 __m128i packed = _mm_shuffle_epi8(_mm_castps_si128(val), shuf_ctrl);
 return packed;
}
Это кажется прекрасным для SSE, который имеет ширину 4, и, таким образом, нуждается только в 16 входе LUT, но для AVX, который имеет ширину 8, LUT становится довольно большим (256 записей, каждые 32 байта или 8k).
Я удивлен, что AVX, похоже, не имеет инструкции для упрощения этого процесса, например, маскированного магазина с упаковкой.
Я думаю, что с некоторой перетасовкой для подсчета # бит знака, установленных слева, вы можете создать необходимую таблицу перестановок, а затем вызвать _mm256_permutevar8x32_ps. Но это также довольно много инструкций, я думаю..
кто-нибудь знает какие-нибудь способы сделать это с поддержкой AVX2? Или какой метод наиболее эффективен?
вот иллюстрация левой проблемы упаковки из приведенного выше документа:
спасибо
4 ответов
ПОДДЕРЖКОЙ AVX2 + BMI2.  См. мой другой ответ для AVX512.  (Обновление: сохранено pdep в 64-битной сборки.)
можно использовать поддержкой AVX2 vpermps (_mm256_permutevar8x32_ps) (или эквивалент целого числа,vpermd), чтобы сделать переменную пересечения полосы-shuffle.
мы можем создавать маски на лету, начиная с BMI2 pext (Извлечение Параллельных Битов) предоставляет нам побитовую версию операции, в которой мы нуждаемся.
для целых векторы с 32-разрядными или более элементов: либо 1) _mm256_movemask_ps(_mm256_castsi256_ps(compare_mask)).
Или 2) использовать _mm256_movemask_epi8 а затем измените первую константу PDEP с 0x0101010101010101 на 0x0F0F0F0F0F0F0F0F, чтобы разбросать блоки из 4 смежных битов.  Измените умножение на 0xFFU в expanded_mask |= expanded_mask<<4; или expanded_mask *= 0x11; (не проверял).  В любом случае используйте маску shuffle с VPERMD вместо VPERMPS.
для 64-разрядного целого числа или double элементы, все еще просто работает; сравнить-маска просто случается, что всегда есть пары 32-разрядных элементов, которые одинаковы, поэтому результирующее перемешивание помещает обе половины каждого 64-разрядного элемента в нужное место.  (Таким образом, вы все еще используете VPERMPS или VPERMD, потому что VPERMPD и VPERMQ доступны только с непосредственными операндами управления.)
алгоритм:
начните с константы упакованных 3-битных индексов, при этом каждая позиция имеет свой собственный индекс.  т. е. [ 7 6 5 4 3 2 1 0 ] где каждый элемент имеет ширину 3 бита.  0b111'110'101'...'010'001'000.
использовать pext чтобы извлечь индексы, которые мы хотим, в непрерывную последовательность в нижней части целочисленного регистра.  например, если мы хотим индексы 0 и 2, наша контрольная маска для pext должно быть 0b000'...'111'000'111.  pext захватить 010 и 000 индексные группы, которые соответствуют 1 битам в селекторе.  Выбранные группы упаковываются в низкие биты вывода, поэтому вывод будет 0b000'...'010'000.  (т. е. [ ... 2 0 ])
см. комментируемый код для того, как для создания 0b111000111 вход для pext из входной векторной маски.
теперь мы в той же лодке, что и compressed-LUT: распакуйте до 8 упакованных индексов.
к тому времени, когда вы сложите все части вместе, есть три итога pext/pdeps. Я работал в обратном направлении от того, что хотел, поэтому, вероятно, легче всего понять это в этом направлении.  (т. е. начните с линии shuffle и работайте назад оттуда.)
мы можем упростить распаковка, если мы работаем с индексами по одному байту вместо упакованных 3-битных групп. Поскольку у нас есть 8 индексов, это возможно только с 64-битным кодом.
посмотреть это и 32-битная версия только на godbolt Compiler Explorer.  Я использовал #ifdefs поэтому он компилируется оптимально с -m64 или -m32.  gcc тратит некоторые инструкции, но clang делает действительно хороший код.
#include <stdint.h>
#include <immintrin.h>
// Uses 64bit pdep / pext to save a step in unpacking.
__m256 compress256(__m256 src, unsigned int mask /* from movmskps */)
{
  uint64_t expanded_mask = _pdep_u64(mask, 0x0101010101010101);  // unpack each bit to a byte
  expanded_mask *= 0xFF;    // mask |= mask<<1 | mask<<2 | ... | mask<<7;
  // ABC... -> AAAAAAAABBBBBBBBCCCCCCCC...: replicate each bit to fill its byte
  const uint64_t identity_indices = 0x0706050403020100;    // the identity shuffle for vpermps, packed to one index per byte
  uint64_t wanted_indices = _pext_u64(identity_indices, expanded_mask);
  __m128i bytevec = _mm_cvtsi64_si128(wanted_indices);
  __m256i shufmask = _mm256_cvtepu8_epi32(bytevec);
  return _mm256_permutevar8x32_ps(src, shufmask);
}
это компилируется в код не загружает из памяти, только константы. (См. ссылку godbolt для этого и 32-битную версию).
    # clang 3.7.1 -std=gnu++14 -O3 -march=haswell
    mov     eax, edi                   # just to zero extend: goes away when inlining
    movabs  rcx, 72340172838076673     # The constants are hoisted after inlining into a loop
    pdep    rax, rax, rcx              # ABC       -> 0000000A0000000B....
    imul    rax, rax, 255              # 0000000A0000000B.. -> AAAAAAAABBBBBBBB..
    movabs  rcx, 506097522914230528
    pext    rax, rcx, rax
    vmovq   xmm1, rax
    vpmovzxbd       ymm1, xmm1         # 3c latency since this is lane-crossing
    vpermps ymm0, ymm1, ymm0
    ret
Итак, согласно числа Агнера Фога, это 6 uops (не считая констант или нулевого расширения, которое исчезает при вставке). На Intel Haswell это задержка 16c (1 для vmovq, 3 для каждого pdep/imul/pext / vpmovzx / vpermps). Нет параллелизма на уровне инструкций. В цикле, где это не является частью циклической зависимости, хотя (как тот, который я включил в Godbolt link), узкое место, надеюсь, просто пропускная способность, сохраняя несколько итераций этого в полете сразу.
это может управлять пропускной способностью одного на 3 цикла, узким местом на port1 для pdep/pext / imul.  Конечно, с нагрузками / магазинами и накладными расходами цикла (включая сравнение, movmsk и popcnt) общая пропускная способность uop может легко быть проблемой.  (например, цикл фильтра в моей ссылке godbolt составляет 14 uops с clang, с -fno-unroll-loops чтобы было легче читать.  Это может поддержать одного итерация за 4c, не отставая от переднего конца, если нам повезет, но я думаю, что clang не смог учесть popcntложная зависимость от его выхода, поэтому он будет узким местом на 3 / 5ths задержки 
см. мой другой ответ для AVX2 + BMI2 без LUT.
поскольку вы упоминаете о масштабируемости AVX512: не волнуйтесь,существует инструкция AVX512F именно для этого:
VCOMPRESSPS - храните разреженные упакованные значения с плавающей запятой с одной точностью в плотной памяти.  (Есть также версии для двойных и 32 или 64-битных целочисленных элементов (vpcompressq), но не байт или слово (16 бит)).  Это как BMI2 pdep / pext, но для векторов вместо битов в целочисленном Рег.
назначением может быть векторный регистр или операнд памяти, а источником-вектор и регистр маски. С регистром dest он может объединить или обнулить верхние биты. С помощью dest памяти "только непрерывный вектор записывается в место назначения памяти".
чтобы выяснить, как далеко продвинуть указатель для следующего вектора, popcnt маска.
предположим, вы хотите отфильтровать все, кроме значений >= 0 из массив:
#include <stdint.h>
#include <immintrin.h>
size_t filter_non_negative(float *__restrict__ dst, const float *__restrict__ src, size_t len) {
    const float *endp = src+len;
    float *dst_start = dst;
    do {
        __m512      sv  = _mm512_loadu_ps(src);
        __mmask16 keep = _mm512_cmp_ps_mask(sv, _mm512_setzero_ps(), _CMP_GE_OQ);  // true for src >= 0.0, false for unordered and src < 0.0
        _mm512_mask_compressstoreu_ps(dst, keep, sv);   // clang is missing this intrinsic, which can't be emulated with a separate store
        src += 16;
        dst += _mm_popcnt_u64(keep);   // popcnt_u64 instead of u32 helps gcc avoid a wasted movsx, but is potentially slower on some CPUs
    } while (src < endp);
    return dst - dst_start;
}
это компилируется (с gcc4.9 или позже) до (Проводник Компилятора Godbolt):
 # Output from gcc6.1, with -O3 -march=haswell -mavx512f.  Same with other gcc versions
    lea     rcx, [rsi+rdx*4]             # endp
    mov     rax, rdi
    vpxord  zmm1, zmm1, zmm1             # vpxor  xmm1, xmm1,xmm1 would save a byte, using VEX instead of EVEX
.L2:
    vmovups zmm0, ZMMWORD PTR [rsi]
    add     rsi, 64
    vcmpps  k1, zmm0, zmm1, 29           # AVX512 compares have mask regs as a destination
    kmovw   edx, k1                      # There are some insns to add/or/and mask regs, but not popcnt
    movzx   edx, dx                      # gcc is dumb and doesn't know that kmovw already zero-extends to fill the destination.
    vcompressps     ZMMWORD PTR [rax]{k1}, zmm0
    popcnt  rdx, rdx
    ## movsx   rdx, edx         # with _popcnt_u32, gcc is dumb.  No casting can get gcc to do anything but sign-extend.  You'd expect (unsigned) would mov to zero-extend, but no.
    lea     rax, [rax+rdx*4]             # dst += ...
    cmp     rcx, rsi
    ja      .L2
    sub     rax, rdi
    sar     rax, 2                       # address math -> element count
    ret
Я придумал этот метод, который использует сжатый LUT, который составляет 768(+1 заполнение) байт, а не 8k. Он требует трансляции одного скалярного значения, которое затем сдвигается на другую величину в каждой полосе, а затем маскируется до нижних 3 бит, что обеспечивает 0-7 LUT.
вот внутренняя версия, а также код для сборки LUT.
//Generate Move mask via: _mm256_movemask_ps(_mm256_castsi256_ps(mask)); etc
__m256i MoveMaskToIndices(int moveMask) {
    u8 *adr = g_pack_left_table_u8x3 + moveMask * 3;
    __m256i indices = _mm256_set1_epi32(*reinterpret_cast<u32*>(adr));//lower 24 bits has our LUT
    __m256i m = _mm256_sllv_epi32(indices, _mm256_setr_epi32(29, 26, 23, 20, 17, 14, 11, 8));
    //now shift it right to get 3 bits at bottom
    __m256i shufmask = _mm256_srli_epi32(m, 29);
    return shufmask;
}
u32 get_nth_bits(int a) {
    u32 out = 0;
    int c = 0;
    for (int i = 0; i < 8; ++i) {
        auto set = (a >> i) & 1;
        if (set) {
            out |= (i << (c * 3));
            c++;
        }
    }
    return out;
}
u8 g_pack_left_table_u8x3[256 * 3 + 1];
void BuildPackMask() {
    for (int i = 0; i < 256; ++i) {
        *reinterpret_cast<u32*>(&g_pack_left_table_u8x3[i * 3]) = get_nth_bits(i);
    }
}
вот сборка, сгенерированная VS2015:
lea eax, DWORD PTR [rcx+rcx*2]
movsxd  rcx, eax
lea rax, OFFSET FLAT:?g_pack_left_table_u8x3@@3PAEA ; g_pack_left_table_u8x3
vpbroadcastd ymm0, DWORD PTR [rcx+rax]
vpsllvd ymm0, ymm0, YMMWORD PTR __ymm@000000080000000b0000000e0000001100000014000000170000001a0000001d
vpsrld  ymm0, ymm0, 29
в случае, если кто-то заинтересован, вот решение для SSE2, которое использует инструкцию LUT вместо данных LUT aka таблицы перехода. С AVX это потребует 256 случаев.
каждый раз, когда вы называете LeftPack_SSE2 ниже он использует по существу три инструкции: jmp, shufps, jmp.  Пять из шестнадцати случаев не нуждаются в изменении вектора.
static inline __m128 LeftPack_SSE2(__m128 val, int mask)  {
  switch(mask) {
  case  0:
  case  1: return val;
  case  2: return _mm_shuffle_ps(val,val,0x01);
  case  3: return val;
  case  4: return _mm_shuffle_ps(val,val,0x02);
  case  5: return _mm_shuffle_ps(val,val,0x08);
  case  6: return _mm_shuffle_ps(val,val,0x09);
  case  7: return val;
  case  8: return _mm_shuffle_ps(val,val,0x03);
  case  9: return _mm_shuffle_ps(val,val,0x0c);
  case 10: return _mm_shuffle_ps(val,val,0x0d);
  case 11: return _mm_shuffle_ps(val,val,0x34);
  case 12: return _mm_shuffle_ps(val,val,0x0e);
  case 13: return _mm_shuffle_ps(val,val,0x38);
  case 14: return _mm_shuffle_ps(val,val,0x39);
  case 15: return val;
  }
}
__m128 foo(__m128 val, __m128 maskv) {
  int mask = _mm_movemask_ps(maskv);
  return LeftPack_SSE2(val, mask);
}
 
            