Могу ли я использовать SIMD для сортировки/категоризации сегментов?

Мне любопытно, что такое SIMD, и мне интересно, сможет ли он справиться с этим вариантом использования.

Допустим, у меня есть массив из 2048 целых чисел, например [0x018A, 0x004B, 0x01C0, 0x0234, 0x0098, 0x0343, 0x0222, 0x0301, 0x0398, 0x0087, 0x0167, 0x0389, 0x03F34, 0x00x0]

Обратите внимание, что все они начинаются с 0x00, 0x01, 0x02 или 0x03. Я хочу разделить их на 4 массива:

  • Один для всех целых чисел, начинающихся с 0x00
  • Один для всех целых чисел, начинающихся с 0x01
  • Один для всех целых чисел, начинающихся с 0x02
  • Один для всех целых чисел, начинающихся с 0x03

Я предполагаю, что у меня будет такой код:

int main() {
   uint16_t in[2048] = ...;

   // 4 arrays, one for each category
   uint16_t out[4][2048];

   // Pointers to the next available slot in each of the arrays
   uint16_t *nextOut[4] = { out[0], out[1], out[2], out[3] };

   for (uint16_t *nextIn = in; nextIn < 2048; nextIn += 4) {

       (*** magic simd instructions here ***)

       // Equivalent non-simd code:
       uint16_t categories[4];
       for (int i = 0; i < 4; i++) {
           categories[i] = nextIn[i] & 0xFF00;
       }
       for (int i = 0; i < 4; i++) {
           uint16_t category = categories[i];
           *nextOut[category] = nextIn[i];
           nextOut[category]++;
       }
   }
   // Now I have my categoried arrays!
}

Я предполагаю, что мой первый внутренний цикл не нуждается в SIMD, это может быть просто инструкция (x & 0xFF00FF00FF00FF00), но мне интересно, можем ли мы превратить этот второй внутренний цикл в инструкцию SIMD.

Есть ли какая-нибудь SIMD-инструкция для этого действия по «категоризации», которое я делаю?

Инструкции по «вставке» кажутся несколько многообещающими, но я слишком зелен, чтобы понимать описания на https://software.intel.com/en-us/node/695331.

Если нет, то приближается ли что-нибудь?

Спасибо!


person Verdagon    schedule 18.09.2018    source источник
comment
Разрозненные магазины — для этого вам понадобится AVX-512. Вероятно, это тоже не будет суперэффективно.   -  person Paul R    schedule 18.09.2018
comment
Спасибо за наводку, очень интересно! После некоторого чтения кажется, что разрозненные хранилища могут хранить кучу чисел для группы соответствующих указателей. Как мне сопоставить эти числа (0x00-0x03) с указателями? Кроме того, почему бы ему не быть эффективным?   -  person Verdagon    schedule 18.09.2018
comment
Потому что хранилища разброса по-прежнему будут узким местом из-за промахов кеша или, если нет, по-прежнему ограничены 1 элементом за такт, как обычные хранилища. Инструкции Scatter также декодируют большое количество мопов (больше, чем собирают нагрузки), поэтому они снижают пропускную способность внешнего интерфейса. Вы также должны обнаруживать конфликты (когда несколько элементов в векторе будут попадать в одну и ту же корзину, вам нужно записывать их по последовательным адресам, не наступая друг на друга, и вы должны увеличивать счетчики позиций для каждой корзины на правильные суммы .)   -  person Peter Cordes    schedule 18.09.2018
comment
Так что SIMD-версия должна выполнять много дополнительной работы по сравнению с выполнением одного элемента за раз. Возможно, с эффективным vpconflictd (например, на KNL, но не на Skylake-avx512) вы сможете выйти вперед. Это похоже на проблему с гистограммой (где вы увеличиваете массив счетчиков для каждого сегмента), но сложнее, потому что на самом деле вам нужно сохранить каждый элемент.   -  person Peter Cordes    schedule 18.09.2018
comment
Спасибо за всю эту информацию, я очень ценю это! Я также задавался вопросом об этой конфликтной ситуации, похоже, что vpconflictd - самое близкое, что мы можем получить. Я немного почитаю о проблеме с гистограммой, спасибо за наводку!   -  person Verdagon    schedule 19.09.2018


Ответы (1)


Вы можете сделать это с SIMD, но насколько быстро это будет зависеть от того, какие именно наборы инструкций у вас есть, и насколько вы умны в своей реализации.

Один из подходов состоит в том, чтобы взять массив и «просеять» его, чтобы отделить элементы, принадлежащие разным корзинам. Например, возьмите 32 байта из вашего массива, который будет иметь 16 16-битных элементов. Используйте некоторые инструкции cmpgt, чтобы получить маску, которая определяет, попадает ли каждый элемент в корзину 00 + 01 или в корзину 02 + 03. Затем используйте какую-либо операцию «сжатия» или «фильтрации», чтобы переместить все замаскированные элементы непрерывно в один конец регистра, а затем то же самое для немаскированных элементов.

Затем повторите это еще раз, чтобы отделить 00 от 01 и 02 от 03.

С AVX2 вы можете начать с этого вопроса, чтобы вдохновиться операцией "сжатия". С AVX512 вы можете использовать инструкцию vcompress, чтобы помочь: она выполняет именно эту операцию, но только с 32-битной или 64-битной детализацией, поэтому вам нужно будет сделать по крайней мере пару для каждого вектора.

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

Наконец, вы также можете рассмотреть возможность организации ваших данных по-другому, либо в источнике, либо на этапе предварительной обработки: отделить байт «категории», который всегда равен 0-3, от байта полезной нагрузки. Многие этапы обработки должны выполняться только на одном или другом, поэтому вы можете потенциально повысить эффективность, разделив их таким образом. Например, вы можете выполнить операцию сравнения с 32 байтами всех категорий, а затем выполнить операцию сжатия с 32 байтами полезной нагрузки (по крайней мере, на последнем этапе, когда каждая категория уникальна).

Это привело бы к массивам байтовых элементов, а не к 16-битным элементам, где байт «категория» является неявным. Вы сократили размер данных вдвое, что может ускорить все остальные операции, которые вы захотите делать с данными в будущем.

Если вы не можете создать исходные данные в этом формате, вы можете использовать группировку как возможность удалить байт тега, когда вы помещаете полезную нагрузку в правильную корзину, поэтому на выходе будет uint8_t out[4][2048];. Если вы выполняете левый пакет SIMD с тасовкой pshufb байтов, как обсуждалось в комментариях, вы можете выбрать вектор управления тасованием, который упаковывает только байты полезной нагрузки в младшую половину.

(До AVX512BW в x86 SIMD не было никакого перетасовки слов с переменным управлением, только байт или двойное слово, поэтому вам уже требовалось перетасовка байтов, которая может так же легко отделять полезные данные от тегов одновременно с упаковкой байтов полезных данных в конец. )

person BeeOnRope    schedule 19.09.2018
comment
Да, фильтр и левая упаковка должны хорошо работать для 2, 3 или 4 ведер, если вы можете эффективно левостороннюю упаковку. Многоэтапный подход с блокировкой кеша для 4..16 сегментов может быть хорошим, но копирование данных слишком много раз в какой-то момент перестанет того стоить. Локальность кэша (всего от 2 до 4 выходных потоков) действительно помогает многошаговому левому пакету по сравнению с однопроходной гистограммой указателей. Я подумал о начале этой идеи, комментируя вопрос, но я думаю, что отклонил ее, потому что я представлял себе больше ведер и не думал об идее с несколькими проходами. - person Peter Cordes; 19.09.2018
comment
Вертикальная сортировка по элементам с упакованными инструкциями min/max не может исключить наличие элемента для нижнего сегмента в самом максимальном векторе. Так что я не думаю, что это избавит вас от работы, но может привести к лучшему поведению кеша, если вы обычно делаете большие блоки хранилищ, прежде чем касаться других строк кеша. Вы можете pcmpgtd / pmovmskb / test+jcc пропустить левый пакет/сохранение для нижних сегментов в векторе, который обычно не имеет элементов для нижних сегментов. (Или ptest/jcc с маской, которая проверяет, чтобы старшие биты всех элементов были нулевыми.) - person Peter Cordes; 19.09.2018
comment
О, я только что снова посмотрел на вопрос, я не понял, как мало ведер они использовали. Да, одношаговый левый пакет должен быть отличным. Я бы, вероятно, выбрал 128-битные векторы для эффективной упаковки слева, потому что AVX2 не имеет перетасовки 16-битных элементов, пересекающих полосу, поэтому вам нужно pshufb. И нет vpcompressw. Хм, распаковка в 32-битные элементы может быть выигрышной здесь, особенно с AVX512. - person Peter Cordes; 19.09.2018
comment
Я не знаю, каков ограничивающий фактор, не записывая все это, но кажется вероятным, что 256-битные векторы все еще могут быть выигрышными: особенно если вы можете выполнить pshufb все сразу с одной записью LUT, которая охватывает обе дорожки. . Тогда вам останется только разбить магазины по отдельности. Про вертикальную сортировку: да, вы ничего не можете исключить, но, как вы предлагаете, вы можете вероятностно пропустить многие операции, или вы можете записать, какие элементы не на месте, а затем обработать их по-другому (например, скаляр fixup после того, как вы сделали наиболее распространенный тип записи в цикле SIMD). - person BeeOnRope; 19.09.2018
comment
256-битный vpshufb для левой упаковки двух дорожек по отдельности по-прежнему нуждается в отдельном popcnt после разделения результата vpmovmskb и требует одного широкого вектора управления перемешиванием. Я думаю, вы можете загрузить / vinserti128 две половины маски тасования после того, как вы сгенерируете их отдельно с помощью BMI2 pdep, или из 256 * 16-байтовой LUT (гадость), или 256 * 8-байтной LUT с vpmovzxbw или load + vpunpcklbw для повторения каждого байта и vpaddb для получения n, n+1 пары индексов. Да, возможно, 256 * 16 = 4096 байт для LUT - лучший выбор, если он будет использоваться неоднократно. - person Peter Cordes; 19.09.2018
comment
@PeterCordes - да, я пытался придумать хорошие способы перетасовать две дорожки, чтобы их можно было максимально эффективно обрабатывать как единое целое: например, перетасовывая их в противоположных направлениях (нижняя половина вправо, а верхняя половина вправо). слева), поэтому они встречаются в середине вектора, но как тогда это сохранить? Или выполнять перетасовку с пересечением полос, чтобы все элементы были непрерывными (т. е. полностью заполнять один поздно, если исходный вектор был заполнен более чем наполовину), но здесь это также кажется неудобным, поскольку нет переменной маски vpermw. - person BeeOnRope; 19.09.2018
comment
Поскольку нам не нужно сохранять порядок в этом случае (требование слабее, чем истинная операция фильтрации или сжатия), vpermd на самом деле работает для многих конфигураций, поскольку вы можете поменять местами полные DWORD с одной полосы на другую, чтобы попытаться заполните его и сделайте вещи смежными, но некоторые случаи не работают (например, 5 и 3 элемента), и в любом случае это кажется дорогим. Возможно, отсутствие сохранения порядка также может привести к упрощению в LUT или другому подходу, но я этого не вижу. - person BeeOnRope; 19.09.2018
comment
AVX512BW vpermw — это переменная маска, но на SKX она стоит несколько операций. Интересный момент о не сохранении порядка, который позволяет распаковывать в dword, фильтровать/упаковывать влево, а затем упаковывать два результата с помощью vpackusdw в полосе или что-то в этом роде. Также было согласовано, что OP должен хранить байты категории отдельно от байтов данных. Операции с поддвойными словами также могут быть операциями с байтами, если только у вас нет AVX512BW. - person Peter Cordes; 19.09.2018
comment
@PeterCordes - да, но одна проблема заключается в том, что LUT становятся очень большими, когда вы упаковываете в два раза больше элементов в каждом векторе. Таким образом, использование полного 256-битного LUT может оказаться невозможным. Возможно, создание маски перемешивания вручную с использованием любых приемов, которые могут сработать. Может быть, там также скрывается гибридная стратегия с меньшим LUT и некоторыми операциями ALU ... - person BeeOnRope; 19.09.2018
comment
Правильно, я отвечал в конце этот комментарий, где вы сказали, что нет маски переменной vpermw. Может ты имел ввиду в AVX2? Определенно сложно использовать, а байтовые элементы действительно усложняют создание масок. Так что да, фильтровать их по ведрам было бы намного лучше. - person Peter Cordes; 19.09.2018
comment
@PeterCordes - возможно, была путаница, я не возражал и даже не имел в виду vpermw (и да, я имел в виду AVX2). Я просто согласился с вами, что вы могли бы также использовать байтовые операции (в AVX2), и просто сказал, что если вы плотно упаковываете байты полезной нагрузки, ваши LUT взорвутся. Таким образом, речь шла об упаковке байтов полезной нагрузки и о том, как это может привести к различным методам, поскольку компромиссы разные (вам действительно нужны байтовые перетасовки, а не просто использование байтовых перетасовок для перетасовки элементов WORD, так что ... большие LUT). - person BeeOnRope; 19.09.2018
comment
Вы всегда можете работать с кусками размером 8x1 байт с movq load/compare/pmovmskb. Тогда вам нужен только 256 * 8-байтный LUT, и вы можете использовать его без каких-либо дополнительных инструкций по его расширению. Или pdep. Так что более плотная упаковка не вредит, она может просто не помочь. - person Peter Cordes; 19.09.2018
comment
@PeterCordes - да, точно. Конечно, это не повредит, если вам нужно предварительно обработать свои данные, чтобы получить их такими, а не иметь возможность генерировать их такими в первую очередь. - person BeeOnRope; 19.09.2018