Используя инструкции AVX512, я могу использовать индексный вектор для сбора 16 значений одинарной точности из массива. Однако такие операции сбора не так эффективны и на моей машине выполняются со скоростью всего 2 скалярных загрузки/цикл.
В одном из моих приложений мне всегда нужно собрать четыре смежных float
элемента. В скалярном псевдокоде:
for (int i = 0; i < 16; ++i) {
result.x[i] = source[offset[i]*4 + 0];
result.y[i] = source[offset[i]*4 + 1];
result.z[i] = source[offset[i]*4 + 2];
result.w[i] = source[offset[i]*4 + 3];
}
Графические процессоры NVIDIA могут делать что-то с помощью одной инструкции ld.global.v4.f32
. Что касается процессора, то также кажется, что можно использовать эту смежность, чтобы получить результат лучше, чем 4 сейсмограммы шириной 16. Кто-нибудь здесь знает более быструю последовательность инструкций AVX512, которая улучшила бы наивную стратегию? Можно предположить произвольное выравнивание.
Сборщики на процессорах Intel и AMD не могут использовать преимущества соседства исходных элементов друг с другом; они обращаются к кешу отдельно для каждого скалярного элемента. (https://uops.info/ - обратите внимание, что несмотря на то, что количество мопов портов 2/3 на последних моделях Intel низкое, пропускная способность соответствует узкому месту чтения кэша 2/такт или 3/такт).
Кроме того, на Intel от Skylake до Tiger Lake пропускная способность инструкций теперь является мусором из-за смягчения микрокода для GDS (https://downfall.page/#faq). А сборка AMD никогда не была очень быстрой.
Как говорит chtz, вам следует вручную выполнять 128-битную загрузку SIMD, поскольку ваша схема доступа проста.
Но тогда я думаю, вам нужно перетасовать 4 таких вектора, чтобы они были x[i + 0..3]
смежными для 128-битного хранилища и так далее, поскольку вы распределяете результаты в выходные данные Struct-of-Arrays.
Вы можете создать две пары vmovups
XMM / vinsertf128
YMM, m128, а затем перемешать их вместе с помощью vpermt2ps
, чтобы получить один 512-битный вектор со всеми четырьмя смежными значениями x
, затем со всеми четырьмя смежными значениями y
и т. д. (В C++ с внутренними функциями используйте _mm512_castps256_ps512
чтобы переосмыслить __m256
как __m512
, верхняя половина которого представляет собой неопределенный мусор.)
Это устанавливает vextractf32x4
в память для двух старших, vextractf128
для дорожки 1 и vmovups
для дорожки 0. Надеюсь, компилятор оптимизирует таким образом, если вы сделаете 4x _mm_store_ps(&result.x[i], _mm512_extractf32x4_ps(v, 0) )
и т. д., но вы можете использовать вручную
__m512 v = _mm512_permutex2var_ps(_mm512_castps256_ps512(first_2_loads),
_mm512_castps256_ps512(second_2_loads),
shuffle_constant);
_mm_store_ps(&result.x[i], _mm512_castps512_ps128(v) ); // vmovups
_mm_store_ps(&result.y[i], _mm256_extractf128_ps( _mm512_castps512_ps256(v), 1) ); // vextractf128 mem, ymm, 1
_mm_store_ps(&result.z[i], _mm512_extractf32x4_ps(v, 2) ); // vextractf32x4 mem, zmm, 2
_mm_store_ps(&result.w[i], _mm512_extractf32x4_ps(v, 3) );
Или избегайте 512-битных векторов и делайте два отдельных 256-битных вектора vpermt2ps
, , если остальная часть вашего кода не использует 512-битные векторы .
(Если бы вы делали больше итераций, вы могли бы перетасовать по-другому, чтобы настроить еще более широкие магазины, например, 256 или, может быть, даже 512, если стоит так сильно перетасовать.)
У AVX-512 есть инструкции разброса, но они неэффективны даже на Intel и намного хуже на AMD Zen 4. Вы можете просто vinsertf128 / vinsert32x4 и т. д. и выполнить на их основе разброс со смещениями, которые включают расстояние между началами x
и y
векторы, предполагая, что они распределены в пределах 2 ГБ друг от друга, поэтому может достигать 32-битного смещения. Но это было бы намного медленнее, я думаю, что даже не получится получить одно 32-битное хранилище за такт (или 2 на Ice Lake и более поздних версиях). Но фиксация из буфера хранилища в L1d составляет только 1 за такт, если только не используются два последовательных хранилища. в одну и ту же строку кэша, тогда они смогут объединиться https://travisdowns.github.io/blog/2019/06/11/speed-limits.html#memory-related-limits)
Спасибо за подробный ответ! Я не знал, что сборки сейчас микрокодируются 🤯. Есть ли у вас информация о пропускной способности/задержке этой операции после этого «улучшения»?
@WenzelJakob: phoronix.com/review/intel-downfall-benchmarks содержит некоторые (немикро) тесты (целых приложений, в которых есть некоторые функции, использующие сборку). В нем также говорится, что Intel сообщает о замедлении работы до 50% в «крайних случаях». Я не думаю, что uops.info был перезапущен с обновленным микрокодом. (Кстати, я не говорил, что сама инструкция имеет микрокод, хотя это было бы так, если бы она теперь декодировалась в какие-либо дополнительные мопы. Это 4 на SKL без смягчения микрокода. Обновления «микрокода» Intel могут изменить вещи, кроме ПЗУ ucode, например, отключение буфера цикла в Skylake.)
Но хороший вопрос, кто-то должен его задать, каковы именно детали производительности сборок сейчас на затронутых процессорах с включенным смягчением.
В моем приложении все делается регистрами AVX512. В итоге я выполнил 16 выровненных 4 x float32
загрузок, которые я добавлял друг к другу, чтобы сформировать 4 регистра ZMM. Далее я использую последовательность unpacklo
/unpackhi
/movehl
/movelh
, которую обычно используют для транспонирования матрицы, хранящейся в регистрах 4xXMM. Эти инструкции также существуют в вариантах ZMM, где они работают с блоками шириной 4, что правильно. Вам это кажется разумным? Есть ли преимущество у упомянутой вами стратегии vpermt2ps
?
@WenzelJakob: Я думаю, что запутался с циклом i < 16
, связанным с вопросом, и подумал, что он обрабатывает всего 16 чисел с плавающей запятой. Но на самом деле это один фрагмент из 16 x
чисел с плавающей точкой, 16 y
чисел с плавающей запятой и т. д., так что вы можете перетасовать 512-битные хранилища, как я предложил. То, что вы делаете, звучит разумно.
Я бы, наверное, просто загрузил 4
__m128
вектора и объединил их с помощью инструкций вставки. Вы также можете попробовать инструкциюvgatherdpd
(с некоторым приведением типов) — она должна быть примерно в два раза быстрее эквивалентнойvgatherdps
.