Можно ли использовать SIMD для последовательной зависимости в вычислениях, как экспоненциальный фильтр скользящего среднего? - PullRequest
0 голосов
/ 16 декабря 2018

Я обрабатываю несколько (независимых) Экспоненциальное скользящее среднее 1-полюсный фильтры по различным параметрам, которые есть в моем приложении Audio, с целью сглаживания каждого значения параметра в аудиоОценить:

for (int i = 0; i < mParams.GetSize(); i++) {
    mParams.Get(i)->SmoothBlock(blockSize);
}

...

inline void SmoothBlock(int blockSize) {
    double inputA0 = mValue * a0;

    for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
        mSmoothedValues[sampleIndex] = z1 = inputA0 + z1 * b1;
    }
}   

Я бы хотел воспользоваться инструкциями CPU SIMD, обрабатывая их параллельно, но я не совсем уверен, как мне этого добиться.

Фактически, z1 является рекурсивным: не может "упаковать" массив значений типа double с учетом "предыдущих значений", верно?

Может быть, есть способ правильно организовать данные различных фильтрови обрабатывать их параллельно?

Любые советы или предложения приветствуются!

Обратите внимание: у меня нет нескольких путей прохождения сигналов.Любые параметры представляют различные элементы управления для (уникального) сигнала обработки.Скажем, у меня есть сигнал греха: параметр 1 будет влиять на усиление, уровень 2, высоту фильтра 3, панорамирование 4 и так далее.

Ответы [ 2 ]

0 голосов
/ 16 декабря 2018

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

Как и в этом случае, z1 = c + z1 * b, поэтому, применив это дважды, мы получим

# I'm using Z0..n as the elements in the series your loop calculates
Z2 = c + (c+Z0*b)*b
   = c + c*b + Z0*b^2

c + c*b и b^2 являются константами, если я правильно понимаю ваш код, что все переменные C на самом деле являются просто переменными C, а не псевдокодом для ссылки на массив.(Таким образом, все, кроме вашего z1, является инвариантом цикла).


Поэтому, если у нас есть вектор SIMD из 2 элементов, начиная с Z0 и Z1, мы можем выполнить шаг каждый изих вперед на 2, чтобы получить Z2 и Z3.

void SmoothBlock(int blockSize, double b, double c, double z_init) {

    // z1 = inputA0 + z1 * b1;
    __m128d zv = _mm_setr_pd(z_init, z_init*b + c);

    __m128d step2_mul = _mm_set1_pd(b*b);
    __m128d step2_add = _mm_set1_pd(c + c*b);

    for (int i = 0; i < blockSize-1; i+=2) {
        _mm_storeu_pd(mSmoothedValues + i, zv);
        zv = _mm_mul_pd(zv, step2_mul);
        zv = _mm_add_pd(zv, step2_add);
       // compile with FMA + fast-math for the compiler to fold the mul/add into one FMA
    }
    // handle final odd element if necessary
    if(blockSize%2 != 0)
        _mm_store_sd(mSmoothedValues+blockSize-1, zv);
}

С float + AVX (8 элементов на вектор) у вас будет

__m256 zv = _mm256_setr_ps(z_init, c + z_init*b,
                         c + c*b + z_init*b*b,
                         c + c*b + c*b*b + z_init*b*b*b, ...);

// Z2 = c + c*b + Z0*b^2
// Z3 = c + c*b + (c + Z0*b) * b^2
// Z3 = c + c*b + c*b^2 + Z0*b^3

и добавление / мулькоэффициенты будут для 8 шагов.

Обычно люди используют float для SIMD, потому что вы получаете вдвое больше элементов на вектор и вдвое меньшую пропускную способность памяти / кэш-память, поэтому обычно вы получаете коэффициент ускорения в 2 раза большеfloat.(Одинаковое количество векторов / байтов в такт.)


Вышеприведенный цикл на Haswell или Sandybridge, например, CPU будет работать с одним вектором на 8 циклов, узкое место с задержкой mulpd (5 циклов) + addps (3 цикла). Мы генерируем 2 double результатов на вектор, но это все еще огромное узкое место по сравнению с 1 муль и 1 добавлением на тактовую пропускную способность.Мы упускаем пропускную способность в 8 раз.

(или если скомпилирован с одним FMA вместо multi-> add, то у нас будет 5 циклов задержки).

SidesteppingПоследовательная зависимость полезна не только для SIMD, но и для того, чтобы избежать узких мест на задержке FP add / mul (или FMA), что приведет к дальнейшему ускорению, вплоть до соотношения FP add + mul latency к add + mul пропускной способности.

Просто разверните больше и используйте несколько векторов, таких как zv0, zv1, zv2, zv3.Это также увеличивает количество шагов, которые вы делаете одновременно.Так, например, 16-байтовые векторы float с 4 векторами будут 4x4 = 16 шагов.

0 голосов
/ 16 декабря 2018

У вас есть особый случай, когда входной сигнал равен Шаговая функция Хевисайда .Вы хотите получить ответ фильтра на эту функцию, который называется Шаговый ответ .Рекурсия в этом случае может быть устранена.Во-первых, давайте развернем рекурсию за несколько шагов.

z[1] = in + z[0]*b
z[2] = in + z[1]*b = in + (in + z[0]*b)*b  = in*(1 + b) + z[0]*b^2
z[3] = in + z[2]*b = in*(1 + b + b^2) + z[0]*b^3
z[4] = in + z[3]*b = in*(1 + b + b^2 + b^3) + z[0]*b^4

Из последнего выражения:

z[1] = in*(1 + b + b^2 + b^3) + z[-3]*b^4
z[2] = in*(1 + b + b^2 + b^3) + z[-2]*b^4
z[3] = in*(1 + b + b^2 + b^3) + z[-1]*b^4
z[4] = in*(1 + b + b^2 + b^3) + z[0]*b^4

Теперь очень легко переписать его в векторизованной форме.

in' = {in, in, in, in};
z' = in' * (1 + b + b^2 + b^3) + z'*b^4

Где "'" означает вектор илиодин SIMD регистр.Теперь будет легко перевести его в инструкции immintrin.Обратите внимание, что теперь вы не можете изменить входное значение ни для одного семпла, но время от времени кратно четырем семплам.

Кроме того, вы можете представить два или более SIMD-регистров как один вектор и расширить рекурсию больше.Это повысит производительность, так как лучше использует конвейер, но не переусердствуйте, иначе у вас не будет достаточно регистров.

...