2016-08-17 43 views
2

我是SSE编程新手,所以我希望有人能帮助我。我最近使用GCC SSE内在函数实现了一个函数来计算32位整数数组的总和。下面给出了我的实现代码。大型阵列尺寸的SSE性能下降

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循环进行了比较。这个实验的结果可以在这里找到:

SIMD vs. for-loop

正如你可以看到,相较于一个for循环,这个实现确实表现出约〜60%的加速为输入尺寸(指的长度阵列)高达约5M元素。然而,对于输入大小的较大值,与for循环有关的性能会急剧下降,并且只会产生大约20%的加速。

我无法解释这种戏剧性的下降。我或多或少地通过内存线性地步进,所以缓存未命中和页面错误的影响应该对于两种实现大致相同。我在这里错过了什么?有什么办法可以将这条曲线弄平吗?任何想法将不胜感激。

+1

你使用的是什么CPU? –

+2

首先,你是否检查gcc是否会自动化标量代码?其次,你可能会受限于内存带宽。 – EOF

+0

正如@EOF所说,你在循环中几乎不做任何事(一个SIMD算术指令),所以当你有大的数组时,你很可能会限制内存带宽。 –

回答

4

对于大量输入,数据在缓存之外,并且代码是内存有界的。
对于较小的输入,数据位于缓存内(即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中读取100MBytes的数据(25GB/S)大约需要100e6/25e9 = 4msec。
根据我的经验,利用的带宽约为理论带宽的一半,因此读取时间约为8毫秒。

计算时间更短:
假设您的循环的每次迭代需要大约2个CPU时钟(只是一个示例)。
每次迭代处理16个字节的数据。
处理100MB的总CPU时钟大约需要(100e6/16)* 2 = 12500000个时钟。
假设CPU频率是3GHz。
总SSE处理时间约为12500000/3e9 = 4.2毫秒。

正如你所看到的,从外部RAM读取数据需要两倍于SSE计算时间。由于数据传输和计算并行发生,总时间最大为4.2msc和8msec(即8msec)。

假设没有使用SSE的循环会花费两倍的计算时间,所以如果不使用SSE,计算时间约为8.4毫秒。

在上面的例子中,使用SSE的总体改进大约是0.4毫秒。

注意:所选数字仅用于示例目的。


基准:
我做了我的系统上的一些基准。
我正在使用Windows 10和Visual Studio 2010.
基准测试:总结100MB数据(总计25 * 1024^2 32位整数)。

CPU

  • Intel酷睿i5 3550(Ivy Bridge的)。
  • CPU基频为3.3GHz。
  • 测试过程中的实际核心速度:3.6GHz(启用了涡轮增压)。
  • L1数据缓存大小:32KBytes。
  • L2高速缓存大小:256Bytes(单核心L2高速缓存大小)。
  • L3缓存大小:6MBytes。

内存:

  • 8GB DDR3双通道。
  • RAM频率:666MHz(相当于1333MHz无DDR)。内存理论最大带宽:(128 * 1333/8)/ 1024 = 20.8GBytes/Sec。内存理论最大带宽:(128 * 1333/8)/ 1024 = 20.8GBytes/Sec。

  1. 萨姆100MB如大块与SSE(在外部RAM中的数据)。
    处理时间:6.22msec
  2. 总和1KB 100次与SSE(缓存内的数据)。
    处理时间:3.86msec
  3. 总计100MB为大块没有SSE(外部RAM中的数据)。
    处理时间:8.1毫秒
  4. 总计1KB 100倍无SSE(缓存内的数据)。
    处理时间:4。73msec

可利用的存储器带宽:100/6.22 = 16GB /秒(分割数据大小通过时间)。 (3.6e9 * 3.86e-3)/(25/4 * 1024^2)= 2.1 clks/iteration(将总CPU时钟数除以迭代次数)(每个迭代的平均时钟数为高速缓存) )

+0

这是这样一个详细的答案。谢谢!鉴于你获得的结果,你认为软件缓冲可能有什么优势吗?基本上一次将DRAM 32KB的内存复制到第二个软件管理的缓冲区并在那里进行计算?我会想象这会加载到主内存中的性能命中,并且不会导致高速缓存以64字节(高速缓存行的长度)丢失。我不是计算机体系结构方面的专家,所以请随时告诉我这是否是完全疯狂的谈话。 – voidbip

+0

当我编程到DSP(从外部RAM到DSP内部RAM的初始DMA传输)时,我曾经这样做。现代x86体系结构使用自动预取机制来检测内存访问模式(即加入地址)并在程序使用它之前将数据读入缓存。缓存未命中并不是问题,性能受内存带宽的限制(你不能从DDR SDRAM更快地读取数据)。 – Rotem

+0

@voidbip:这是一个好主意,但实际上很糟糕。这意味着您的所有计算都不会与第一次触摸冷内存时发生的缓存未命中事件重叠。 L1/L2/L3缓存可以缓存主内存的任何部分,而额外的拷贝只会增加缓存的占用空间。反弹到L1中保持热点的小缓冲区对于非常罕见的情况非常有用,例如[将NT视频RAM中的NT加载与NT存储分隔到常规RAM](https://software.intel.com/zh-cn/articles/copying -accelerated视频解码帧缓冲器)。直到/除非你理解那篇文章,否则不要这样做。 –