SIMD: накопление соседних пар - PullRequest
2 голосов
/ 08 марта 2019

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

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

template <typename T, typename U>
inline void accumulate_2x2_x_pass(
  T* channel, U* accum,
  const size_t sx, const size_t sy, 
  const size_t osx, const size_t osy,
  const size_t yoff, const size_t oyoff
) {

  const bool odd_x = (sx & 0x01);

  size_t i_idx, o_idx;

  // Should be vectorizable somehow...
  for (size_t x = 0, ox = 0; x < sx - (size_t)odd_x; x += 2, ox++) {
    i_idx = x + yoff;
    o_idx = ox + oyoff;
    accum[o_idx] += channel[i_idx];
    accum[o_idx] += channel[i_idx + 1];
  }

  if (odd_x) {
    // << 1 bc we need to multiply by two on the edge 
    // to avoid darkening during render
    accum[(osx - 1) + oyoff] += (U)(channel[(sx - 1) + yoff]) * 2;
  }
}

Тем не менее, Godbolt показывает, что мой цикл не является autovectorizable. (https://godbolt.org/z/qZxvof) Как мне построить встроенные функции SIMD для решения этой проблемы? У меня есть контроль выравнивания для аккумулятора, но не для канала.

(я знаю, что есть средняя присущая, но здесь она не подходит, потому что мне нужно сгенерировать несколько уровней mip, и эта команда приведет к потере точности для следующего уровня.)

Спасибо всем. :)

1 Ответ

3 голосов
/ 08 марта 2019

Расширяющий регистр с узким типом T = uint8_t или uint16_t, вероятно, лучше всего реализовать с SSSE3 pmaddubsw или SSE2 pmaddwd с множителем 1. ( Руководство по встроенным функциям ) Эти инструкции используются по одному и точно расширяют горизонтальное пространство, добавляя, что вам нужно более эффективно, чем перетасовка.

Если вы можете сделать это без потери точности, сделайте вертикальное сложение между строками сначала , прежде чем расширять горизонтальное, добавьте . (например, 10, 12 или 14-битные пиксельные компоненты в [u]int16_t не могут переполниться). Загрузка и вертикальное добавление имеют (по крайней мере) 2 на тактовую частоту на большинстве процессоров, по сравнению с 1 на такт для pmadd*, имеющей только 2 тактовых такта на Skylake и более поздних версиях. И это означает, что вам нужно только 1x add + 1x pmadd против 2x pmadd + 1x add, так что это значительный выигрыш даже на Skylake. (Для 2-го способа обе загрузки могут сложиться в операнды памяти для pmadd, если у вас есть AVX. Для способа добавления перед pmadd сначала вам понадобится чистая загрузка, а затем сложите вторую загрузку в add, чтобы вы не могли сохранить входные мопы, если не используете режимы индексированной адресации и они не ламинируются. ) * * тысяча двадцать-один

И в идеале вам не нужно += в массив аккумуляторов, и вместо этого вы можете просто читать 2 строки параллельно, и аккумулятор предназначен только для записи, поэтому ваш цикл имеет только 2 входных потока и 1 выходной поток.

// SSSE3
__m128i hadd_widen8_to_16(__m128i a) {
                      // uint8_t, int8_t  (doesn't matter when multiplier is +1)
    return _mm_maddubs_epi16(a, _mm_set_epi8(1));
}

// SSE2
__m128i hadd_widen16_to_32(__m128i a) {
                   // int16_t, int16_t
    return _mm_madd_epi16(a, _mm_set_epi16(1));
}

Эти порты напрямую подключаются к 256-битному AVX2, поскольку ширина входа и выхода одинакова. Перестановка не требуется для исправления набивки на линии.

Да, действительно, они оба _epi16. Intel может быть дико несовместимым с собственными именами. мнемоника asm более последовательна и легче запомнить что к чему. (ubsw = байт без знака для подписанного слова, за исключением того, что один из входных данных является байтом со знаком. pmaddwd упакован, умножить, добавить слово к dword, такая же схема именования, как у punpcklwd и т. Д.)


Случай T = U с uint16_t или uint32_t является вариантом использования для SSSE3 _mm_hadd_epi16 или _mm_hadd_epi32. Это стоит столько же, сколько 2 шаффла + вертикальное добавление, но в любом случае вам нужно упаковать 2 входа в 1.

Если вы хотите обойти узкое место в случайном порте в Haswell и более поздних версиях, вы можете рассмотреть возможность использования сдвигов qword на входах и затем перемешать результат с shufps (_mm_shuffle_ps + некоторое приведение). Возможно, это может быть победой на Skylake (с 2-мя пропускными способностями на смену тактового генератора), даже несмотря на то, что он будет стоить 5 полных мопов вместо 3-х. Он может работать в лучшем случае 5/3 цикла на вектор выхода вместо 2 циклов на вектор, если есть нет узкого места переднего конца

// UNTESTED

//Only any good with AVX, otherwise the extra movdqa instructions kill this
//Only worth considering for Skylake, not Haswell (1/c shifts) or Sandybridge (2/c shuffle)
__m128i hadd32_emulated(__m128i a, __m128i b) {
    __m128i a_shift = _mm_srli_epi64(a, 32);
    __m128i b_shift = _mm_srli_epi64(b, 32);
    a = _mm_add_epi32(a, a_shift);
    b = _mm_add_epi32(b, b_shift);
    __m128 combined = _mm_shuffle_ps(_mm_castsi128_ps(a), _mm_castsi128_ps(b), _MM_SHUFFLE(2,0,2,0));
    return _mm_castps_si128(combined);
}

Для версии AVX2 вам понадобится перестановка переулка, чтобы исправить результат vphadd. Так что эмуляция хэда со сменами может быть более выигрышной.

// 3x shuffle 1x add uops
__m256i hadd32_avx2(__m256i a, __m256i b) {
    __m256i hadd = _mm256_hadd_epi32(a, b);  // 2x in-lane hadd
    return _mm256_permutex_epi64( hadd, _MM_SHUFFLE(3,1,2,0) );
}

// UNTESTED
// 2x shift, 2x add, 1x blend-immediate (any ALU port), 1x shuffle
__m256i hadd32_emulated_avx2(__m256i a, __m256i b)
{
        __m256i a_shift = _mm256_srli_epi64(a, 32);  // useful result in the low half of each qword
        __m256i b_shift = _mm256_slli_epi64(b, 32);  // ... high half of each qword
        a = _mm256_add_epi32(a, a_shift);
        b = _mm256_add_epi32(b, b_shift);
        __m256i blended = _mm256_blend_epi32(a,b, 0b10101010);  // alternating low/high results
        return _mm256_permutexvar_epi32(_mm256_set_epi32(7,5,3,1, 6,4,2,0),  blended);
}

На Haswell и Skylake hadd32_emulated_avx2 может работать с частотой 1 на 2 такта (насыщая все векторные порты ALU). Дополнительные add_epi32 для суммирования в accum[] замедляют его до лучших 7/3 циклов на 256-битный вектор результатов, и вам нужно будет развернуть (или использовать компилятор, который развертывает), чтобы не просто узкое место на внешний интерфейс.

hadd32_avx2 может работать с частотой 1 на 3 такта (узкое место на порту 5 для перемешивания). Load + store + extra add_epi32 мопов для реализации вашего цикла может легко работать в тени.

(https://agner.org/optimize/, и см. https://stackoverflow.com/tags/x86/info)

...