MOVSD 性能取决于参数

MOVSD performance depends on arguments

我刚刚注意到我的一段代码在复制内存时表现出不同的性能。一项测试表明,如果目标缓冲区的地址大于源地址,内存复制性能会下降。听起来很荒谬,但下面的代码显示了差异 (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的。结果:

Starting Memory Bandwidth Test...

LpSrc 0x06FC0000

LpDest 0x0A1C0000

src->dest Transfer: 5242880000 bytes in 1,188 sec @4,110 GB/s.

dest->src Transfer: 5242880000 bytes in 0,805 sec @6,066 GB/s.

src->dest Transfer: 5242880000 bytes in 1,142 sec @4,275 GB/s.

dest->src Transfer: 5242880000 bytes in 0,832 sec @5,871 GB/s.

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

从来没有见过这样的东西。无法 google 它。这是已知行为吗?这只是另一个与缓存相关的特性吗?

更新:

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

Starting Memory Bandwidth Test...

LpSrc 0x06F70000

LpDest 0x0A170000

src->dest Transfer: 5242880000 bytes in 0,781 sec @6,250 GB/s.

dest->src Transfer: 5242880000 bytes in 0,731 sec @6,676 GB/s.

src->dest Transfer: 5242880000 bytes in 0,750 sec @6,510 GB/s.

dest->src Transfer: 5242880000 bytes in 0,735 sec @6,640 GB/s.

src->dest Transfer: 5242880000 bytes in 0,742 sec @6,585 GB/s.

dest->src Transfer: 5242880000 bytes in 0,750 sec @6,515 GB/s.

... and so on.

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

通常 fast-strings 或 微代码使 rep movsb/w/d/qrep stosb/w/d/q 快速进行大量计数(复制 16、32 甚至 64 字节块)。并且可能与商店的 RFO-avoiding 协议。 (其他 repe/repne scas/cmps 总是很慢)。

某些输入条件会干扰 best-case,特别是 DF=1(向后)而不是正常的 DF=0。

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

性能 取决于 src > dstsrc < dst per-se。如果指针在重叠的 16 或 32 字节范围内,那也可以强制 fall-back 一次到 1 个元素。

Intel 的优化手册中有一节是关于 memcpy 实现和比较 rep movs 与 well-optimized SIMD 循环的。启动开销是 rep movs 最大的缺点之一,但它不能很好地处理的错位也是如此。 (IceLake 的 "fast short rep" 功能大概解决了这个问题。)

I did not disclose the CopyMemory body - and it indeed used copying backwards (df=1) when avoiding overlaps.

是的,这是你的问题。如果您需要避免实际重叠,则仅向后复制,而不仅仅是基于哪个地址更高。然后用 SIMD 向量来做,而不是 rep movsd.


rep movsd 只有在 DF=0(升序地址)时才快,至少在 Intel CPUs 上是这样。 我刚刚检查了 Skylake:1000000使用 rep movsb 从 page-aligned 缓冲区复制 4096 non-overlapping 字节的次数在:

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

uops_executed perf counter 还确认它在向后复制时花费了更多的微指令。 (这是在 Linux 下长模式下的 dec ebp / jnz 循环内。与使用 NASM 构建的 相同的测试循环,使用 BSS 中的缓冲区。循环做了 cldstd / 2x lea / mov ecx, 4096 / rep movsb。将 cld 提升到循环外并没有太大区别。)

您使用的是一次复制 4 个字节的 rep movsd,因此对于向后复制,如果它们命中缓存,我们可以预期 4 个字节/周期。而且您可能正在使用大缓冲区,因此缓存会错过向前方向的瓶颈,不会比向后快多少。但是来自反向复制的额外 uops 会损害内存并行性:适合 out-of-order window 的加载 uops 触及的缓存行更少。此外,在 Intel CPUs 中,某些预取器向后工作的效果较差。 L2 流光在任一方向都有效,但我认为 L1d 预取只会向前。

相关: 你的 Sandybridge 对于 ERMSB 来说太旧了,但是 rep movs/rep stos 的 Fast Strings 自最初的 P6 以来就已经存在了。以今天的标准来看,您 2006 年的 Clovertown Xeon 已经相当古老了。 (Conroe/Merom 微架构)。与今天的 many-core Xeon 不同,那些 CPU 可能太旧了,以至于 Xeon 的单个内核可以饱和微薄的内存带宽。


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

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


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

页面对齐是确保您获得最佳案例的一种方法rep movs,不过

通常情况下,您可以从 SIMD 循环中获得最佳性能,但前提是您使用机器支持的 SIMD 矢量(如 AVX,甚至可能是 AVX512)。你应该根据硬件和周围的代码选择 NT 商店还是普通商店。