Доступ к произвольным 16-битным элементам, упакованным в 128-битный регистр

С помощью встроенных функций компилятора Intel, учитывая 128-битный регистр, упаковывающий 8 16-битных элементов, как мне получить доступ (дешево) к произвольным элементам из регистра для последующего использования _mm_cvtepi8_epi64 (знак расширяет два 8-битных элемента, упакованных в младшие 16 бит регистра, до двух 64-битных элементов)?


Объясню, почему я спрашиваю:

  1. Вход: буфер в памяти с k байтами, каждый 0x0 или 0xff.
  2. Желаемый результат: для каждых двух последовательных байтов входа регистр, упаковывающий два четверных слова (64-битных) с 0x0 и 0xffff ffff ffff ffff соответственно.
  3. Конечная цель: суммировать буфер из k двойников, замаскированных в соответствии с записями входного буфера.

Примечание. Значения 0x0 и 0xff входного буфера могут быть изменены на наиболее полезные, при условии, что эффект маскирования перед суммой сохраняется.

Как видно из моего вопроса, мой текущий план заключается в следующем, потоковая передача через входные буферы:

  1. Расширьте буфер входной маски с 8 до 64 бит.
  2. Замаскируйте двойной буфер расширенной маской.
  3. Суммируйте замаскированные двойники.

Спасибо, Асаф


person Whaa    schedule 01.04.2012    source источник
comment
pmovsxbq может фактически взять операнд памяти и напрямую загрузить эти два байта из памяти. Но, конечно, команду MSVC это не волнует.   -  person harold    schedule 01.04.2012
comment
@harold Да, на самом деле отсутствует режим адресации для встроенных функций, предоставленных Intel. Так что на самом деле виновата Intel, а не MS (как я ненавижу это говорить ;-)). Простое решение - использовать pmovsxbq во встроенной сборке. В противном случае можно прочитать сразу 16 байт и несколько pshufb, чтобы переместить байты в нужные места.   -  person Gunther Piez    schedule 01.04.2012
comment
@drhirsch ну это неожиданно .. спасибо, что дали мне знать   -  person harold    schedule 01.04.2012
comment
@drhirsch, @harold: См. мой ответ ниже - просто используйте внутреннюю функцию, передав ему разыменованный указатель. По крайней мере, gcc и icc решают поступать правильно.   -  person FrankH.    schedule 05.04.2012


Ответы (3)


Каждый байт является маской для целого числа double, поэтому PMOVSXBQ делает именно то, что нам нужно: загружает два байта из указателя m16 и расширяет их знаком до двух половин 64-битного (qword) регистра xmm. .

# UNTESTED CODE
# (loop setup stuff)
# RSI: double pointer
# RDI: mask pointer
# RCX: loop conter = mask byte-count
    add   rdi, rcx
    lea   rsi, [rsi + rcx*8]  ; sizeof(double) = 8
    neg   rcx  ; point to the end and count up

    XORPS xmm0, xmm0  ; clear accumulator
      ; for real use: use multiple accumulators
      ; to hide ADDPD latency

ALIGN 16
.loop:
    PMOVSXBQ XMM1, [RDI + RCX]
    ANDPD    XMM1, [RSI + RCX * 8]
    ADDPD    XMM0, XMM1
    add      RCX, 2      ; 2 bytes / doubles per iter
    jl       .loop

    MOVHLPS  XMM1, XMM0    ; combine the two parallel sums
    ADDPD    XMM0, XMM1 
    ret

Для реального использования используйте несколько аккумуляторов. Также см. режимы микрослияния и адресации re: режимы индексированной адресации.

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


Чтобы ответить на другую часть вашего вопроса, о как переместить данные, чтобы выровнять их для PMOVSX:

В Sandybridge и более поздних версиях использование PMOVSXBQ из ОЗУ, вероятно, хорошо. На более ранних процессорах, которые не могли обрабатывать две загрузки за цикл, загрузка 16 Б данных маски за раз и сдвиг их на 2 байта за раз с помощью PSRLDQ xmm1, 2 поместит 2 байта данных маски в младшие 2 байта регистра. Или, может быть, PUNPCKHQDQ или PSHUFD, чтобы запустить две цепочки зависимостей, переместив высокий 64 в низкий 64 другого регистра. Вам нужно будет проверить, какой порт используется какой инструкцией (сдвиг или перемешивание / извлечение), и посмотреть, какой из них меньше конфликтует с PMOVSX и ADDPD.

punpck и pshufd оба используют p1 / p5 на SnB, как и pmovsx. addpd может работать только на p1. andpd может работать только на p5. Хм, может быть, PAND было бы лучше, так как он может работать на p0 (и p1 / p5). В противном случае ничто в цикле не будет использовать порт выполнения 0. Если есть штраф за задержку для перемещения данных из целочисленных доменов в домены fp, это неизбежно, если мы будем использовать PMOVSX, поскольку это приведет к получению данных маски в домене int. Лучше использовать больше аккумуляторов, чтобы цикл был длиннее, чем самая длинная цепочка зависимостей. Но держите его ниже 28 мопов или около того, чтобы уместиться в буфере цикла, чтобы обеспечить возможность выдачи 4 мопов за цикл.

И еще об оптимизации всего этого: выравнивание цикла на самом деле не требуется, так как в nehalem и позже он уместится в буфере цикла.

Вы должны развернуть цикл на 2 или 4, потому что процессоры Intel до Haswell не имеют достаточного количества исполнительных блоков для обработки всех 4 (объединенных) мопов за один цикл. (3 вектора и одна объединенная _17 _ / _ 18_. Две нагрузки объединяются с векторными мопами, частью которых они являются.) Sandybridge и более поздние версии могут выполнять обе загрузки в каждом цикле, поэтому возможна одна итерация за цикл, за исключением накладных расходов на цикл.

О, ADDPD имеет задержку в 3 цикла. Таким образом, вам нужно развернуть и использовать несколько аккумуляторов, чтобы цепочка зависимостей, переносимая по циклу, не была узким местом. Вероятно, разверните на 4, а затем суммируйте 4 аккумулятора в конце. Вам придется сделать это в исходном коде даже с помощью встроенных функций, потому что это изменит порядок операций для математики FP, поэтому компилятор может не захотеть делать это во время развертывания.

Таким образом, каждый цикл, развернутый на 4, потребует 4 тактовых цикла плюс 1 муп на накладные расходы цикла. В Nehalem, где у вас есть крошечный кеш цикла, но нет кеша uop, развертывание может означать, что вам нужно начать заботиться о пропускной способности декодера. Однако на «до-песчаном мосту» одна загрузка за такт, вероятно, в любом случае будет узким местом.

Для пропускной способности декодера вы, вероятно, можете использовать ANDPS вместо ANDPD, что требует для кодирования на один байт меньше. IDK, если это поможет.


Расширение этого числа до 256 ymm регистров потребует AVX2 для наиболее простой реализации (для VPMOVSXBQ ymm). Вы можете получить ускорение на AVX-only, выполнив два VPMOVSXBQ xmm и объединив их с VINSERTF128 или чем-то в этом роде.

person Peter Cordes    schedule 04.06.2015
comment
Ищу следующую трансформацию. Это 2 слова XMM на 4 слова XMM со значением X безразлично. Видите ли вы эффективный способ сделать это? [A1 A2 A3 A4][B1 B2 B3 B4] ... => [A1 B1 X X][A2 B2 X X][A3 B3 X X][A4 B4 X X]. Я пробовал pinsrd и pextrd, но у них больше накладных расходов, чем я могу вынести. - person jww; 19.06.2018
comment
@jww: r0 = punpckldq(v0,v1) / r1 = извлечь высокую половину r0 с помощью movhlps или punpckhqdq. Повторите с punpckHdq для выходов A3 / B3 и A4 / B4. Без AVX вам понадобится movdqa, чтобы избежать затирания первого входа, чтобы вы могли распаковывать как низкие, так и высокие. - person Peter Cordes; 19.06.2018
comment
Я просматривал старые записи, в которых вы мне помогали раньше. Как вы относитесь к _mm_shuffle_ps с _MM_SHUFFLE? Обратной стороной является то, что мне приходится делать это четыре раза. - person jww; 19.06.2018
comment
@jww: Вам определенно нужны 4 инструкции для получения 4 выходных данных, как и мое решение с 4 инструкциями. x86 не имеет инструкций SIMD с несколькими выходами. _mm_shuffle_ps с AVX - это хорошо: все 4 могут читать из исходных источников, а не зависят от предыдущего. Но без кодирования операндов VEX 3, похоже, нет никаких преимуществ перед _mm_unpacklo/hi_ps и _mm_movehi_ps(tmp, vec_with_high_half) (или какими бы то ни было встроенными функциями для unpcklps / movhlps). Использование movhlps с правильно выбранной целевой переменной может сэкономить movaps - person Peter Cordes; 19.06.2018
comment
В итоге мы использовали распаковки. Казалось, что для всех путей нужно по 4 из них. Распаковка победила и не требовала *_ps забросов. Кроме того, нам требовалось _mm_shuffle_epi8 из SSSE3, поэтому мы не ограничивались MMX / SSE / SSE2. См. Также cham-simd.cpp. - person jww; 20.06.2018
comment
@jww: Вы заметили избыточность между выходами? Странно, как вы помещаете _mm_unpacklo_epi32(a, b); и так далее как в UnpackXMM<0>, так и в UnpackXMM<1>, но я думаю, что если компиляторы заботятся обо всех надежных CSE, которые удалены после встраивания, это не причинит никакого вреда. То, что не удастся оптимизировать в вашем коде, - это использование kr = _mm_shuffle_epi8(k, _mm_set_epi8(7,6,5,4, 7,6,5,4, 7,6,5,4, 7,6,5,4)); и т. Д. Для трансляции 1 из 4 элементов. gcc не может преобразовать это в pshufd копирование и перемешивание (_mm_shuffle_epi32), поэтому вместо этого вы получите movdqa + pshufb с отдельной маской. - person Peter Cordes; 20.06.2018
comment
@jww: также __m128i a = UnpackXMM<0>(block0); и так далее (в функциях блока Enc / Dec 1) следует оптимизировать до одного pshufb, просто переставляя данные в block0, но я не уверен, что это произойдет с gcc. Хотя, наверное, с лязгом. RotateRight32<8> можно избежать использования другой маски, используя _mm_alignr_epi8(same,same,1) (palignr). Я попытался перенести код на Godbolt, где было бы легко увидеть сопоставления исходных текстов и asm, но трудно было распутать зависимости / вспомогательные функции Crypto ++. В 4-блочных функциях, если вы выполните endian-swap перед xpose, пара этих insns может быть смешана. - person Peter Cordes; 20.06.2018

Скорее, касательно самого вопроса, более подробное заполнение некоторой информации о комментариях, потому что сам раздел комментариев слишком мал, чтобы вместить это (sic!):

По крайней мере, gcc может иметь дело со следующим кодом:

#include <smmintrin.h>

extern int fumble(__m128i x);

int main(int argc, char **argv)
{
    __m128i foo;
    __m128i* bar = (__m128i*)argv;

    foo = _mm_cvtepi8_epi64(*bar);

    return fumble(foo);
}

Это превращает это в следующую сборку:

Disassembly of section .text.startup:

0000000000000000 :
   0:   66 0f 38 22 06          pmovsxbq (%rsi),%xmm0
   5:   e9 XX XX XX XX          jmpq   .....

Это означает, что встроенные функции не обязательно должны иметь форму аргумента памяти - компилятор прозрачно обрабатывает разыменование аргумента mem и, если возможно, использует соответствующую инструкцию операнда памяти. ICC делает то же самое. У меня нет Windows-машины / Visual C ++, чтобы проверить, поддерживает ли MSVC то же самое, но я бы ожидал этого.

person FrankH.    schedule 04.04.2012
comment
Не совсем уверен в этом. Форма сборки не требует выравнивания и принимает указатель на слово (movw или mov WORD PTR). Будет ли компилятор выдавать pmovsxbq, даже если указатель не выровнен? В любом случае это лучший ответ, чем у Пола Р., который бесполезен для этого сценария. - person Gunther Piez; 05.04.2012
comment
Хорошо, теперь я вижу, что указатель фактически не выровнен. Простите за шум :-) - person Gunther Piez; 05.04.2012
comment
@drhirsch: Вышеупомянутое, конечно, надумано - просто чтобы проиллюстрировать, что компилятор выдаст pmovsxbq (...), %xmm.., если встроенному объекту в качестве аргумента будет задан разыменованный указатель. Я просто выбрал произвольный доступный указатель, отличный от NULL ;-) - person FrankH.; 05.04.2012

Вы смотрели _mm_extract_epi16 (PEXTRW) и _mm_insert_epi16 (PINSRW)?

person Paul R    schedule 01.04.2012
comment
Я сделал, и я предполагаю, что они потенциально будут выводиться в память, а не в регистр, что замедлит все. Я ошибся? Компилятор (MSVC) исправит это? - person Whaa; 02.04.2012
comment
Нет - эти инструкции работают напрямую между регистрами SSE (xmm) и обычными регистрами. Если вы посмотрите на код, созданный, например, для _mm_set_epi16 вы увидите, что он просто генерирует строку из PINSRWs. - person Paul R; 02.04.2012
comment
@ Анонимный отрицательный голос: не могли бы вы добавить комментарий, почему вы считаете, что приведенный выше ответ неуместен или бесполезен? - person Paul R; 05.04.2012
comment
Ответ бесполезен, потому что вы не объясняете, как использовать pextrw и pinsrw в сценарии, заданном OP. Единственный способ - это развернуть цикл 8 раз (потому что pextrw принимает только немедленно), переместить 16-битное значение в gpr, обратно в регистр xmm и расширить с помощью pmovsxbq, чтобы выполнить операцию маскирования с двойными числами. Если есть простое использование этих двух инструкций, которое я не вижу, пожалуйста, объясните это. - person Gunther Piez; 05.04.2012
comment
@drhirsch: извините - я думал, что это очевидно - я не уверен, что он заслуживает отрицательного голосования - OP, похоже, нашел его полезным, а другие проголосовали за ответ. Я полагаю, что вы имеете право на свое мнение, каким бы резким оно ни было, но подобный негатив имеет тенденцию отталкивать людей от предложения помощи. - person Paul R; 05.04.2012
comment
Я действительно сомневаюсь, что ответ был полезен OP, даже если он согласился - по причине, которую я указал: инструкции, которые вы рекомендуете, едва ли применимы в сценарии этого вопроса. Скажите, а был ли у вас алгоритм, аналогичный тому, что я описал в уме, когда вы писали ответ? Если нет, то это быстрый и дешевый ответ, который иногда работает и полезен, а иногда - нет. На этот раз не работает. И, эй, у вас 3 голоса за против 1 против, не говорите мне, что вы сейчас разочарованы :-) - person Gunther Piez; 05.04.2012
comment
@drhirsch: Алгоритм, который вы описали, - это именно то, что я сделал. К сожалению, это не так быстро, как я думал. - person Whaa; 08.04.2012
comment
@Whaa Я почти ожидал этого. На большинстве процессоров Intel существует штраф за перенос данных из GPR в SSE или обратно. Вы можете попробовать подход Franks, это экономит некоторые перемещения регистров за счет дополнительных обращений к памяти (хотя все они находятся в L1). - person Gunther Piez; 08.04.2012
comment
@Whaa Возможно, самый быстрый подход - развернуть цикл восемь раз, но вместо pextrw используйте pshufb или psrldq, чтобы переместить байты в нужные места (требуется одно перемещение регистра и одна логическая инструкция, если у вас есть AVX, оба объединяются в один). - person Gunther Piez; 08.04.2012