Со входами в регистрах вы можете сделать это в 5 инструкциях тасования:
- 3x
vinsertf128
для создания y0, y2 и y4 путем объединения 2 регистров xmm каждый. - 2x
vshufpd
(тасование в пределах полосы) между этими результатами для создания y1 и y3.
Обратите внимание, что нижние полосы y0 и y2 содержат a1 и a2, элементы, необходимые для нижней полосыиз y1.И та же самая случайность также работает для верхней полосы.
#include <immintrin.h>
void merge(__m128d x0, __m128d x1, __m128d x2, __m128d x3,
__m256d *__restrict y0, __m256d *__restrict y1,
__m256d *__restrict y2, __m256d *__restrict y3, __m256d *__restrict y4)
{
*y0 = _mm256_set_m128d(x1, x0);
*y2 = _mm256_set_m128d(x2, x1);
*y4 = _mm256_set_m128d(x3, x2);
// take the high element from the first vector, low element from the 2nd.
*y1 = _mm256_shuffle_pd(*y0, *y2, 0b0101);
*y3 = _mm256_shuffle_pd(*y2, *y4, 0b0101);
}
Компилируется довольно хорошо ( с gcc и clang -O3 -march=haswell
на Godbolt ), чтобы:
merge(double __vector(2), double __vector(2), double __vector(2), double __vector(2), double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*):
vinsertf128 ymm0, ymm0, xmm1, 0x1
vinsertf128 ymm3, ymm2, xmm3, 0x1
vinsertf128 ymm1, ymm1, xmm2, 0x1
# vmovapd YMMWORD PTR [rdi], ymm0
vshufpd ymm0, ymm0, ymm1, 5
# vmovapd YMMWORD PTR [rdx], ymm1
vshufpd ymm1, ymm1, ymm3, 5
# vmovapd YMMWORD PTR [r8], ymm3
# vmovapd YMMWORD PTR [rsi], ymm0
# vmovapd YMMWORD PTR [rcx], ymm1
# vzeroupper
# ret
Я прокомментировал магазины и прочее, что могло бы уйти при вставке, так что у нас действительно есть только 5 инструкций случайного перемешивания против 9 инструкций случайного перемешивания для кода в вашем вопросе.(Также включено в ссылку на проводник компилятора Godbolt).
Это очень хорошо для AMD, где vinsertf128
очень дешево (потому что 256-битные регистры реализованы как 2x 128-битов пополам, так что это всего лишь 128-битная копия без необходимости специального порта для перемешивания.) 256-битные тасовки с пересечением полос медленны на AMD, но 256-битные тасовки внутри строк, такие как vshufpd
, составляют всего 2 мопа.
На Intel это довольно хорошо, но основные процессоры Intel с AVX имеют только 1 пропускную способность на тактовую частоту для 256-битных или FP-перемешиваний.(Sandybridge и более ранние версии имеют более высокую пропускную способность для целочисленных 128-битных перемешиваний, но процессоры AVX2 отбросили дополнительные блоки перемешивания, и они все равно не помогли.)
Таким образом, процессоры Intel не могут использовать инструкциюуровень параллелизма вообще, но всего 5 моп, что приятно.Это минимально возможный результат, потому что вам нужно 5 результатов.
Но особенно если окружающий код также является узким местом на шаффлах, стоит рассмотреть стратегию сохранения / перезагрузки всего с 4 магазинами и 5 перекрывающимися векторами.грузы .Или, может быть, 2x vinsertf128
для построения y0
и y4
, затем 2x 256-битных хранилищ + 3 перезагружаемых перезагрузки.Это может привести к тому, что exec-of-order exec начнет работать с зависимыми инструкциями, используя только y0
или y4
, в то время как остановка пересылки хранилища разрешена для y1..3.
Особенно, если вас это не волнуето Intel Sandybridge первого поколения, где невыровненные 256-битные векторные нагрузки менее эффективны.(Обратите внимание, что вы захотите скомпилировать с помощью gcc -mtune=haswell
, чтобы отключить настройку -mavx256-split-unaligned-load
default / sandybridge, если вы используете GCC. Независимо от компилятора, -march=native
- хорошая идея, если вы запускаете двоичные файлы для запускамашина, на которой вы его компилируете, чтобы в полной мере воспользоваться наборами команд и установить параметры настройки.)
Но если общая пропускная способность UOP от внешнего интерфейса больше, чем узкое место, тогда реализация в случайном порядке является наилучшей.
(См. https://agner.org/optimize/ и другие ссылки на производительность в x86 wiki для получения дополнительной информации о настройке производительности. Также Какие соображения относятся к прогнозированию задержки для операций на современном суперскалярномпроцессоры и как их вычислить вручную? , но на самом деле руководство Агнера Фога - это более подробное руководство, объясняющее, что на самом деле означает пропускная способность в сравнении с задержкой.)
Мне даже не нужно сохранять, так как данные также уже доступны в непрерывной памяти.
Затем просто загрузите их с 5 перекрывающимисяреклама почти наверняка самая эффективная вещь, которую вы можете сделать.
Haswell может делать 2 загрузки в такт от L1d или меньше, когда любой пересекает границу строки кэша. Так что, если вы можете выровнять свой блок на 64, он будет совершенно эффективен без разделения строк кэша. Промежуток в кэше происходит медленно, но перезагрузка горячих данных из кэша L1d очень дешева, а современные процессоры с AVXПоддержка обычно имеет эффективную поддержку unaligned-load.
(Как я уже говорил ранее, если вы используете gcc, убедитесь, что вы компилируете с -march=haswell
или -mtune=haswell
, а не только с -mavx
, чтобы избежать использования gcc -mavx256-split-unaligned-load
.)
4 нагрузки + 1 vshufpd
(y0, y2) может быть хорошим способом сбалансировать давление в порту нагрузки с давлением ALU, в зависимости от узких мест в окружающем коде.Или даже 3 загрузки + 2 перемешивания, если в окружающем коде мало давления в порту перемешивания.
они находятся в регистрах предыдущих вычислений, которые требовали их загрузки.
Если в этом предыдущем вычислении исходные данные все еще содержались в регистрах, вы могли бы в первую очередь выполнить 256-битную загрузку и просто использовать их младшие 128-битные половинки для более раннего вычисления. (AnРегистр XMM - это младшие 128 соответствующих регистров YMM, и чтение их не нарушает верхние полосы, поэтому _mm256_castpd256_pd128
компилируется с нулевыми инструкциями asm.)
Делает 256-битные загрузки для y0, y2,и y4, и используйте их нижние половины как x0, x1 и x2.(Создайте y1 и y3 позже с невыровненными нагрузками или тасовками).
Только x3 - это не младшие 128 бит 256-битного вектора, который вам тоже нужен.
В идеале компилятор ужеобратите внимание на эту оптимизацию, когда вы делаете _mm_loadu_pd
и _mm256_loadu_pd
с одного и того же адреса, но, вероятно, вам нужно удерживать его вручную, выполнив
__m256d y0 = _mm256_loadu_pd(base);
__m128d x0 = _mm256_castpd256_pd128(y0);
и т. д., и либо извлечь встроенный ALU(_mm256_extractf128_pd
) или 128-битная загрузка для x3
, в зависимости от окружающего кода.Если это необходимо только один раз, то его можно сложить в операнд памяти для любой используемой инструкции.
Потенциальный недостаток: немного более высокая задержка перед началом 128-битного вычисления или несколько циклов, если 256-разрядныйбитовые нагрузки были пересечением строк кэша, где не было 128-битных.Но если ваш блок данных выровнен на 64 байта, этого не произойдет.