Эффективно собирать отдельные байты, разделенные байтовым шагом 4

Я пытаюсь оптимизировать алгоритм, который будет обрабатывать массивные наборы данных, которые могут сильно выиграть от инструкций AVX SIMD. К сожалению, схема входной памяти не оптимальна для требуемых вычислений. Информация должна быть переупорядочена путем сборки __m256i значений из отдельных байтов, разделенных ровно на 4 байта:

НАЧАТЬ РЕДАКТИРОВАНИЕ

Мой целевой CPUS не поддерживает инструкции AVX2, поэтому, как указали @Elalfer и @PeterCordes, я не могу использовать значения __m256i, вместо этого код должен быть преобразован для использования значений __m128i)

КОНЕЦ РЕДАКТИРОВАНИЯ

Макет DataSet в памяти


Byte 0   | Byte 1   | Byte 2   | Byte 3
Byte 4   | Byte 5   | Byte 6   | Byte 7
...
Byte 120 | Byte 121 | Byte 122 | Byte 123
Byte 124 | Byte 125 | Byte 126 | Byte 127

Желаемые значения в __m256i переменной:


| Byte 0 | Byte 4 | Byte 8 |     ...     | Byte 120 | Byte 124 |

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

union {  __m256i   reg;   uint8_t bytes[32]; } aux;
...
for( int i = 0; i < 32; i++ )
    aux.bytes[i] = data[i * 4];

Изменить:

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

Я выполняю транспонирование, переупорядочивая данные, как показано, выполняя сдвиг влево, чтобы вывести желаемый битовый столбец как наиболее значимые биты в каждом подбайте, и, наконец, извлекаю и собираю биты в одно значение uint32_t с помощью внутреннего _mm256_movemask_epi8().


person BlueStrat    schedule 12.08.2015    source источник
comment
Насколько вы заботитесь о порядке байтов?   -  person Elalfer    schedule 13.08.2015
comment
И обрабатываете ли вы их как байты позже в своем алгоритме?   -  person Elalfer    schedule 13.08.2015
comment
Почему бы вам не загрузить 4 256-битных фрагмента и не преобразовать их в 4 256-битных вектора?   -  person user3528438    schedule 13.08.2015
comment
@Elalfer, да, порядок важен, и да, после загрузки я выполняю операции на уровне байтов (на самом деле я выполняю битовую транспозицию)   -  person BlueStrat    schedule 13.08.2015
comment
@ user3528438, звучит интересно, не могли бы вы рассказать немного подробнее?   -  person BlueStrat    schedule 13.08.2015


Ответы (3)


Я только что заметил правку, в которой есть особый ответ.

Если вам нужно сделать много разных битовых позиций для одних и тех же данных, тогда ваш текущий план хорош.

Если вам нужна только одна битовая позиция (особенно самая высокая битовая позиция) из 128B памяти, вы можете использовать _mm256_movemask_ps, чтобы получить старший бит из каждого элемента 32b. Затем объедините четыре 8-битные маски в регистрах GP.

Хороший компилятор должен оптимизировать это, чтобы:

vmovdqu   ymm0, [buf + 0]
; to select a different bit:
; vpslld  ymm0, ymm0, count   ; count can be imm8 or the low byte of an xmm register
vmovmskps eax, ymm0

vmovdqu   ymm0, [buf + 32]
vmovmskps ebx, ymm0

...  ecx and edx

mov       ah, bl
mov       ch, dl
shl       ecx, 16
or        eax, ecx

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


Ответ на исходный вопрос:

Подобно идее Элальфера, но для pack инструкций вместо pshufb используйте блок перемешивания. Кроме того, все операторы AND независимы, поэтому они могут выполняться параллельно. Процессоры Intel могут выполнять 3 операции AND одновременно, но только в одном порядке. (Или сразу две перетасовки на pre-Haswell.)

// without AVX2: you won't really be able to
// do anything with a __m256i, only __m128i
// just convert everything to regular _mm_..., and leave out the final permute

mask = _mm256_set1_epi32(0x000000ff);

// same mask for all, and the load can fold into the AND
// You can write the load separately if you like, it'll still fold
L1 = and(mask, (buf))     // load and zero the bytes we don't want
L2 = and(mask, (buf+32))
L3 = and(mask, (buf+64))
L4 = and(mask, (buf+96))

// squish dwords from 2 concatenated regs down to words in 1 reg
pack12 = _mm256_packus_epi32(L1, L2);
pack34 = _mm256_packus_epi32(L3, L4);

packed = _mm256_packus_epi16(pack12, pack34);  // note the different width: zero-padded-16 -> 8

Vec = permute(packed)  // fix DWORD order in the vector (only needed for 256b version)

Vec = shift(Vec, bit_wanted)
bitvec = movemask(Vec)

    // shift:
    //  I guess word or dword granularity is fine, since byte granularity isn't available.
    //  You only care about the high bit, so it doesn't matter than you're not shifting zeroes into the bottom of each byte.

    // _mm_slli_epi32(Vec, imm8): 1 uop, 1c latency if your count is a compile-time constant.
    // _mm_sll_epi32 (Vec, _mm_cvtsi32_si128(count)): 2uop 2c latency if it's variable.

    // *not* _mm_sllv_epi32(): slower: different shift count for each element.

Если вы делаете это только с AVX (как вы сказали), тогда у вас не будет доступных целочисленных инструкций 256b. Просто постройте 128b векторов и получите 16b за один раз маскирования данных. В конце вам не понадобится окончательная перестановка.

Объединить маски с целочисленными инструкциями: (m2<<16) | m1. При желании можно даже увеличить до 64 байт данных маски, объединив две маски 32 байт.

Производительность: это позволяет избежать необходимости в отдельных инструкциях по загрузке с AVX, поскольку vpand может микро- объединить операнд памяти, если он используется с режимом однорегистровой адресации.

  • цикл 1: 3 vpand инструкций. (или только 2, если бы мы ждали адрес, так как порта загрузки всего 2.)
  • цикл 2: последние один или два vpand, один pack (L1, L2)
  • цикл 3: следующий pack (L3, L4)
  • цикл 4: финал pack
  • // 256b AVX2: переставить
  • цикл 5: упакованный сдвиг с счетчиком imm8: 1 моп, задержка 1 с.
  • цикл 6: маска движения (задержка в 3 цикла)

Задержка = 8 (SnB и выше)

Пропускная способность: 3 тасования (p5), 4 логических (p015), 1 смена (p0), 1 pmovmsk (p0). 4 загрузочных упа.

  • SnB / IvB: 9 мопов ALU -> 3c. 4 чтения из памяти: 2c.
    Итак, в зависимости от того, что вы делаете с масками, потребуется 3 аккумулятора для поддержания насыщения портов выполнения. (ceil (8/3) = 3.).

Со счетчиком сдвига в переменной, которая не может быть разрешена в константу времени компиляции путем встраивания / развертывания компилятора: задержка = 9. И сдвиг создает еще один uop для p1 / p5.

С AVX2 для Haswell и более поздних версий для vpermd есть еще 3 дополнительных задержки.

person Peter Cordes    schedule 18.08.2015
comment
Спасибо, @PeterCordes! Ты прав! Я не заметил, что внутренняя функция _mm256_shuffle_epi8 помечена как AVX2. Он работает на моей машине разработки, но не работает на целевых серверах (Sandy Bridge). - person BlueStrat; 19.08.2015
comment
@BlueStrat: Да, просто работайте с векторами 128b, используя инструкции в кодировке VEX (скомпилируйте с поддержкой AVX и убедитесь, что при разборке отображается vpand, а не pand и т. Д.). Весь целочисленный материал 256b предназначен только для AVX2, кроме loadu_si256. Вы не сможете выполнять необходимые побитовые сдвиги с векторами 256b. (Но, тем не менее, неразрушающие операции с 3 операндами отлично подходят для экономии на mov инструкциях. Что является еще большим выигрышем для SnB, потому что обработка mov* инструкций на этапе переименования регистров не выполнялась до IvyBridge.) - person Peter Cordes; 19.08.2015
comment
Еще раз большое спасибо - мне очень любопытно, вы демонстрируете огромные познания в программировании SIMD и информацию, связанную с процессором. Вы получили это специальной тренировкой? - person BlueStrat; 19.08.2015
comment
@BlueStrat: Я много лет тренировался у шаолиньского монаха, мастера компьютерного фу. Нет, серьезно, мне просто нравится знать, как все работает на самом деле, и настраивать / оптимизировать вещи. Я узнал все, что знаю в этой области, читая материалы в Интернете, время от времени экспериментируя со счетчиками производительности. Я читал статьи realworldtech.com о конструкции ЦП и agner.org/optimize содержит инструкции по внутреннему устройству ЦП. Как только у вас будет ментальная модель того, что делает ваш процессор, части будут довольно хорошо сочетаться друг с другом, и любая новая информация станет доступной. - person Peter Cordes; 19.08.2015
comment
@BlueStrat: Также получите копию справочного руководства Intel. Недавно я обновил stackoverflow.com/tags/x86/info, добавив ссылки на полезные материалы. - person Peter Cordes; 19.08.2015
comment
software.intel.com/sites/landingpage/IntrinsicsGuide - хороший краткий справочник по внутренним функциям . Я использую это все время. - person Elalfer; 19.08.2015
comment
@PeterCordes хотел бы, чтобы я мог проголосовать за это больше, спасибо за подробный счет циклов! - person BlueStrat; 19.08.2015
comment
@BlueStrat: вы можете изменить ответ, который вы пометили как принятый. Ответ Элалфера хорош, но я думаю, что мой лучше :) Рад, что вы нашли это полезным. :) - person Peter Cordes; 19.08.2015

Один из способов - упаковать байты в _mm256_shuffle_epi8, смешать все _mm256_blend_epi32 результирующие векторы (вам нужно будет сделать 4 таких загрузки + перемешивание) и выполнить одну 32-битную перестановку _mm256_permutevar8x32_epi32.

Вот псевдокод (надеюсь, вы сможете придумать маски тасования):

L1 = load32byte(buf)
L2 = load32byte(buf+32)
L3 = load32byte(buf+64)
L4 = load32byte(buf+96)

// Pack 4 bytes in the corresponding 32bit DWORD in each lane and zero-out other bytes
L1 = shuffle(L1, mask_for_L1)   
L2 = shuffle(L2, mask_for_L2)
L3 = shuffle(L3, mask_for_L3)
L4 = shuffle(L4, mask_for_L4)

// Vec = blend(blend(L1,L2),blend(L3,L4))
Vec = or(or(or(L1,L2),L3),L4)
Vec = permute(Vec)  // fix DWORD order in the vector

Обновление: я забыл причину, по которой я сказал "обнулить другие байты" - таким образом вы можете заменить blend на or

Обновление: уменьшена задержка на один цикл за счет изменения порядка or операций в соответствии с приведенным ниже комментарием Питера.

PS. Я также рекомендую вам взглянуть на Набор инструкций BMI, когда вы выполняете битовые манипуляции.

person Elalfer    schedule 14.08.2015
comment
Хорошее решение! К сожалению, я не могу использовать инструкции BMI, так как серверы, на которых будет выполняться этот код, не имеют процессоров с поддержкой BMI. Спасибо! - person BlueStrat; 18.08.2015
comment
Предполагается, что BMI будет поддерживаться на всех платформах с поддержкой AVX2 как для Intel, так и для AMD. - person Elalfer; 18.08.2015
comment
ты совершенно прав. Я указал инструкции AVX2, но на самом деле мои целевые процессоры поддерживают только AVX. Тем не менее я мог бы применить вашу технику. Спасибо еще раз! - person BlueStrat; 18.08.2015
comment
@BlueStrat и Elalfer: если вы хотите сделать версию 128b, вы можете использовать punpcklqdq для объединения регистров (dest[64:127] = src2[0:63]). Это сократит потребность в разных масках для перетасовки. Также, возможно, интересным является И-маскирование байтов, которые вы не хотите обнулять, а затем использование packusdw для сжатия двойных слов из 2 конкатенированных регистров до слов в 1 регистре. Ни то, ни другое не подходит, и я думаю, что лучше всего использовать POR для слияния. pack / punpck вообще не поможет в случае 256b, потому что он выполняет две отдельные операции с полосой 128b. - person Peter Cordes; 18.08.2015
comment
Фактически, в случае 256b вы можете просто поставить перестановку в конце, как это решение Элальфера. pshufb имеет то же поведение в полосе движения, что и pack/punpck, что и исправляет окончательная перестановка. У вас будет 4 оператора AND с той же маской, затем два packusdw, затем один packuswb, затем перестановка. Я отправлю ответ с этим. - person Peter Cordes; 18.08.2015
comment
@PeterCordes, вы правы, можно использовать unpack инструкции, чтобы слить regs. Есть несколько причин, по которым я использовал pshufb & por - ООО загрузит маски заранее, так что это не проблема, для por есть больше исполнительных единиц, чем для pack/unpack, и por будет выполняться параллельно с pshufb (по крайней мере, для Микроархитектуры Haswell / Broadwell). - person Elalfer; 19.08.2015
comment
@Elalfer: Смотрите мой ответ на код. Я делаю 4 pand (любой порт) и 3 pack insns (случайный порт). Вы делаете 4 pshufb (перемешивание портов) и 3 por. Итак, в моем случае зависимые операции (pack) также являются операциями с более ограниченной пропускной способностью. Кроме того, я согласен, что экономия на масках является незначительным преимуществом, но с моей вы даже можете сгенерировать маску с двумя инструкциями перед циклом вместо ее загрузки, таким образом не затрагивая никаких строк кеша данных. (pcmpeq xmm5, xmm5 / psrld xmm5, 24, который занимает всего 8 insn байтов) - person Peter Cordes; 19.08.2015
comment
@Elalfer: В вашей версии цепочка dep была бы короче, если бы вы сделали L1=or( L4, or(L3, or(L1,L2))). Тогда вместо 2 циклов зависимости после 4-го pshufb у вас будет только один. cycle1, 2: только pshufb. цикл3: пшуфб + пор. cycle4: pshufb + por cycle5: por. (Отличается от SnB / IvB и Nehalem, у которых есть 2 порта случайного воспроизведения. Core2 имеет медленный pshufb.) - person Peter Cordes; 19.08.2015

Вы можете попробовать развернуть этот цикл, это должно как минимум избавиться от одного сравнения (i ‹32), одного приращения (i ++) и одного умножения (i * 4) в теле цикла. Также смещения постоянного массива могут работать немного быстрее, чем переменные. Но обратите внимание, что ваш компилятор может в любом случае сгенерировать аналогичный (или лучший) код с включенными соответствующими параметрами компиляции.

union {  __m256i   reg;   uint8_t bytes[32]; } aux;
...
aux.bytes[0] = data[0];
aux.bytes[1] = data[3];
...
aux.bytes[31] = data[124];
person davlet    schedule 13.08.2015
comment
Спасибо за подсказку, действительно, компилятор развернул цикл (хотя и не полностью), уродливым было то, что при заполнении объединения по байтам выполнялось много избыточных загрузок и сохранений. В итоге я применил решение @Elalfer, чтобы избавиться от этой проблемы. - person BlueStrat; 18.08.2015
comment
@BlueStrat и davlet: другая слабость этого решения состоит в том, что ›задержка переадресации загрузки гарантируется, потому что процессоры Intel и AMD не могут перенаправить несколько хранилищ меньшего размера на более широкую нагрузку. Таким образом, после всех побайтных записей возникает дополнительная задержка в ~ 10 циклов. - person Peter Cordes; 18.08.2015