【问题标题】:MOVSD performance depends on argumentsMOVSD 性能取决于参数
【发布时间】:2019-11-29 21:38:05
【问题描述】:

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

...等等。

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

【问题讨论】:

  • 两个缓冲区是否具有相同的对齐方式? 4k 混叠会是问题吗?也许在一个方向上,dst 在页面内的偏移量比 src 稍低,因此内存消歧可以看到负载无法重新加载存储。但另一方面,它可能会错误地检测到混叠并减少带宽。让您的代码打印地址。另外,您在什么 CPU 硬件上进行了测试?哈斯韦尔?天湖?原子?锐龙? K10?
  • 如果反转它们会发生什么?或者在它们之间添加一个睡眠?
  • 感谢您的建议。将分配更改为 VirtualAlloc 以进行对齐。输出:
  • 测试的 CPU 是 SandyBridge 和 Clovertown
  • @BeeOnRope: rep movsd 只对DF=0 (升序地址)快速。我刚刚检查了 Skylake:使用 rep movsb 复制 4096 个非重叠字节的 1000000 次重复使用 cld 运行 174M 周期,而使用 std 运行 4161M 周期,用于页面对齐输入或页面 1 输入(我试过两者都是向下的,两者都很糟糕)。执行的 uops 也证实了它在向后复制时花费了更多的 uops。仅当 rep movsd 被 SIMD 循环替换时,您的向后复制建议才可行。

标签: performance delphi assembly x86 memory-bandwidth


【解决方案1】:

通常快速字符串或ERMSB 微码使rep movsb/w/d/qrep 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 的“fast short 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 rep movsb 的缓冲区运行在:

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

uops_executed perf 计数器还确认它在向后复制时花费了更多的微指令。 (这是在 Linux 下的长模式下的 dec ebp / jnz 循环内。与使用 NASM 构建的 Can x86's MOV really be "free"? Why can't I reproduce this at all? 相同的测试循环,缓冲区位于 BSS 中。循环执行 cldstd / 2x lea / mov ecx, 4096 / rep movsb. 将 cld 吊出循环并没有太大区别。)

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

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


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

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


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

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

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

【讨论】:

  • 其他说明:我尝试了 rep movsd 在页面对齐和 32 位对齐缓冲区之间的各种组合。在我的 SandyBridge 上,al->al 是最好的,un->un 是第二好的,a->u 和 u->a 是最差的(!)。在我的“相当老”的 Xeons 上 al->al,u->u,u->a 没有区别并且是最好的,而 a->u 则差两倍。而且我的 SIMD 实现比任何 rep movsd 都差得多,甚至倒退。
  • @user4859735:当您执行u->u 时,src 和 dst 的相对偏差是否相同?因此,在未对齐的启动之后,它可能会到达对齐边界并得到 al->al 的情况。另外,请注意我说的是 32 byte 对齐(AVX 宽度),而不是 32 bit。 Sandybridge 可能只关心 16 字节,这与 Haswell 及更高版本不同。
  • @user4859735:如果您的 SIMD 实现速度较慢,您可能做错了。例如movups 在 Core 2 上速度很慢,即使地址在运行时对齐也是如此。 Core 2 是一个挑战,但 Sandybridge 应该通过适当的循环展开和处理相对错位来提高效率。 (我认为通常的建议是更喜欢对齐的目标,而不是对齐的源,如果由于不同的相对错位而不能同时拥有两者。)
  • 顺便说一句,我想这个话题的答案应该是'是的,MOVSD 性能取决于参数......至少在某种方式上。')
  • 对,目标缓冲区的对齐比源缓冲区的对齐更重要。顺便说一句,L1 IP 预取器可以检测具有负跨度的访问模式并相应地向后预取。但 DCU 预取器不能。
猜你喜欢
  • 1970-01-01
  • 2019-04-24
  • 2016-11-09
  • 2016-05-27
  • 1970-01-01
  • 2014-11-05
  • 1970-01-01
  • 1970-01-01
  • 2020-05-03
相关资源
最近更新 更多