Загрузка и перемещение восьми 8-элементных векторов с плавающей запятой - PullRequest
1 голос
/ 10 февраля 2020

В одном из жестких l oop, выполняющих алгоритм DSP, мне нужно загрузить восемь 8-элементных векторов с плавающей запятой с указателем базовых данных и смещениями в целочисленном регистре AVX2. Мой самый быстрый код выглядит следующим образом:

void LoadTransposed(
    const float* data, __m256i offsets,
    __m256& v0, __m256& v1, __m256& v2, __m256& v3, __m256& v4, __m256& v5, __m256& v6, __m256& v7)
{
    const __m128i offsetsLo = _mm256_castsi256_si128(offsets);
    const __m128i offsetsHi = _mm256_extracti128_si256(offsets, 1);
    __m256 a0 = _mm256_loadu_ps(data + (uint32)_mm_cvtsi128_si32(offsetsLo   ));
    __m256 a1 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 1));
    __m256 a2 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 2));
    __m256 a3 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsLo, 3));
    __m256 a4 = _mm256_loadu_ps(data + (uint32)_mm_cvtsi128_si32(offsetsHi   ));
    __m256 a5 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 1));
    __m256 a6 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 2));
    __m256 a7 = _mm256_loadu_ps(data + (uint32)_mm_extract_epi32(offsetsHi, 3));

    // transpose
    const __m256 t0 = _mm256_unpacklo_ps(a0, a1);
    const __m256 t1 = _mm256_unpackhi_ps(a0, a1);
    const __m256 t2 = _mm256_unpacklo_ps(a2, a3);
    const __m256 t3 = _mm256_unpackhi_ps(a2, a3);
    const __m256 t4 = _mm256_unpacklo_ps(a4, a5);
    const __m256 t5 = _mm256_unpackhi_ps(a4, a5);
    const __m256 t6 = _mm256_unpacklo_ps(a6, a7);
    const __m256 t7 = _mm256_unpackhi_ps(a6, a7);
    __m256 v = _mm256_shuffle_ps(t0, t2, 0x4E);
    const __m256 tt0 = _mm256_blend_ps(t0, v, 0xCC);
    const __m256 tt1 = _mm256_blend_ps(t2, v, 0x33);
    v = _mm256_shuffle_ps(t1, t3, 0x4E);
    const __m256 tt2 = _mm256_blend_ps(t1, v, 0xCC);
    const __m256 tt3 = _mm256_blend_ps(t3, v, 0x33);
    v = _mm256_shuffle_ps(t4, t6, 0x4E);
    const __m256 tt4 = _mm256_blend_ps(t4, v, 0xCC);
    const __m256 tt5 = _mm256_blend_ps(t6, v, 0x33);
    v = _mm256_shuffle_ps(t5, t7, 0x4E);
    const __m256 tt6 = _mm256_blend_ps(t5, v, 0xCC);
    const __m256 tt7 = _mm256_blend_ps(t7, v, 0x33);
    v0 = _mm256_permute2f128_ps(tt0, tt4, 0x20);
    v1 = _mm256_permute2f128_ps(tt1, tt5, 0x20);
    v2 = _mm256_permute2f128_ps(tt2, tt6, 0x20);
    v3 = _mm256_permute2f128_ps(tt3, tt7, 0x20);
    v4 = _mm256_permute2f128_ps(tt0, tt4, 0x31);
    v5 = _mm256_permute2f128_ps(tt1, tt5, 0x31);
    v6 = _mm256_permute2f128_ps(tt2, tt6, 0x31);
    v7 = _mm256_permute2f128_ps(tt3, tt7, 0x31);
}

Как вы можете видеть, я уже использую смеси вместо тасов, чтобы уменьшить давление на порту 5. Я также выбрал _mm_cvtsi128_si32 при загрузке извлечения 1-го векторного элемента, который составляет всего 1 моп вместо 2 моп в случае незаметного _mm_extract_epi32. Кроме того, извлечение нижней и верхней линий вручную, похоже, немного помогает компилятору и удаляет лишние vextracti128 инструкции.

Я пробовал эквивалентный код с использованием инструкций сборки, которые, как и предполагалось, оказались в 2 раза медленнее, поскольку он эффективно выполняет 64 загрузки под капотом:

void LoadTransposed_Gather(
    const float* data, __m256i offsets,
    __m256& v0, __m256& v1, __m256& v2, __m256& v3, __m256& v4, __m256& v5, __m256& v6, __m256& v7)
{
    v0 = _mm256_i32gather_ps(data + 0, offsets, 4);
    v1 = _mm256_i32gather_ps(data + 1, offsets, 4);
    v2 = _mm256_i32gather_ps(data + 2, offsets, 4);
    v3 = _mm256_i32gather_ps(data + 3, offsets, 4);
    v4 = _mm256_i32gather_ps(data + 4, offsets, 4);
    v5 = _mm256_i32gather_ps(data + 5, offsets, 4);
    v6 = _mm256_i32gather_ps(data + 6, offsets, 4);
    v7 = _mm256_i32gather_ps(data + 7, offsets, 4);
}

Есть ли способ ускорить этот (прежний фрагмент) еще дальше? Согласно VTune и IACA, самым большим нарушителем является высокое давление на портах 0 и 5 (вероятно, из-за vpextrd, используемого во время извлечения смещения из регистров __m128i, и всех vunpckhps, vunpcklps и vshufps, используемых во время транспонирования).

1 Ответ

2 голосов
/ 11 февраля 2020

Есть ли у ваших смещений шаблон, как фиксированный шаг, который вы могли бы просто масштабировать?

Если нет, возможно, передайте их как структуру вместо __m256i, если вам просто нужно в любом случае, чтобы извлечь их?

Или, если вы используете SIMD для вычисления смещений (поэтому они, естественно, изначально находятся в __m256i): сохранение / перезагрузка в локальный массив Когда вам нужно, чтобы все 8 элементов сэкономили бы пропускную способность порта shuffle . Возможно _mm_cvtsi128_si32 / _mm_extract_epi32(offsetsLo, 1)), чтобы получить первые 1 или 2 смещения с помощью операций ALU, с задержкой в ​​пару циклов, чем в store -> reload store forwarding.

например alignas(32) uint32_t offsets[8]; и _mm256_store_si256 в него , (С некоторыми компиляторами вам, возможно, придется помешать ему «оптимизировать» это в экстракты ALU. Вы можете использовать volatile в массиве в качестве неприятного хака, чтобы обойти это. (Но будьте осторожны, чтобы не победить оптимизацию больше, чем необходимо, например, загружать в tmp vars вместо того, чтобы обращаться к энергозависимому массиву несколько раз, если вы хотите, чтобы каждый элемент был более одного раза, это всегда будет препятствовать постоянному распространению, поскольку FP будет побеждать такие вещи, как использование нижнего элемента вектора как скаляр без необходимости перемешивания.)


2 / пропускная способность тактовой нагрузки, а эффективная пересылка хранилища из векторного хранилища в скалярные перезагрузки 32-битных элементов делает это хорошим (возможно, задержка 7 циклов IIR C, для 256-битного хранилища).

Особенно, если вы выполняете эту транспонирование в al oop с другими ALU, работающими над результатом транспонирования, поэтому l oop в основном узкие места на порте В фоновом режиме 5. Дополнительные загрузочные операции не должны быть узким местом на портах загрузки, особенно если есть какие-либо пропуски кэша L1d. (В этом случае повторно проигрывает дополнительные циклы на портах для инструкций, которые потребляют результатов загрузки, а не самих загрузочных мопов).

Также меньше внешних мопов:

  • 1 store (p237 + p4 с микроплавлением) + 1 vmovd (p0) + 7 загрузок (p23) - это всего 9 операций переднего плана (слитый домен)
  • против. vextracti128 + 2x vmovd + 6x vpextrd = 15 ALU Uops для порта 0 и порта 5

Сохранение / перезагрузка также подходит для Zen / Zen2.

IceLake имеет большую пропускную способность ALU shuffle (некоторые векторные тасовки могут работать на другом порту, а также на p5), но сохранение / перезагрузка все еще является хорошей стратегией, когда вам нужны все элементы, а их 8. Специально для пропускной способности при небольших затратах времени ожидания.



@ Witek902 сообщает (в комментариях), что предложение @ chtz из строит транспонирование из vmovups xmm + vinsertf128 уменьшает узкое место пропускной способности порта 5 в HSW / SKL и на практике ускоряет . vinsertf128 y,y,mem,i - это 2 мопа (не может быть микроплавким предохранителем) для p015 + p23 на Intel. Так что это больше похоже на смесь, не нуждающуюся в случайном порте. (Это также будет отлично для семейства Bulldozer / Zen1, которые обрабатывают регистры YMM как две 128-битные половины.)

Выполнение только 128-битной загрузки также хорошо для Sandybridge / IvyBridge, где смещение 256 бит нагрузки очень дорогие.

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

...