【问题标题】:Slower SSE performance on large array sizes大型阵列上的 SSE 性能较慢
【发布时间】:2016-12-24 02:09:19
【问题描述】:

我是 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 循环进行了比较。该实验的结果可在此处获得:

SIMD vs. for-loop

如您所见,与 for 循环相比,这个实现确实显示了大约 60% 的加速,输入大小(即数组的长度)高达约 5M 元素。但是,对于较大的输入大小值,与 for 循环相关的性能会急剧下降,并且只产生大约 20% 的加速。

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

【问题讨论】:

  • 你用的是什么CPU?
  • 首先,您是否检查了 gcc 是否自动矢量化了标量代码?其次,您可能会受到内存带宽的限制。
  • 正如@EOF 所说,您在循环中几乎没有做任何事情(一条 SIMD 算术指令),因此当您拥有大型数组时,您很可能会受到内存带宽的限制。
  • @RomanKhimov 我在我可以访问的各种服务器上都看到了这种现象,但这个特定的实验是在 Intel(R) Xeon(R) CPU E5-2667 v4 @ 3.20GHz w。 504 GB 内存。
  • 您应该查看实时时间,而不仅仅是相对时间。当然,内存带宽、缓存和其他开销(包括 VM 实现)对这两种基准的影响是相同的,但这意味着它们对速度更快的基准的影响会成比例地增加。

标签: c performance sse simd intrinsics


【解决方案1】:

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

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

如您所见,从外部 RAM 读取数据所需的时间是 SSE 计算时间的两倍。

由于数据传输和计算并行发生,总时间最长为 4.2 毫秒和 8 毫秒(即 8 毫秒)。

假设不使用 SSE 的循环需要两倍的计算时间,因此不使用 SSE 的计算时间约为 8.4 毫秒。

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

注意:所选数字仅供参考。


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

处理器

  • 英特尔酷睿 i5 3550(常春藤桥)。
  • CPU 基本频率为 3.3GHz。
  • 测试期间的实际核心速度:3.6GHz(启用涡轮增压)。
  • L1 数据缓存大小:32KBytes。
  • 二级缓存大小:256Bytes(单核二级缓存大小)。
  • L3 缓存大小:6MBytes。

内存:

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

  1. 将 100MB 作为大块 与 SSE(外部 RAM 中的数据)相加。
    处理时间:6.22 毫秒
  2. 将 1KB 相加 100 次与 SSE(缓存内的数据)。
    处理时间:3.86 毫秒
  3. 总计 100MB 作为大块无 SSE(外部 RAM 中的数据)。
    处理时间:8.1 毫秒
  4. 总和 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按迭代次数计算时钟)

【讨论】:

  • 这是一个如此详细的答案。谢谢!鉴于您获得的结果,您认为软件缓冲可能有什么优势吗?基本上一次将内存从 DRAM 32KB 复制到第二个软件管理缓冲区并在那里进行计算?我想这会将字体加载到主内存中的性能损失,并且不会导致 64 字节(缓存行的长度)的缓存未命中。无论如何,我都不是计算机架构方面的专家,所以如果这完全是疯狂的谈话,请随时告诉我。
  • 我曾经在对 DSP 编程时这样做(启动 DMA 从外部 RAM 传输到 DSP 内部 RAM)。现代 x86 架构使用自动预取机制来检测内存访问模式(即访问地址)并在程序使用数据之前将数据读入缓存。缓存未命中不是问题,性能受到内存带宽的限制(您无法更快地从 DDR SDRAM 读取数据)。
  • @voidbip:这是个好主意,但实际上很糟糕。这意味着您的任何计算都不能与您在第一次接触冷内存时发生的缓存未命中重叠。 L1/L2/L3 缓存可以缓存主内存的任何部分,额外的复制只会增加你的缓存占用。对于像keeping NT loads from video RAM separate from NT stores to regular RAM 这样的非常罕见的情况,弹跳到一个在 L1 中保持热的小缓冲区很有用。直到/除非你理解那篇文章,否则不要这样做。
猜你喜欢
  • 2016-11-04
  • 1970-01-01
  • 1970-01-01
  • 2019-12-22
  • 1970-01-01
  • 2021-12-03
  • 2020-03-04
  • 2019-04-09
  • 1970-01-01
相关资源
最近更新 更多