MOVSD performance depends on arguments

use*_*735 9 delphi performance x86 assembly memory-bandwidth

I just noticed a pieces of my code exhibit different performance when copying memory. A test showed that a memory copying performance degraded if the address of destination buffer is greater than address of source. Sounds ridiculous, but the following code shows the difference (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
Run Code Online (Sandbox Code Playgroud)

Here CopyMemory is based on MOVSD. The results :

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.

Tried on two systems, the results are consistent no matter how many times repeated.

Never saw anything like that. Was unable to google it. Is this a known behavior? Is this just another cache-related peculiarity?

Update:

Here are the final results with page-aligned buffers and forward direction of 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.

Here the transfer rates are constant.

Pet*_*des 5

通常情况下快速的字符串或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-src32或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:在rep movsb运行时从页面对齐的缓冲区复制4096个非重叠字节的1000000次重复:

  • 具有cld(DF = 0向前)的174M个周期。在约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_executed性能计数器还确认,向后复制时它会花费更多的代码。(这是dec ebp / jnz在Linux下长模式下的循环内。与x86的MOV真的可以“免费”进行相同的测试循环吗?为什么我根本不能重现这一点?由NASM构建,在BSS中使用了缓冲区。循环做了cldstd/ 2x lea/ mov ecx, 4096/ rep movsb.。cld退出循环并没有太大的不同。)

您使用的rep movsd是一次复制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商店与普通商店。