Если есть формула закрытой формы для шагов 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 шагов.