我是SSE编程的新手,所以我希望有人可以帮助我。我最近使用GCC SSE内在函数实现了一个函数来计算32位整数数组的总和。我的实现代码如下。
int ssum(const int *d, unsigned int len)
{
static const unsigned int BLOCKSIZE=4;
unsigned int i,remainder;
int output;
__m128i xmm0, accumulator;
__m128i* src;
remainder = len%BLOCKSIZE;
src = (__m128i*)d;
accumulator = _mm_loadu_si128(src);
output = 0;
for(i=BLOCKSIZE;i<len-remainder;i+=BLOCKSIZE){
xmm0 = _mm_loadu_si128(++src);
accumulator = _mm_add_epi32(accumulator,xmm0);
}
accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 8));
accumulator = _mm_add_epi32(accumulator, _mm_srli_si128(accumulator, 4));
output = _mm_cvtsi128_si32(accumulator);
for(i=len-remainder;i<len;i++){
output += d[i];
}
return output;
}
正如您所看到的,这是一个相当简单的实现,我使用扩展的xmm寄存器一次对数组4求和,然后通过将剩余的元素相加来最后清理。
然后,我将此SIMD实现的性能与简单的for循环进行了比较。这个实验的结果可以在这里找到:正如您所看到的,与for循环相比,此实现确实显示输入大小(意味着数组的长度)的约60%加速,大约为5M元素。但是,对于较大的输入大小值,相对于for循环而言,性能会急剧下降,并且只会产生大约20%的加速。
我无法解释这种性能急剧下降的原因。我或多或少地通过内存线性地步进,因此对于两种实现,缓存未命中和页面错误的影响应该大致相同。我在这里错过了什么?有什么办法可以让这条曲线变平吗?任何想法将不胜感激。
答案 0 :(得分:5)
对于大输入,数据在缓存之外,代码是内存限制的 对于小输入,数据在缓存内(即L1 / L2 / L3缓存),代码是计算限制的 我假设在性能测量之前你没有尝试刷新缓存。
高速缓冲存储器位于CPU内部,高速缓冲存储器和ALU(或SSE)单元之间的带宽非常高(高带宽 - 传输数据的时间更少)。
您的最高级别缓存(即L3)大小约为4MB到8MB(取决于您的CPU型号)
大量数据必须位于DDR SDRAM上,外部RAM(CPU外部)
CPU通过内存总线连接到DDR SDRAM,带宽远低于高速缓存。
例:
假设您的外部RAM类型为Dual Channel DDR3 SDRAM 1600。
外部RAM和CPU之间的最大理论带宽约为25GB /秒。
从RAM向CPU读取100MB的数据(25GB / S)大约需要100e6 / 25e9 = 4msec。 根据我的经验,所使用的带宽约为理论带宽的一半,因此读取时间约为8毫秒。
计算时间较短:
假设循环的每次迭代需要大约2个CPU时钟(仅举例)
每次迭代处理16个字节的数据
处理100MB的总CPU时钟大约需要(100e6 / 16)* 2 = 12500000 clks
假设CPU频率为3GHz
总SSE处理时间约为12500000 / 3e9 = 4.2毫秒。
如您所见,从外部RAM读取数据所需的时间是SSE计算时间的两倍。
由于数据传输和计算是并行进行的,因此总时间最长为4.2毫秒和8毫秒(即8毫秒)。
假设不使用SSE的循环需要两倍的计算时间,因此不使用SSE计算时间约为8.4毫秒。
在上面的例子中,使用SSE的总改进是大约0.4毫秒。
注意:所选数字仅用于举例目的。
基准:
我在我的系统上做了一些基准测试
我使用的是Windows 10和Visual Studio 2010
基准测试:总计100MB的数据(总计25 * 1024 ^ 2 32位整数)。
CPU
记忆:
利用内存带宽:100 / 6.22 = 16GB /秒 (按时间划分数据大小)。
使用SSE(缓存中的数据)每次迭代的平均时钟:(3.6e9 * 3.86e-3)/(25/4 * 1024 ^ 2)= 2.1 clks / iteration (除了总CPU按迭代次数计算时钟)。