大型阵列的 SSE 性能较慢
Slower SSE performance on large array sizes
我是 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% 的加速。
我无法解释性能的急剧下降。我或多或少在内存中线性步进,因此缓存未命中和页面错误的影响对于两种实现应该大致相同。我在这里错过了什么?有什么办法可以拉平这条曲线吗?任何想法将不胜感激。
对于大输入,数据在缓存之外,代码受内存限制。
对于小输入,数据在缓存中(即 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/Sec。
从 RAM 读取 100MBytes 的数据(以 25GB/S 的速度)到 CPU 大约需要 100e6 / 25e9 = 4 毫秒。
根据我的经验,使用带宽约为理论带宽的一半,因此读取时间约为 8 毫秒。
计算时间更短:
假设循环的每次迭代大约需要 2 CPU 个时钟(仅作为示例)。
每次迭代处理 16 个字节的数据。
处理 100MB 的总 CPU 个时钟大约需要 (100e6 / 16)*2 = 12500000 个时钟。
假设CPU频率为3GHz。
SSE 总处理时间约为 12500000 / 3e9 = 4.2 毫秒。
如您所见,从外部RAM读取数据所花费的时间是SSE计算时间的两倍。
由于数据传输和计算是并行进行的,所以总时间是4.2mesc和8msec的最大值(即8msec)。
假设不使用 SSE 的循环需要两倍的计算时间,因此不使用 SSE 的计算时间约为 8.4 毫秒。
在上面的示例中,使用 SSE 的总改进约为 0.4 毫秒。
注意:所选数字仅供参考。
基准:
我在我的系统上做了一些基准测试。
我正在使用 Windows 10 和 Visual Studio 2010.
基准测试:Summing 100MBytes of data (summing 25*1024^2 32bits integers).
CPU
- 英特尔酷睿 i5 3550(常春藤桥)。
- CPU 基频为 3.3GHz。
- 测试期间的实际核心速度:3.6GHz(已启用 Turbo boost)。
- 一级数据缓存大小:32KBytes。
- 二级缓存大小:256Bytes(单核二级缓存大小)。
- 三级缓存大小:6MBytes。
内存:
- 8GB DDR3 双通道。
- RAM 频率:666MHz(相当于没有 DDR 时的 1333MHz)。
- 内存理论最大带宽:(128*1333/8) / 1024 = 20.8GBytes/Sec.
- 用 SSE 将 100MB 作为大块求和 (外部 RAM 中的数据)。
处理时间:6.22毫秒
- 用 SSE 对 1KB 求和 100 次(缓存中的数据)。
处理时间:3.86毫秒
- 总计 100MB 作为大块没有 SSE(外部 RAM 中的数据)。
处理时间:8.1毫秒
- 总和 1KB 100 次没有 SSE(缓存中的数据)。
处理时间:4.73毫秒
已使用内存带宽:100/6.22 = 16GB/Sec (按时间划分数据大小).
SSE 每次迭代的平均时钟(缓存中的数据):(3.6e9*3.86e-3)/(25/4*1024^2) = 2.1 clks/iteration (将总 CPU 时钟除以迭代次数)。
我是 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% 的加速。
我无法解释性能的急剧下降。我或多或少在内存中线性步进,因此缓存未命中和页面错误的影响对于两种实现应该大致相同。我在这里错过了什么?有什么办法可以拉平这条曲线吗?任何想法将不胜感激。
对于大输入,数据在缓存之外,代码受内存限制。
对于小输入,数据在缓存中(即 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/Sec。
从 RAM 读取 100MBytes 的数据(以 25GB/S 的速度)到 CPU 大约需要 100e6 / 25e9 = 4 毫秒。
根据我的经验,使用带宽约为理论带宽的一半,因此读取时间约为 8 毫秒。
计算时间更短:
假设循环的每次迭代大约需要 2 CPU 个时钟(仅作为示例)。
每次迭代处理 16 个字节的数据。
处理 100MB 的总 CPU 个时钟大约需要 (100e6 / 16)*2 = 12500000 个时钟。
假设CPU频率为3GHz。
SSE 总处理时间约为 12500000 / 3e9 = 4.2 毫秒。
如您所见,从外部RAM读取数据所花费的时间是SSE计算时间的两倍。
由于数据传输和计算是并行进行的,所以总时间是4.2mesc和8msec的最大值(即8msec)。
假设不使用 SSE 的循环需要两倍的计算时间,因此不使用 SSE 的计算时间约为 8.4 毫秒。
在上面的示例中,使用 SSE 的总改进约为 0.4 毫秒。
注意:所选数字仅供参考。
基准:
我在我的系统上做了一些基准测试。
我正在使用 Windows 10 和 Visual Studio 2010.
基准测试:Summing 100MBytes of data (summing 25*1024^2 32bits integers).
CPU
- 英特尔酷睿 i5 3550(常春藤桥)。
- CPU 基频为 3.3GHz。
- 测试期间的实际核心速度:3.6GHz(已启用 Turbo boost)。
- 一级数据缓存大小:32KBytes。
- 二级缓存大小:256Bytes(单核二级缓存大小)。
- 三级缓存大小:6MBytes。
内存:
- 8GB DDR3 双通道。
- RAM 频率:666MHz(相当于没有 DDR 时的 1333MHz)。
- 内存理论最大带宽:(128*1333/8) / 1024 = 20.8GBytes/Sec.
- 用 SSE 将 100MB 作为大块求和 (外部 RAM 中的数据)。
处理时间:6.22毫秒 - 用 SSE 对 1KB 求和 100 次(缓存中的数据)。
处理时间:3.86毫秒 - 总计 100MB 作为大块没有 SSE(外部 RAM 中的数据)。
处理时间:8.1毫秒 - 总和 1KB 100 次没有 SSE(缓存中的数据)。
处理时间:4.73毫秒
已使用内存带宽:100/6.22 = 16GB/Sec (按时间划分数据大小).
SSE 每次迭代的平均时钟(缓存中的数据):(3.6e9*3.86e-3)/(25/4*1024^2) = 2.1 clks/iteration (将总 CPU 时钟除以迭代次数)。