Обратный порядок байтов в регистре XMM или YMM? - PullRequest
1 голос
/ 01 июня 2019

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

Есть ли способ изменить порядок байтов в регистре XMM или YMM?

Ответы [ 2 ]

4 голосов
/ 02 июня 2019

Да, используйте SSSE3 _mm_shuffle_epi8 или AVX2 _mm256_shuffle_epi8 для перестановки байтов в 16-байтовых «дорожках» AVX2.В зависимости от вектора управления тасованием вы можете поменять пары байтов, отменить 4-байтовые единицы или отменить 8-байтовые единицы.Или переверните все 16 байтов.

Но vpshufb не является пересечением полосы, поэтому вы не можете перевернуть 32 байта одной инструкцией до AVX512VBMI vpermb.vpshufb ymm выполняет 2x 16-байтовые тасования в двух 128-битных дорожках вектора YMM.

Так что, если вы обращаетесь к массиву всего байта, а не к порядку байтов или порядку байтов отдельного элементов в массиве, у вас есть 3 варианта:

  • Придерживайтесь 128-битных векторов (простых и переносимых, и, вероятно, не медленнее на современных процессорах).И для лучшей производительности требуется только 16-байтовое выравнивание.
  • Используйте vpermq для смены полосы движения до или после vpshufb (не очень хорошо для AMD, и узкие места на 1 на тактовую частоту тасуют на текущей Intel).Но потенциально очень хорош на Ice Lake (2 порта случайного воспроизведения)
  • Загрузка с vmovdqu / vinsert128, затем vpshufb и 32-байтовым хранилищем.(Или выполнить 32-байтовую загрузку и разделить 16-байтовые хранилища, но это, вероятно, не так хорошо.)

vpshufb - это одна инструкция UOP для Intel или 2 для AMD,и обрабатывает 32 байта данных одновременно.

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


Но потенциально даже лучше - поменять только блок 16kiB перед его использованием , так что в L1d-кэше все еще горячо, когда следующий шаг читает его.Это называется блокировкой кеша.Или, может быть, использовать блоки размером 128 КБ для блокировки размера кэша L2.

Вы можете поменяться частями , когда читаете данные из файла .например, делайте системные вызовы read() в блоках по 64 КБ или 128 КБ и меняйте результат, пока он остается горячим в кеше, после того как ядро ​​скопировало данные из кэша страниц в буфер пользовательского пространства.Или используйте mmap, чтобы отобразить файл в память, и запустить цикл копирования и замены из этого.(Или для частного сопоставления - подкачка на месте; но это в любом случае вызовет копирование при записи, что не принесет особой пользы. А в Linux файл с файловой поддержкой не может использовать анонимные огромные страницы).

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

Пропуск, затрагивающий все ваши данные итолько байт-свопы имеют очень слабую вычислительную интенсивность ;Вы хотите делать больше данных со своими данными, пока они находятся в регистрах или, по крайней мере, когда они горячие в кеше.Но если вы только один раз меняете байт, а затем читаете данные много раз, или в режиме произвольного доступа, или с другого языка, такого как Python или JavaScript, который не может эффективно поменяться на лету, тогда обязательно сделайтепроход подкачки.

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


Скалярная опция, bswap, ограничена в лучшем случае 8 байтами за такт,и каждые 8 ​​байтов требуют отдельной инструкции загрузки и сохранения.(movbe при загрузке из памяти с заменой байтов сохраняет инструкцию, но на основных процессорах микросхемы не объединяются в одну загрузку + подкачка. Однако в Silvermont это одиночная работа.)

Это может привести к насыщению пропускной способности однопоточной памяти на современных процессорах, но SIMD с меньшим общим числом операций обработки для обработки одних и тех же данных позволяет неупорядоченному выполнению "видеть" дальше вперед и, например, быстрее начинать обработку пропусков TLB для следующих страниц.,Предварительная выборка данных HW и предварительная выборка TLB очень помогают, но, как правило, по крайней мере, немного лучше использовать более широкие загрузки / хранилища для memcpy.

(vpshufb достаточно дешев, чтобы по-прежнему работать, как memcpy. Или лучше, если переписать на месте.)

И, конечно, если у вас когда-нибудь будут какие-либо попадания в кеш, даже кеш L3, SIMD действительно будет сиять.

1 голос
/ 04 июня 2019

Я не могу конкурировать с легендарным Питером Кордесом ... Я хочу показать реализацию C.

Вот примеры изменения порядка следования байтов с использованием встроенных функций C (может использоваться для обратного байтового преобразования всего массива).

Есть 3 примера кода.

  1. Использование SSE2 набора инструкций.
  2. Использование набора команд SSSE3 .
  3. Использование AVX2 набора инструкций.

//Initialize XMM register with uint8 values 0 to 15 (for testing):
__m128i a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0 = _mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);


//SSE2:
//Advantage: No need to build a shuffle mask (efficient for very short loops).
//////////////////////////////////////////////////////////////////////////
//Reverse order of uint32:
__m128i a_3_2_1_0_7_6_5_4_B_A_9_8_F_E_D_C = _mm_shuffle_epi32(a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0, _MM_SHUFFLE(0, 1, 2, 3));

//Swap pairs of uint16:
__m128i a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E = _mm_shufflehi_epi16(_mm_shufflelo_epi16(a_3_2_1_0_7_6_5_4_B_A_9_8_F_E_D_C, _MM_SHUFFLE(2, 3, 0, 1)), _MM_SHUFFLE(2, 3, 0, 1));

//Swap pairs of uint8:
__m128i a_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F = _mm_or_si128(_mm_slli_epi16(a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E, 8), _mm_srli_epi16(a_1_0_3_2_5_4_7_6_9_8_B_A_D_C_F_E, 8));
//////////////////////////////////////////////////////////////////////////


//SSSE3: 
//Advantage: Not requires AVX2 support
//////////////////////////////////////////////////////////////////////////
//Build shuffle mask
const __m128i shuffle_mask = _mm_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);

a_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F = _mm_shuffle_epi8(a_F_E_D_C_B_A_9_8_7_6_5_4_3_2_1_0, shuffle_mask);
//////////////////////////////////////////////////////////////////////////


//AVX2: 
//Advantage: Potentially faster than SSSE3
//////////////////////////////////////////////////////////////////////////
//Initialize YMM register with uint8 values 0 to 31 (for testing):
__m256i a__31_to_0 = _mm256_set_epi8(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, 0);

//Build shuffle mask
const __m256i shuffle_mask2 = _mm256_set_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);

//Reverse bytes oreder of upper lane and lower lane of YMM register.
__m256i a__16_to_31__0_to_15 = _mm256_shuffle_epi8(a__31_to_0, shuffle_mask2);

//Swap upper and lower lane of YMM register
__m256i a__0_to_31 = _mm256_permute4x64_epi64(a__16_to_31__0_to_15, _MM_SHUFFLE(1, 0, 3, 2));
//////////////////////////////////////////////////////////////////////////
...