9

我们有带 XP64 的 Core2 机器(戴尔 T5400)。

我们观察到,在运行 32 位进程时,memcpy 的性能在 1.2GByte/s 量级;然而,在 64 位进程中的 memcpy 可以达到大约 2.2GByte/s(或使用 Intel 编译器 CRT 的 memcpy 可以达到 2.4GByte/s)。虽然最初的反应可能只是将其解释为由于 64 位代码中可用的寄存器更广泛,但我们观察到我们自己的类似 memcpy 的 SSE 汇编代码(应该使用 128 位宽的加载存储,而不管 32 /64 位进程)展示了它实现的复制带宽的类似上限。

我的问题是,这种差异实际上是由什么引起的?32 位进程是否必须跳过一些额外的 WOW64 箍才能获得 RAM?它与 TLB 或预取器有关还是......什么?

感谢您的任何见解。

也在英特尔论坛上提出。

4

7 回答 7

8

我认为以下内容可以解释它:

要将数据从内存复制到寄存器并返回内存,您可以

mov eax, [address]
mov [address2], eax

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

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

这会将 64 位 2 字节从地址移动到地址 2。根据英特尔的规格,“mov”本身,无论是 64 位还是 32 位,都具有 0.5 的延迟和 0.5 的吞吐量。延迟是指令通过流水线所需的时钟周期数,吞吐量是 CPU 在再次接受同一指令之前必须等待的时间。正如您所看到的,它每个时钟周期可以执行两个 mov,但是,它必须在两个 mov 之间等待半个时钟周期,因此它实际上每个时钟周期只能执行一个 mov(或者我在这里错了并且误解了这些术语?有关详细信息,请参阅此处的 PDF)。

当然,amov reg, mem可以长于 0.5 个周期,具体取决于数据是在 1 级还是 2 级缓存中,或者根本不在缓存中,需要从内存中获取。但是,上面的延迟时间忽略了这个事实(正如我在上面链接的 PDF 状态),它假设 mov 所需的所有数据都已经存在(否则延迟将增加从任何地方获取数据所需的时间)现在 - 这可能是几个时钟周期,并且完全独立于正在执行的命令,如第 482/C-30 页上的 PDF 所述)。

有趣的是,mov 是 32 位还是 64 位都不起作用。这意味着除非内存带宽成为限制因素,否则 64 位 mov 与 32 位 mov 一样快,并且由于使用 64 位时将相同数量的数据从 A 移动到 B 只需要一半的 mov,因此吞吐量可以(理论上)是两倍高(事实并非如此,可能是因为内存不是无限快的)。

好的,现在您认为当使用更大的 SSE 寄存器时,您应该获得更快的吞吐量,对吧?AFAIK 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 最终并没有比正常的慢很多。

于 2008-11-06T18:28:06.117 回答
5

我终于明白了这一点(并且死在 Sente 的答案是正确的,谢谢)

在下面,dst 和 src 是 512 MByte 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 MByte/s 的速度运行。

但是在 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 MByte/s 的速度运行。

然而

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) 

并以 ~2100MByte/s 的速度运行(证明 32 位不受带宽限制)。

我撤回了我自己的类似 memcpy 的 SSE 代码在 32 位构建中遭受类似的 ~1300 MByte/限制的说法;我现在在 32 位或 64 位上获得 >2GByte/s 没有任何问题;诀窍(如上述结果提示)是使用非临时(“流式”)存储(例如_mm_stream_ps内在)。

dst.size()32 位“ ”memcpy 最终不会调用更快的“ ”版本似乎有点奇怪movnt(如果您进入 memcpy,则会有最令人难以置信的CPUID检查和启发式逻辑,例如将要复制的字节数与缓存大小等进行比较在它接近您的实际数据之前),但至少我现在了解观察到的行为(而且它与 SysWow64 或硬件无关)。

于 2009-02-11T12:58:46.077 回答
3

当然,您确实需要通过使用调试器单步执行机器代码来查看在 memcpy 最内层循环中正在执行的实际机器指令。其他一切都只是猜测。

我的问题是它可能与 32 位和 64 位本身没有任何关系;我的猜测是更快的库例程是使用 SSE 非临时存储编写的。

如果内部循环包含传统加载-存储指令的任何变体,则必须将目标内存读入机器的缓存、修改并写回。由于该读取完全没有必要——正在读取的位会立即被覆盖——您可以通过使用绕过缓存的“非临时”写入指令来节省一半的内存带宽。那样的话,目标内存只是写成单程到内存而不是往返。

我不知道英特尔编译器的 CRT 库,所以这只是一个猜测。没有什么特别的原因为什么 32 位 libCRT 不能做同样的事情,但是你引用的加速在我所期望的范围内,只需将 movdqa 指令转换为 movnt ......

由于 memcpy 不进行任何计算,因此它始终受到您读写内存的速度的限制。

于 2008-11-25T20:20:05.240 回答
1

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

于 2008-11-06T17:00:50.350 回答
1

感谢您的积极反馈!我想我可以部分解释这里发生了什么。

如果您只是为 memcpy 调用计时,那么使用memcpy 的非临时存储绝对是禁食的。

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

因此,如果您正在设计一个运行时库,并且如果您可以假设调用 memcpy 的应用程序将在 memcpy 调用后立即使用目标缓冲区,那么您将需要提供 movdqa 版本。这有效地优化了从内存返回到将遵循 movntdq 版本的 cpu 的行程,并且调用之后的所有指令将运行得更快。

但另一方面,如果目标缓冲区与处理器的缓存相比较大,则该优化不起作用,并且 movntdq 版本将为您提供更快的应用程序基准测试。

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

当然,这一切都与 32 位和 64 位的区别无关。

我的猜测是 64 位库还不够成熟。开发人员还没有开始在该版本的库中提供这两个例程。

于 2009-02-12T05:16:04.617 回答
0

我面前没有参考资料,所以我对时间/说明并不是绝对肯定的,但我仍然可以给出理论。如果您在 32 位模式下进行内存移动,您将执行类似“rep movsd”的操作,它会在每个时钟周期移动一个 32 位值。在 64 位模式下,您可以执行“rep movsq”,它在每个时钟周期执行一次 64 位移动。该指令不适用于 32 位代码,因此您将以一半的执行速度执行 2 x rep movsd(以 1 个周期)。

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

于 2008-11-11T17:43:31.963 回答
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/

于 2010-12-17T23:40:38.687 回答