32位和64位进程之间的memcpy性能差异

9
我们有Core2机器(Dell T5400)运行XP64。我们观察到,当运行32位进程时,memcpy的性能约为1.2GByte/s;然而,在64位进程中,memcpy可达到约2.2GByte/s(或使用Intel编译器CRT的memcpy时可达到2.4GByte/s)。虽然最初的反应可能是仅仅将其归因于64位代码中更宽的寄存器,但我们观察到我们自己的类似memcpy的SSE汇编代码(无论进程的32/64位性如何,都应该使用128位宽的加载存储)在实现的复制带宽上具有类似的上限。
我的问题是,这种差异实际上是由于什么造成的?32位进程是否需要跳过一些额外的WOW64障碍才能访问RAM?这与TLB或预取器有关还是...什么?
感谢您的任何见解。
也在Intel论坛上提出了相关问题

你是说你的SSE代码在本机64位模式下也比WOW64快两倍?你是否在32位XP上进行了基准测试以查看WOW64是否影响性能? - bk1e
是的,完全正确。32位操作系统测试是一个很好的建议...但不幸的是,我们没有任何等效的32位操作系统硬件!我希望有人能告诉我WOW64是否是问题所在。将研究获取32位安装程序。 - timday
7个回答

8
我认为以下内容可以解释它:
要将数据从内存复制到寄存器,然后再复制回内存,您需要执行以下操作:
mov eax, [address]
mov [address2], eax

这将32位(4字节)从地址移动到地址2。在64位模式下,64位也是如此。

mov rax, [address]
mov [address2], rax

这将64位、2字节的数据从地址移动到地址2。无论是64位还是32位,根据英特尔规格,“mov”本身的延迟为0.5,吞吐量为0.5。延迟是指指令通过管道所需的时钟周期数,而吞吐量是CPU在接受相同指令之前必须等待的时间。正如您所看到的,它可以在一个时钟周期内执行两个mov操作,但是在两个mov操作之间必须等待半个时钟周期,因此它实际上只能在一个时钟周期内执行一个mov操作(或者我在这里理解错误并且误解了这些术语?有关详细信息,请参见此处的PDF)。
当然,取决于数据是否在第1或第2级缓存中,或者根本不在缓存中并且需要从内存中获取,mov reg, mem可能需要超过0.5个周期。然而,上面的延迟时间忽略了这一事实(如我上面链接的PDF所述),它假设所有用于mov的数据都已经存在(否则,延迟将增加为从目前所在的位置获取数据所需的时间长度 - 这可能需要几个时钟周期,并且与正在执行的命令完全无关,如PDF第482/C-30页所述)。
有趣的是,mov是32位还是64位并不重要。这意味着除非内存带宽成为限制因素,否则使用64位mov与32位mov一样快,而且由于使用64位时只需要移动一半的mov操作即可将相同数量的数据从A移动到B,吞吐量(理论上)可以提高两倍(事实上没有,可能是因为内存速度不是无限快)。
好了,现在您认为使用更大的SSE寄存器应该获得更快的吞吐量,对吗?顺便说一下,BTW(维基百科的参考文献)xmm寄存器不是256位,而是128位宽。但是,您考虑过延迟和吞吐量吗?要么您要移动的数据是128位对齐的,要么不是。根据情况,您可以使用以下任一方式移动它:
movdqa xmm1, [address]
movdqa [address2], xmm1

或者如果没有对齐

movdqu xmm1, [address]
movdqu [address2], xmm1

movdqa/movdqu指令的延迟为1,吞吐量为1。因此,这些指令执行所需的时间是普通mov指令的两倍,指令后的等待时间也是普通mov指令的两倍。

还有一点我们甚至没有考虑到的是CPU实际上将指令分成微操作,并且可以并行执行这些操作。现在情况变得非常复杂了……对我来说甚至太复杂了。

无论如何,我从经验中知道,将数据加载到/从xmm寄存器比将数据加载到/从普通寄存器要慢得多,因此你使用xmm寄存器加速传输的想法从一开始就注定失败。事实上,我很惊讶SSE memmove最终与普通memmove的速度差别不大。


写得非常好,我理解了,虽然我不太了解处理器的实际操作。 - cfeduke
这很好(感谢SSE宽度校正),但实际上并没有回答基本问题:为什么应该只是饱和内存带宽的代码在本机64位下比WOW64的32位表现得更好。瓶颈在哪里? - timday

5

我终于找到了答案(Die in Sente的回答是正确的,谢谢)

在下面的代码中,dst和src都是512MB的std::vector。 我正在使用Intel 10.1.029编译器和CRT。

在64位上,下面两种方式都可以:

memcpy(&dst[0],&src[0],dst.size())

memcpy(&dst[0],&src[0],N)

其中N之前声明为const size_t N=512*(1<<20);时,调用

__intel_fast_memcpy

它的主要部分包括:

  000000014004ED80  lea         rcx,[rcx+40h] 
  000000014004ED84  lea         rdx,[rdx+40h] 
  000000014004ED88  lea         r8,[r8-40h] 
  000000014004ED8C  prefetchnta [rdx+180h] 
  000000014004ED93  movdqu      xmm0,xmmword ptr [rdx-40h] 
  000000014004ED98  movdqu      xmm1,xmmword ptr [rdx-30h] 
  000000014004ED9D  cmp         r8,40h 
  000000014004EDA1  movntdq     xmmword ptr [rcx-40h],xmm0 
  000000014004EDA6  movntdq     xmmword ptr [rcx-30h],xmm1 
  000000014004EDAB  movdqu      xmm2,xmmword ptr [rdx-20h] 
  000000014004EDB0  movdqu      xmm3,xmmword ptr [rdx-10h] 
  000000014004EDB5  movntdq     xmmword ptr [rcx-20h],xmm2 
  000000014004EDBA  movntdq     xmmword ptr [rcx-10h],xmm3 
  000000014004EDBF  jge         000000014004ED80 

并且以大约2200兆字节/秒的速度运行。

但在32位系统上

memcpy(&dst[0],&src[0],dst.size())

调用

__intel_fast_memcpy

其中大部分是

  004447A0  sub         ecx,80h 
  004447A6  movdqa      xmm0,xmmword ptr [esi] 
  004447AA  movdqa      xmm1,xmmword ptr [esi+10h] 
  004447AF  movdqa      xmmword ptr [edx],xmm0 
  004447B3  movdqa      xmmword ptr [edx+10h],xmm1 
  004447B8  movdqa      xmm2,xmmword ptr [esi+20h] 
  004447BD  movdqa      xmm3,xmmword ptr [esi+30h] 
  004447C2  movdqa      xmmword ptr [edx+20h],xmm2 
  004447C7  movdqa      xmmword ptr [edx+30h],xmm3 
  004447CC  movdqa      xmm4,xmmword ptr [esi+40h] 
  004447D1  movdqa      xmm5,xmmword ptr [esi+50h] 
  004447D6  movdqa      xmmword ptr [edx+40h],xmm4 
  004447DB  movdqa      xmmword ptr [edx+50h],xmm5 
  004447E0  movdqa      xmm6,xmmword ptr [esi+60h] 
  004447E5  movdqa      xmm7,xmmword ptr [esi+70h] 
  004447EA  add         esi,80h 
  004447F0  movdqa      xmmword ptr [edx+60h],xmm6 
  004447F5  movdqa      xmmword ptr [edx+70h],xmm7 
  004447FA  add         edx,80h 
  00444800  cmp         ecx,80h 
  00444806  jge         004447A0

它的速度仅为每秒约1350兆字节。

然而

memcpy(&dst[0],&src[0],N)

其中N是之前声明的const size_t N=512*(1<<20);,在32位上编译为直接调用一个

__intel_VEC_memcpy

大部分由...组成
  0043FF40  movdqa      xmm0,xmmword ptr [esi] 
  0043FF44  movdqa      xmm1,xmmword ptr [esi+10h] 
  0043FF49  movdqa      xmm2,xmmword ptr [esi+20h] 
  0043FF4E  movdqa      xmm3,xmmword ptr [esi+30h] 
  0043FF53  movntdq     xmmword ptr [edi],xmm0 
  0043FF57  movntdq     xmmword ptr [edi+10h],xmm1 
  0043FF5C  movntdq     xmmword ptr [edi+20h],xmm2 
  0043FF61  movntdq     xmmword ptr [edi+30h],xmm3 
  0043FF66  movdqa      xmm4,xmmword ptr [esi+40h] 
  0043FF6B  movdqa      xmm5,xmmword ptr [esi+50h] 
  0043FF70  movdqa      xmm6,xmmword ptr [esi+60h] 
  0043FF75  movdqa      xmm7,xmmword ptr [esi+70h] 
  0043FF7A  movntdq     xmmword ptr [edi+40h],xmm4 
  0043FF7F  movntdq     xmmword ptr [edi+50h],xmm5 
  0043FF84  movntdq     xmmword ptr [edi+60h],xmm6 
  0043FF89  movntdq     xmmword ptr [edi+70h],xmm7 
  0043FF8E  lea         esi,[esi+80h] 
  0043FF94  lea         edi,[edi+80h] 
  0043FF9A  dec         ecx  
  0043FF9B  jne         ___intel_VEC_memcpy+244h (43FF40h) 

这段代码的运行速度大约为2100兆字节/秒(并且证明了32位不受带宽限制)。

我撤回了自己写的类似memcpy的SSE代码在32位版本中速度受到了1300兆字节的限制的说法。现在我已经没有任何问题,无论是在32位还是64位模式下,都能够获得超过2GB/s的速度;技巧(如上面的结果所示)是使用非临时存储("streaming")指令(例如_mm_stream_ps内置函数)。

看起来有点奇怪的是,32位"dst.size()" memcpy函数最终并没有调用更快的"movnt"版本(如果你进入memcpy函数,会发现有大量的CPUID检查和启发式逻辑,例如将要复制的字节数与缓存大小进行比较等等),但至少现在我理解了观察到的行为(它与SysWow64或硬件无关)。


3
当然,你真正需要查看的是在memcpy的最内层循环中执行的实际机器指令,通过使用调试器进入机器代码。其他任何事情都只是猜测。
我猜它可能与32位与64位本身无关;我猜更快的库例程是使用SSE非暂态存储器写入编写的。
如果内部循环包含任何常规的加载存储指令变化, 那么目标内存必须读取到机器缓存中,修改并重新写入。由于该读取是完全不必要的——被读取的位立即被覆盖——您可以通过使用“非暂态”写入指令来节省一半的内存带宽,从而绕过缓存。这样,目标内存只需进行一次写入,就可以进行单向传输到内存,而不是往返传输。
我不知道英特尔编译器的CRT库,所以这只是一个猜测。 32位libCRT没有理由不能做同样的事情,但是您引用的加速度在我转换movdqa指令为movnt时是我预期的范围...
由于memcpy没有进行任何计算,因此它始终受制于您可以读取和写入内存的速度。

原来你对于非时序存储是正确的。请查看我的答案以获取详细的汇编级别细节。根本问题似乎是Intel编译器/CRT在32位中并不总是使用其非时序版本的memcpy。 - timday

1

我随口猜测,64位进程正在使用处理器的本机64位内存大小,这优化了内存总线的使用。


1

感谢积极的反馈!我觉得我可以部分解释一下正在发生的事情。

当仅计时memcpy调用时,使用非临时存储进行memcpy绝对是最快的。

另一方面,如果你正在对一个应用进行基准测试,movdqa存储的好处在于它们将目标内存保留在缓存中。或者至少是适合缓存的部分。

所以,如果你正在设计一个运行时库,并且可以假设调用memcpy的应用程序将立即使用目标缓冲区,那么你将希望提供movdqa版本。这有效地优化掉了从内存返回到CPU的行程,随后的所有指令都会运行得更快。

但另一方面,如果目标缓冲区相对于处理器的缓存来说很大,那么这种优化就不起作用了,movntdq版本将给你带来更快的应用程序基准测试结果。

因此,memcpy的想法在内部将有多个版本。当目标缓冲区与处理器的高速缓存相比较小时,使用movdqa,否则,当目标缓冲区与处理器的高速缓存相比较大时,使用movntdq。听起来这就是32位库中正在发生的事情。

当然,所有这些都与32位和64位之间的差异无关。

我的猜想是,64位库只是不够成熟。开发人员还没有提供该库版本中的两种例程。


是的,关于在后复制中想要缓存处于什么状态的整个问题是一个有趣的问题。我正在使用>256MByte的拷贝。如果我拷贝一些与缓存大小更相似的东西,我会看到我所看过的所有memcpy从流式(非暂态)存储合理地恢复到常规移动。 - timday

0
这是一个针对64位架构特别设计的memcpy例程示例。
void uint8copy(void *dest, void *src, size_t n){
    uint64_t * ss = (uint64_t)src;
    uint64_t * dd = (uint64_t)dest;
    n = n * sizeof(uint8_t)/sizeof(uint64_t); 

    while(n--)
        *dd++ = *ss++;
}//end uint8copy()

完整的文章在这里: http://www.godlikemouse.com/2008/03/04/optimizing-memcpy-routines/


这很好,但如果你在现代x86上使用基于所谓的非临时存储(例如Intel编译器CRT提供的存储方式)的优秀memcpy进行基准测试,那么你的程序会更慢。 - timday
2
顺便说一句,这个网站非常花哨,但如果你要写可信的优化文章,就需要对比其他方案,并给出每个方案的定量时间结果作为证据,以证明某种特定方法更好。你显然有能力做到这一点(例如你关于文件写入性能的文章);我建议你重新审视一下你的文章,至少先比较一下你的代码性能与系统memcpy的性能。 - timday

0

我手头没有参考资料,所以对于时间/指令我并不完全确定,但是我可以给出理论。如果你在32位模式下进行内存移动,你会执行类似于“rep movsd”的操作,每个时钟周期移动一个32位值。在64位模式下,你可以执行“rep movsq”,每个时钟周期移动一个64位值。该指令在32位代码中不可用,因此你需要执行2次rep movsd(每个周期1个)才能达到一半的执行速度。

非常简化,忽略了所有的内存带宽/对齐问题等,但这就是一切开始的地方...


但这并不能解释为什么通过SSE寄存器(无论您处于32位还是64位模式,它们都是128位)进行代码复制似乎在32位中受到带宽限制。 - timday
SSE寄存器应该以数据总线的宽度(64位)进行存储。然而,由于我没有时间表在手边,SSE存储可能会使用正常寄存器存储的两倍时钟周期,因此具有与32位复制相同的数据速率。 - Brian Knoblauch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接