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

Posted

技术标签:

【中文标题】32 位和 64 位进程之间的 memcpy 性能差异【英文标题】:memcpy performance differences between 32 and 64 bit processes 【发布时间】:2010-09-21 03:06:29 【问题描述】:

我们有配备 XP64 的 Core2 机器 (Dell T5400)。

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

我的问题是,这实际上有什么区别 由于 ? 32位进程是否必须跳过 一些额外的 WOW64 箍来获取内存?是不是什么东西 与 TLB 或预取器有关,或者......什么?

感谢您的任何见解。

也在Intel forums上提出。

【问题讨论】:

您是说您的 SSE 代码在本机 64 位模式下的速度也是 WOW64 中的两倍吗?您是否在 32 位 XP 上对其进行了基准测试,以查看 WOW64 是否影响性能? 是的,就是这样。 32 位操作系统测试是一个很好的建议……但不幸的是,我们没有任何与 32 位操作系统等效的硬件!我希望有人能告诉我WOW64是否是问题所在。将考虑获得 32 位安装。 【参考方案1】:

我认为以下可以解释:

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

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 here)。

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

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

好的,现在您认为当使用更大的 SSE 寄存器时,您应该获得更快的吞吐量,对吧? AFAIK xmm 寄存器不是 256,而是 128 位宽,顺便说一句(reference at Wikipedia)。但是,您是否考虑过延迟和吞吐量?您要移动的数据是否 128 位对齐。根据这一点,您可以使用移动它

movdqa xmm1, [address]
movdqa [address2], xmm1

如果没有对齐

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

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

还有一些我们甚至没有考虑到的事实是 CPU 实际上将指令拆分为微操作,并且它可以并行执行这些操作。现在它开始变得非常复杂......甚至对我来说太复杂了。

无论如何,根据经验,我知道向/从 xmm 寄存器加载数据比向/从普通寄存器加载数据要慢得多,因此您使用 xmm 寄存器加快传输速度的想法从一开始就注定要失败。我真的很惊讶,最终 SSE memmove 并没有比正常的慢很多。

【讨论】:

写得很好,我明白了,但我不太了解处理器的实际运行方式。 这一切都很好(感谢 SSE 宽度校正),但它实际上并没有回答基本问题:为什么应该简单地使内存带宽饱和的代码在本机 64 位而不是比作为WOW64下的32位。瓶颈在哪里?【参考方案2】:

我终于明白了这一点(并且 Die in 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/limit;我现在没有任何问题 在 32 位或 64 位上获得 >2GByte/s;诀窍(如上述结果提示) 是使用非临时(“流式”)存储(例如_mm_stream_psintrinsic)。

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

【讨论】:

【参考方案3】:

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

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

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

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

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

【讨论】:

是的,事实证明你对非临时存储是正确的。有关粗糙的 asm 级详细信息,请参阅我的答案。根本问题似乎是英特尔编译器/CRT 并不总是在 32 位中使用它的非临时版本的 memcpy。【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

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

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

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

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

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

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

【讨论】:

是的,你希望缓存在复制后处于什么状态的整个问题是一个有趣的问题。我正在使用 >256MByte 的副本。如果我复制与缓存大小更相似的内容,我会看到我查看过的所有 memcpy 都从流式(非临时)存储明显地恢复为传统移动。【参考方案6】:

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

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

【讨论】:

但这并不能解释为什么通过 SSE 寄存器(无论您是在 32 位还是 64 位模式下都是 128 位)复制代码似乎在 32 位中受到带宽限制。 SSE 寄存器应该在数据总线的宽度(64 位)上进行存储。但是,由于我没有摆在我面前的时序,SSE 存储可以使用两倍于普通寄存器存储的时钟周期,因此具有与 32 位副本相同的数据速率。【参考方案7】:

这是一个专门针对 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/

【讨论】:

这一切都很好,但是如果您使用所谓的非临时存储(例如,英特尔编译器 CRT 中提供的存储)在现代 x86 上针对良好的 memcpy 对它进行基准测试,您的会慢一些。 顺便说一句,该网站非常华丽,但是如果您要撰写有关优化的可靠文章,您确实需要与备选方案进行比较,并为每个备选方案提供一些定量时间结果,以证明一种特定方法更好.你显然有能力做到这一点(例如你关于文件写入性能的文章);我强烈建议您重新阅读您的文章,并至少将您的代码性能与系统的 memcpy 进行比较。

以上是关于32 位和 64 位进程之间的 memcpy 性能差异的主要内容,如果未能解决你的问题,请参考以下文章

Windows x64 上 32 位和 64 位应用程序之间的进程间通信

TestComplete 64位和32位之间的区别

为啥 32 位和 64 位 numpy/pandas 之间存在差异

64位和32位是啥?

32 位和 64 位之间的指针增量差异

在两个 C# 应用程序(32 位和 64 位)之间进行 IPC 的最佳方式是啥