Загрузите и продублируйте 4 числа с плавающей точкой с одной точностью в упакованную переменную __m256 с наименьшим количеством инструкций

#c #avx

Вопрос:

У меня есть массив с плавающей точкой,содержащий A,B,C, D 4 числа с плавающей точкой, и я хочу загрузить их в __m256 переменную, такую как AABBCCDD. Как лучше всего это сделать? Я знаю, что использование _mm256_set_ps() -это всегда вариант, но он кажется медленным с инструкциями по 8 процессорам. Спасибо.

Комментарии:

1. Есть ли у вас AVX2, для vpmovzdq ymm, mem которого (2 uops на Intel) нужно настроить vmovsldup ? Или просто AVX2 vpermps с постоянной вектора перетасовки, после 128-битной загрузки.

2. @PeterCordes да AVX2 доступен. Я ориентируюсь на обычный настольный процессор

3. Хорошо, тогда я бы рекомендовал принять мой ответ. Это, по крайней мере, так же хорошо, как ответ Майка на современные основные процессоры с AVX2. (Или с помощью clang компилируется в тот же asm.)

Ответ №1:

Если бы ваши данные были результатом другого векторного вычисления (и в __m128), вам понадобился бы AVX2 vpermps ( _mm256_permutexvar_ps ) с управляющим вектором _mm256_set_epi32(3,3, 2,2, 1,1, 0,0) .

vpermps ymm составляет 1 uop на Intel, но 2 uop на Zen2 (с пропускной способностью 2 цикла). И 3 uops на Zen1 с пропускной способностью по одному на 4 такта. (https://uops.info/)

Если это был результат отдельных скалярных вычислений, вы можете перемешать их вместе с _mm_set_ps(d,d, c,c) (1x vshufps), чтобы настроить для vinsertf128.


Но с данными в памяти, я думаю, что лучше всего использовать 128-битную широковещательную загрузку, а затем перетасовку в полосе. Для этого требуется только AVX1, а на современных процессорах это 1 загрузка 1 перетасовка uop на Zen2, Haswell и более поздних версиях. Он также эффективен на Zen1: единственной перетасовкой при пересечении полосы движения является 128-битная широковещательная загрузка.

Использование перетасовки в полосе движения имеет меньшую задержку, чем пересечение полосы движения как на Intel, так и на Zen2 (256-разрядные исполнительные устройства для перетасовки). Для этого по-прежнему требуется 32-байтовая константа вектора управления перетасовкой, но если вам нужно делать это часто, она, как правило / надеюсь, останется горячей в кэше.

 __m256  duplicate4floats(void *p) {
   __m256 v = _mm256_broadcast_ps((const __m128 *) p);   // vbroadcastf128
   v = _mm256_permutevar_ps(v, _mm256_set_epi32(3,3, 2,2,  1,1, 0,0));  // vpermilps
   return v;
}
 

Современные процессоры обрабатывают широковещательные загрузки прямо в порту загрузки, не требуется перестановка uop. (Sandybridge действительно нуждается в uop для перетасовки портов 5 vbroadcastf128 , в отличие от более узких передач, но Haswell и более поздние версии являются чисто портами 2/3. Но SnB не поддерживает AVX2, поэтому перетасовка при пересечении полосы движения с детализацией менее 128 бит не была вариантом.)

Поэтому, даже если AVX2 доступен, я думаю, что инструкции AVX1 здесь более эффективны. На Zen1 vbroadcastf128 это 2 uops против 1 для 128-битного vmovups , но vpermps (пересечение полосы) составляет 3 uops против 2 для vpermilps .

К сожалению, clang пессимизирует это в vmovups нагрузку и a vpermps ymm , но GCC компилирует его так, как написано. (Божья молния)


Если вы хотите избежать использования векторной константы управления перетасовкой vpmovzxdq ymm, [mem] (2 uops на Intel), можно настроить элементы для vmovsldup (1 uops в режиме перетасовки). Или транслировать-загружать, а vunpckl/hps затем смешивать?


Я знаю, что использование _mm256_set_ps() всегда возможно, но это кажется медленным с 8 инструкциями процессора.

Тогда найдите лучший компилятор! (Или не забудьте включить оптимизацию.)

 __m256  duplicate4floats_naive(const float *p) {
   return _mm256_set_ps(p[3],p[3], p[2], p[2], p[1],p[1], p[0],p[0]);
}
 

компилируется с помощью gcc (https://godbolt.org/z/dMzh3fezE) в

 duplicate4floats_naive(float const*):
        vmovups xmm1, XMMWORD PTR [rdi]
        vpermilps       xmm0, xmm1, 80
        vpermilps       xmm1, xmm1, 250
        vinsertf128     ymm0, ymm0, xmm1, 0x1
        ret
 

Так что 3 тасовки, не очень хорошо. И его можно было бы использовать vshufps вместо vpermilps того, чтобы экономить размер кода и позволять ему работать на большем количестве портов на Ледяном озере. Но все равно значительно лучше, чем 8 инструкций.

оптимизатор перемешивания clang делает то же самое, что и с моими оптимизированными внутренними функциями, потому что именно таков clang. Это довольно приличная оптимизация, просто не совсем оптимальная.

 duplicate4floats_naive(float const*):
        vmovups xmm0, xmmword ptr [rdi]
        vmovaps ymm1, ymmword ptr [rip   .LCPI1_0] # ymm1 = [0,0,1,1,2,2,3,3]
        vpermps ymm0, ymm1, ymm0
        ret
 

Комментарии:

1. Если доступ к соседним элементам p[-2], ..., p[5] является сохраненным, можно также загрузить этот вектор вместо широковещательной передачи и выполнить перетасовку в полосе.

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

3. @Noob: Да, я бы рекомендовал написать оптимальные встроенные функции, как только вы потратите время на то, чтобы понять, как это будет выглядеть, если вы хотите, чтобы ваш код хорошо компилировался с несколькими компиляторами. (например, посмотрев на вывод clang _mm256_setr_ps и превратив его обратно в встроенные функции.) Но использование хорошего компилятора, такого как clang, означает, что вы можете получить хорошие результаты с гораздо меньшим количеством работы, и компилятор часто найдет лучшие способы сделать что-то, когда/если вы сами не придумаете лучший способ. Основной смысл этого ответа заключается в первом блоке кода, использующем _mm256_broadcast_ps и _mm256_permutevar_ps

4. @чтз Я вижу, что загрузка, похоже, имеет меньшую задержку, чем трансляция. Или загрузите все,что содержит A,B,C, D, например, p[0]~p[7], если не произойдет нарушения доступа к памяти.

5. @чтз и Нуб: Но смотри также agner.org/optimize/blog/read.php?i=872amp;v=f#854 — 128-битная нагрузка с нулевым расширением, потребляемая 256-битной векторной операцией, также имеет дополнительную задержку. Так что, вероятно, на самом деле это 7 1 против 7 3 циклов. А 256-битная vmovups ymm, [mem] загрузка составляет 7 циклов. (Или, что еще хуже, при разделении строк кэша, поэтому на процессорах с дешевыми vbroadcastf128 процессорами лучше всего просто это сделать.)

Ответ №2:

_mm_load_ps -> _mm256_castps128_ps256 ->> _mm256_permute_ps

Комментарии:

1. _mm256_permute_ps является внутренним для vpermilps , потому что Intel недальновидно называла свои внутренние компоненты, используя «перестановку» для этой инструкции AVX1, а затем пришлось использовать _mm256_permutexvar_ps aka _mm256_permutevar8x32_ps для vpermps того, чтобы однажды появился AVX2. Но да, это наиболее эффективный способ, если у вас есть AVX2, особенно если вы можете загрузить вектор управления перемешиванием только один.

2. В противном случае с AVX1, я думаю VBROADCASTF128 / _mm_permutevar_ps (vpermilps ymm, ymm, ymm с векторным управлением). На самом деле да, это лучше, потому что широковещательная загрузка так же дешева, как и обычная загрузка, а перетасовка в полосе движения обеспечивает меньшую задержку и быстрее на Zen 1.

3. Я все еще не понимаю, как работает permute после прочтения документов Intels. Но эта ссылка ниже действительно помогает. codeproject.com/Articles/874396/…

4.@Noob: на самом деле это _mm256_permute_ps не работает для этого; у этого ответа была правильная идея, но он использовал неправильный внутренний для vpermps . __m256 _mm256_permute_ps (__m256 a, int control); является внутренним для vpermilps с непосредственным управляющим операндом (одинаковое перемешивание в каждой 128-битной полосе) ( felixcloutier.com/x86/vpermilps).

5. @PeterCordes ты прав! _mm256_permutevar_ps должен выполнить эту работу