9

我刚刚注意到我的一段代码在复制内存时表现出不同的性能。测试表明,如果目标缓冲区的地址大于源地址,则内存复制性能会下降。听起来很荒谬,但以下代码显示了差异(Delphi):

  const MEM_CHUNK = 50 * 1024 * 1024;
        ROUNDS_COUNT = 100;


  LpSrc := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);
  LpDest := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);

  QueryPerformanceCounter(LTick1);
  for i := 0 to ROUNDS_COUNT - 1 do
    CopyMemory(LpDest,LpSrc,MEM_CHUNK);
  QueryPerformanceCounter(LTick2);
    // show timings

  QueryPerformanceCounter(LTick1);
  for i := 0 to ROUNDS_COUNT - 1 do
    CopyMemory(LpSrc,LpDest,MEM_CHUNK);
  QueryPerformanceCounter(LTick2);
   // show timings

这里 CopyMemory 是基于 MOVSD 的。结果 :

开始内存带宽测试...

LpSrc 0x06FC0000

LpDest 0x0A1C0000

src->dest 传输:5242880000 字节在 1,188 秒 @4,110 GB/s。

dest->src 传输:5242880000 字节在 0,805 秒 @6,066 GB/s。

src->dest 传输:5242880000 字节在 1,142 秒 @4,275 GB/s。

dest->src 传输:5242880000 字节在 0,832 秒 @5,871 GB/s。

在两个系统上尝试过,无论重复多少次,结果都是一致的。

从来没有见过这样的事情。无法谷歌它。这是一种已知的行为吗?这只是另一个与缓存相关的特性吗?

更新:

以下是页面对齐缓冲区和 MOVSD 正向 (DF=0) 的最终结果:

开始内存带宽测试...

LpSrc 0x06F70000

LpDest 0x0A170000

src->dest 传输:5242880000 字节在 0,781 秒 @6,250 GB/s。

dest->src 传输:5242880000 字节在 0,731 秒 @6,676 GB/s。

src->dest 传输:5242880000 字节在 0,750 秒 @6,510 GB/s。

dest->src 传输:5242880000 字节,0,735 秒 @6,640 GB/s。

src->dest 传输:5242880000 字节,0,742 秒 @6,585 GB/s。

dest->src 传输:5242880000 字节在 0,750 秒 @6,515 GB/s。

... 等等。

这里的传输速率是恒定的。

4

1 回答 1

5

通常快速字符串或ERMSB微码可以rep movsb/w/d/q快速rep stosb/w/d/q处理大量数据(复制 16、32 甚至 64 字节的块)。并且可能带有针对商店的 RFO 避免协议。(其他repe/repne scas/cmps总是很慢)。

输入的某些条件可能会干扰最佳情况,特别是 DF=1(向后)而不是正常的 DF=0。

rep movsd性能可能取决于 src 和 dst 的对齐方式,包括它们的相对偏差。显然同时拥有两个指针 =32*n + same并不算太糟糕,因此大部分复制都可以在达到对齐边界后完成。(绝对未对齐,但指针彼此对齐。即dst-src是 32 或 64 字节的倍数)。

性能不依赖src > dstsrc < dst本身。如果指针在 16 或 32 字节的重叠范围内,这也可以强制一次回退到 1 个元素。

英特尔的优化手册有一节关于 memcpy 实现和rep movs与优化良好的 SIMD 循环的比较。启动开销是最大的缺点之一rep movs,但它不能很好地处理错位。(IceLake 的“快速做空rep”功能大概解决了这个问题。)

我没有透露 CopyMemory 主体 - 在避免重叠时它确实使用了向后复制 (df=1)。

是的,有你的问题。只有在需要避免实际重叠时才向后复制,而不仅仅是基于哪个地址更高。然后使用 SIMD 向量,而不是rep movsd.


rep movsd只有在 DF=0(升序地址)时才快,至少在 Intel CPU 上是这样。我刚刚检查了 Skylake: 1000000 reps of copying 4096 non-overlapping bytes from page-aligned buffers with rep movsbrun in:

  • 174M 循环cld(DF=0 forwards)。在大约 4.1GHz 时大约 42ms,或达到大约 90GiB/s L1d 读写带宽。每个周期大约 23 个字节,因此每个周期的启动开销rep movsb似乎都在伤害我们。在这种简单的纯 L1d 缓存命中情况下,AVX 复制循环应该达到接近 32B/​​s,即使在从内部循环退出循环时出现分支错误预测。
  • 4161M 循环std(DF=1 向后)。在大约 4.1GHz 时大约 1010ms,或大约 3.77GiB/s 读写。大约 0.98 字节/周期,与rep movsb完全未优化一致。(每个周期 1 个计数,因此rep movsd缓存命中的带宽约为 4 倍。)

uops_executedperf counter 还确认它在向后复制时花费了更多的微指令。(这是dec ebp / jnz在 Linux 下的长模式循环内。与 x86 的 MOV 是否真的“免费”相同的测试循环?为什么我根本不能重现这个?用 NASM 构建,缓冲区在 BSS 中。循环确实cldstd/ 2x lea/ mov ecx, 4096/ rep movsb。提升cld出循环并没有太大区别。)

您正在使用rep movsdwhich 一次复制 4 个字节,因此对于向后复制,如果它们在缓存中命中,我们可以预期 4 个字节/周期。而且您可能正在使用大型缓冲区,因此缓存未命中会成为前向速度的瓶颈,不会比后向速度快多少。但是来自向后复制的额外微指令会损害内存并行性:适合无序窗口的加载微指令触及的缓存行更少。此外,一些预取器在 Intel CPU 中向后运行的效果较差。L2 流媒体可以在任一方向工作,但我认为 L1d 预取只会继续前进。

相关:memcpy 的增强 REP MOVSB 您的 Sandybridge 对于 ERMSB 来说太旧了,但自原始 P6 以来就已经存在rep movs/的快速字符串。rep stos按照今天的标准,您从 ~2006 年开始的 Clovertown Xeon 已经非常古老了。(Conroe/Merom 微架构)。这些 CPU 可能太老了,以至于 Xeon 的单个内核可以使微薄的内存带宽饱和,这与当今的多核 Xeon 不同。


我的缓冲区是页面对齐的。对于向下,我尝试让初始 RSI/RDI 指向页面的最后一个字节,因此初始指针未对齐,但要复制的总区域是对齐的。我也试过lea rdi, [buf+4096]让起始指针是页面对齐的,所以[buf+0]没有被写入。两者都没有更快地向后复制;rep movs只是 DF=1 的垃圾;如果您需要向后复制,请使用 SIMD 向量。

rep movs通常,如果您可以使用机器支持的向量,那么SIMD 向量循环至少可以与 一样快。这意味着拥有 SSE、AVX 和 AVX512 版本......在可移植代码中,无需运行时分派到memcpy针对特定 CPU 调整的实现,rep movsd这通常非常好,并且在 IceLake 等未来的 CPU 上应该会更好。


您实际上不需要页面对齐rep movs来快速。IIRC,32 字节对齐的源和目标就足够了。但是 4k 别名也可能是一个问题:如果dst & 4095略高于src & 4095,则加载 uops 可能在内部必须等待一些额外的周期来存储 uops,因为用于检测加载何时重新加载最近存储的快速路径机制只查看页面- 偏移位。

但是,页面对齐是确保获得最佳情况的一种方法rep movs

通常,您可以从 SIMD 循环中获得最佳性能,但前提是您使用的 SIMD 向量与机器支持的一样宽(例如 AVX,甚至可能是 AVX512)。您应该根据硬件和周围的代码选择 NT 存储与普通存储。

于 2019-07-23T03:04:38.323 回答