从二阶导数计算的曲线的SIMD优化

时间:2017-12-26 21:01:40

标签: c++ optimization vectorization simd

这个问题确实是个好奇心。

我正在将一个例程转换为SIMD指令(我对SIMD编程很陌生),并且遇到以下代码问题:

// args:
uint32_t phase_current;
uint32_t phase_increment;
uint32_t phase_increment_step;

for (int i = 0; i < blockSize; ++i)
{
    USEFUL_FUNC(phase_current);
    phase_increment += phase_increment_step;
    phase_current += phase_increment;
}

问题:假设USEFUL_FUNC有一个SIMD实现,而我只是想计算一个正确的phase_current向量进行处理,那么处理{{{0}的正确方法是什么? 1}}依赖于之前的值?

反过来,函数式编程phase_current - 就像实现一样有用,因为我试图理解如何提升数据依赖性,而不是为了优化而试图优化。< / p>

最后,如果你能推荐一些文献,请做。不确定Google如何处理此主题。

2 个答案:

答案 0 :(得分:2)

我唯一能想到的是水平加法。想象一下,你有__m128i向量,内容为{pc,0,pi,pis}。然后第一个HADD将进入 {pc,pi + pis},第二个HADD将进入pc + pi + pis

HADD同时对两个__m128i进行操作,因此可以进行一些加速。

但交错指令使得管道总是满的并不是一件轻而易举的事。链接到HADD:https://msdn.microsoft.com/en-us/library/bb531452(v=vs.120).aspx

让我添加链接到HADD for floats非常有用的讨论。许多代码和结论可以直接应用于整数HADD:Fastest way to do horizontal float vector sum on x86

答案 1 :(得分:2)

所以你只是想找到一种方法来生成4个phase_current值的向量,你可以将它作为arg传递给任意函数。

TL:DR :设置递增和步进的初始向量,使每个向量元素按顺序跨越4,给出phase_current[i+0..i+3]向量,但仍然只有两个向量ADD运算(垂直,不是水平)。 这个序列依赖是你可以用代数/数学来分解的。

这有点像前缀和(which you can SIMD with log2(vector_width) shuffle+add operations for vectors with vector_width elements。)您还可以使用两步计算将前缀和与多个线程并行化,其中每个线程前缀 - 对数组的一个区域求和,然后组合结果并让每个线程通过一个常量(该区域的第一个元素的总和)来偏移其目标数组的区域。请参阅多线程的链接问题。

但是你有phase_increment_step(你想要的值的二阶导数)是常数的巨大简化。我假设USEFUL_FUNC(phase_current);通过值获取其arg,而不是非const引用,因此phase_current的唯一修改是循环中的+=。并且useful_func不能以某种方式改变增量或increment_step。

实现此目的的一个选项是在SIMD向量的4个独立元素中独立运行标量算法,每次偏移1次迭代。使用整数添加,特别是在其中向量整数添加延迟仅为1个周期的英特尔CPU上,运行4次迭代的运行总计很便宜,我们可以在调用USEFUL_FUNC之间执行此操作。这将是一种向USEFUL_FUNC生成矢量输入的方法与标量代码完全一样的工作(假设SIMD整数添加与标量整数添加一样便宜,如果我们受数据依赖性限制为每时钟2个增加,则大多数情况下都是如此)。

上面的方法有点更通用,可能对这个问题的变化有用,因为有一个真正的串行依赖,我们无法通过简单的数学便宜地消除它。

如果我们聪明,我们可以比前缀总和或者一次一步地运行4个序列的暴力更好。理想情况下,我们可以在值序列中推导出一种封闭形式的步骤4(或者无论SIMD向量宽度是多少,都需要为USEFUL_FUNC的多个累加器所需的任何展开因子

stepstep*2step*3,......的序列求和将给我们一个恒定的时间Gauss's closed-form formula for the sum of integers up to nsum(1..n) = n*(n+1)/2。该序列为0,1,3,6,10,15,21,28,......(https://oeis.org/A000217)。 (我已经考虑了最初的phase_increment)。

诀窍是按顺序进行4。 (n+4)*(n+5)/2 - n*(n+1)/2 simplifies down to 4*n + 10。再次得到它的导数,我们得到4.但是要在第2个积分中走4步,我们得到4*4 = 16。因此,我们可以维护一个向量phase_increment,我们使用向量为16*phase_increment_step的SIMD加法进行递增。

我不完全确定我有正确的步数推理(额外因子4给16有点令人惊讶)。 制定正确的公式,并在向量序列中采用第一和第二差异,这非常清楚如何解决这个问题

 // design notes, working through the first couple vectors
 // to prove this works correctly.

S = increment_step (constant)
inc0 = increment initial value
p0 = phase_current initial value

// first 8 step-increases:
[ 0*S,  1*S,   2*S,  3*S ]
[ 4*S,  5*S,   6*S,  7*S ]

// first vector of 4 values:
[ p0,  p0+(inc0+S),  p0+(inc0+S)+(inc0+2*S),  p0+(inc0+S)+(inc0+2*S)+(inc0+3*S) ]
[ p0,  p0+inc0+S,  p0+2*inc0+3*S,  p0+3*inc0+6*S ]  // simplified

// next 4 values:
[ p0+4*inc0+10*S,  p0+5*inc0+15*S,  p0+6*inc0+21*S,  p0+7*inc0+28*S ]

使用此和先前的4*n + 10公式:

// first 4 vectors of of phase_current
[ p0,              p0+1*inc0+ 1*S,  p0+2*inc0+3*S,   p0+ 3*inc0+ 6*S ]
[ p0+4*inc0+10*S,  p0+5*inc0+15*S,  p0+6*inc0+21*S,  p0+ 7*inc0+28*S ]
[ p0+8*inc0+36*S,  p0+9*inc0+45*S,  p0+10*inc0+55*S, p0+11*inc0+66*S ]
[ p0+12*inc0+78*S,  p0+13*inc0+91*S,  p0+14*inc0+105*S, p0+15*inc0+120*S ]

 first 3 vectors of phase_increment (subtract consecutive phase_current vectors):
[ 4*inc0+10*S,     4*inc0 + 14*S,   4*inc0 + 18*S,   4*inc0 + 22*S  ]
[ 4*inc0+26*S,     4*inc0 + 30*S,   4*inc0 + 34*S,   4*inc0 + 38*S  ]
[ 4*inc0+42*S,     4*inc0 + 46*S,   4*inc0 + 50*S,   4*inc0 + 54*S  ]

 first 2 vectors of phase_increment_step:
[        16*S,              16*S,            16*S,            16*S  ]
[        16*S,              16*S,            16*S,            16*S  ]
Yes, as expected, a constant vector works for phase_increment_step

所以我们可以用英特尔的SSE / AVX内在函数来编写这样的代码

#include <stdint.h>
#include <immintrin.h>

void USEFUL_FUNC(__m128i);

// TODO: more efficient generation of initial vector values
void double_integral(uint32_t phase_start, uint32_t phase_increment_start, uint32_t phase_increment_step, unsigned blockSize)
{

    __m128i pstep1 = _mm_set1_epi32(phase_increment_step);

    // each vector element steps by 4
    uint32_t inc0=phase_increment_start, S=phase_increment_step;
    __m128i pincr  = _mm_setr_epi32(4*inc0 + 10*S,  4*inc0 + 14*S,   4*inc0 + 18*S,   4*inc0 + 22*S);

    __m128i phase = _mm_setr_epi32(phase_start,  phase_start+1*inc0+ 1*S,  phase_start+2*inc0+3*S,   phase_start + 3*inc0+ 6*S );
     //_mm_set1_epi32(phase_start); and add.
     // shuffle to do a prefix-sum initializer for the first vector?  Or SSE4.1 pmullo by a vector constant?

    __m128i pstep_stride = _mm_slli_epi32(pstep1, 4);  // stride by pstep * 16
    for (unsigned i = 0; i < blockSize; ++i)  {
        USEFUL_FUNC(phase);
        pincr = _mm_add_epi32(pincr, pstep_stride);
        phase = _mm_add_epi32(phase, pincr);
    }
}

进一步阅读:有关SIMD的更多信息,但主要是x86 SSE / AVX,请参阅https://stackoverflow.com/tags/sse/info,特别是来自SIMD at Insomniac Games (GDC 2015)的幻灯片,其中包含一些关于如何考虑SIMD的优点,以及如何布置数据以便您可以使用它。