лучший способ перетасовать по переулкам AVX? - PullRequest
0 голосов
/ 25 октября 2018

Есть вопросы с похожими названиями, но мой вопрос касается одного очень специфического случая использования, не описанного где-либо еще.

У меня есть 4 __128d регистров (x0, x1, x2, x3), и я хочу рекомбинировать ихпри подготовке других вычислений содержимое в 5 __256d регистрах (y0, y1, y2, y3, y4) выглядит следующим образом:

on entry:
    x0 contains {a0, a1}
    x1 contains {a2, a3}
    x2 contains {a4, a5}
    x3 contains {a6, a7}
on exit:
    y0 contains {a0, a1, a2, a3}
    y1 contains {a1, a2, a3, a4}
    y2 contains {a2, a3, a4, a5}
    y3 contains {a3, a4, a5, a6}
    y4 contains {a4, a5, a6, a7}

Моя реализация здесь довольно медленная.Есть ли лучший способ?

y0 = _mm256_set_m128d(x1, x0);

__m128d lo = _mm_shuffle_pd(x0, x1, 1);
__m128d hi = _mm_shuffle_pd(x1, x2, 1);
y1 = _mm256_set_m128d(hi, lo);

y2 = _mm256_set_m128d(x2, x1);

lo = hi;
hi = _mm_shuffle_pd(x2, x3, 1);
y3 = _mm256_set_m128d(hi, lo);

y4 = _mm256_set_m128d(x3, x2);

1 Ответ

0 голосов
/ 25 октября 2018

Со входами в регистрах вы можете сделать это в 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 байта, этого не произойдет.

...