为什么memmove比memcpy快?


89

我正在研究某个应用程序中的性能热点,该应用程序将其50%的时间都花在memmove(3)上。该应用程序将数百万个4字节的整数插入已排序的数组中,并使用memmove将数据“右移”,以便为插入的值腾出空间。

我的期望是复制内存的速度非常快,而令我惊讶的是花了这么多时间在记忆体上。但是后来我想到memmove速度很慢,因为它移动的是重叠区域,必须在一个紧密的循环中实现它,而不是复制大内存页。我写了一个小型的微基准测试,以找出memcpy和memmove之间的性能差异,期望memcpy能够胜任。

我在两台机器(核心i5,核心i7)上运行了基准测试,发现memmove实际上比memcpy快,在较旧的i7核心上甚至快两倍。现在,我正在寻找解释。

这是我的基准。它使用memcpy复制100 mb,然后使用memmove复制大约100 mb;源和目标重叠。尝试了源和目的地的各种“距离”。每次测试运行10次,平均时间被打印出来。

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

以下是Core i5(Linux 3.5.0-54-通用#81〜precise1-Ubuntu SMP x86_64 GNU / Linux)上的结果,gcc为4.6.3(Ubuntu / Linaro 4.6.3-1ubuntu5)。括号中的数字为源与目标之间的距离(间隙大小):

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove被实现为SSE优化的汇编代码,从后到前复制。它使用硬件预取将数据加载到缓存中,然后将128个字节复制到XMM寄存器中,然后将其存储在目标位置。

memcpy-ssse3-back.S,行1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

为什么memmove快于memcpy?我希望memcpy复制内存页,这应该比循环快得多。在最坏的情况下,我希望memcpy与memmove一样快。

PS:我知道我无法在代码中用memcpy代替memmove。我知道代码示例混合了C和C ++。这个问题实际上只是出于学术目的。

更新1

我根据各种答案对测试进行了一些变化。

  1. 当运行memcpy两次时,第二次运行比第一次运行快。
  2. 当“触摸” memcpy(memset(b2, 0, BUFFERSIZE...))的目标缓冲区时,memcpy 的第一次运行也更快。
  3. memcpy仍然比memmove慢一点。

结果如下:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

我的结论是:根据@Oliver Charlesworth的评论,操作系统必须在首次访问memcpy目标缓冲区后立即提交物理内存(如果有人知道如何“证明”这一点,请添加答案!) )。此外,正如@Mats Petersson所说,memmove的缓存比memcpy更友好。

感谢您提供的所有出色答案和评论!


1
您查看了memmove代码,还查看了memcpy代码吗?
奥利弗·查尔斯沃思

8
我的期望是复制内存的速度非常快 -仅当内存位于L1缓存中时。当数据不适合缓存时,您的复制性能会下降。
Maxim Egorushkin

1
顺便说一句,您只复制了的一个分支memmove。当源与目标重叠且目标位于较低地址时,此分支无法处理移动。
Maxim Egorushkin

2
我还没有时间访问Linux机器,所以我还不能测试这个理论。但是另一个可能的解释是过度承诺 ; 您的memcpy循环是第一次b2访问的内容,因此,操作系统必须随其提交物理内存。
奥利弗·查尔斯沃思

2
PS:如果这是瓶颈,我会重新考虑该方法。如何将值放入列表或树结构(例如二叉树)中,然后将它们读入数组的末尾。这种方法中的节点将是池分配的极佳候选者。它们仅在批量发布时才添加到最后。如果您一开始就知道需要多少,那就尤其如此。Boost库具有池分配器。
2015年

Answers:


56

您的memmove呼叫将内存重新分配2到128个字节,而memcpy源和目的地却完全不同。某种程度上解释了性能差异:如果复制到同一位置,您memcpy可能会看到更快的结束速度,例如在ideone.com上

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

虽然几乎没有任何内容-没有证据表明写回已存在内存的页面有很大影响,而且我们当然不会看到时间减少了一半……但是它确实表明,memcpy与苹果相比,不必要地减慢速度没有任何错误为苹果。


我本来希望CPU缓存不会引起差异,因为我的缓冲区比缓存大得多。
cruppstahl 2015年

2
但是每一个都需要相同的主存储器访问总数,对吗?(即100MB的读取和100MB的写入)。缓存模式无法解决这个问题。因此,一个可能比另一个慢的唯一方法是,某些东西必须多次从内存中读取/写入。
奥利弗·查尔斯沃思

2
@Tony D-我的结论是要问比我更聪明的人;)
cruppstahl

1
另外,如果您复制到同一位置,但memcpy再次执行该操作会怎样?
奥利弗·查尔斯沃思

1
@OliverCharlesworth:第一次测试运行总是会遭受重大打击,但是要进行两次memcpy测试:memcpy 0.0688002 0.0583162 | memmove 0.0577443 0.05862 0.0601029 ...请参见ideone.com/8EEAcA
Tony Delroy,

24

使用时memcpy,写入需要进入缓存。当您memmove在复制时使用一小步时,正在复制的内存将已经在缓存中(因为已读取2、4、16或128个字节“后退”)。尝试memmove在目标为几兆字节(> 4 *高速缓存大小)的地方执行操作,我怀疑(但不麻烦进行测试)您会得到类似的结果。

当您执行大内存操作时,我保证所有内容都与高速缓存维护有关。


+1我认为由于您提到的原因,向后循环的记忆体比记忆体友好。但是,我发现两次运行memcpy测试时,第二次运行与memmove一样快。为什么?缓冲区太大,以至于第二次运行memcpy的效率(在高速缓存方面)应与第一次运行的效率一样低。因此,这里似乎还有其他因素会导致性能下降。
cruppstahl

3
在适当的情况下,memcpy仅因为TLB已预先填充,一秒钟的速度就会明显加快。而且,一秒钟memcpy也不必清空您可能需要“摆脱”的东西的缓存(肮脏的缓存行在许多方面对性能都是“不好的”。但是要确定地说,您需要运行诸如“性能”之类的东西,并采样诸如高速缓存未命中,TLB未命中之类的东西
Mats Petersson 2015年

15

从历史上看,memmove和memcopy是相同的功能。他们以相同的方式工作并具有相同的实现。然后意识到,不需要(通常也不需要)定义内存复制以任何特定方式处理重叠区域。

最终结果是,memmove被定义为以特定方式处理重叠区域,即使这会影响性能。内存复制应该使用可用于非重叠区域的最佳算法。通常实现几乎是相同的。

您遇到的问题是x86硬件的变化太多,以至于无法确定哪种转移内存的方法最快。而且,即使您认为在某种情况下会导致结果,例如在内存布局中使用不同的“跨度”之类的简单操作也可能导致高速缓存性能大不相同。

您可以基准测试您实际在做什么,也可以忽略该问题,并依靠为C库完成的基准测试。

编辑:哦,还有最后一件事;转移大量内存内容非常缓慢。我猜想您的应用程序将通过简单的B-Tree实现来运行整数,从而运行得更快。(哦,是的,好的)

Edit2:总结我在评论中的扩展:这里的微基准测试是问题所在,它无法衡量您的想法。分配给memcpy和memmove的任务彼此之间存在显着差异。如果使用memmove或memcpy重复执行多次赋予memcpy的任务,则最终结果将取决于您使用的内存移位功能,除非区域重叠。


但这就是它的意义-我正在对自己的实际工作进行基准测试。这个问题是关于解释基准测试结果的,这与您所声称的相矛盾-对于非重叠区域,memcpy更快。
cruppstahl

我的应用程序 b树!每当在叶子节点中插入整数时,就会调用memmove来腾出空间。我正在使用数据库引擎。
cruppstahl

1
您使用的是微型基准测试,甚至没有使内存复制和内存移动相同的数据。您要处理的数据在内存中的确切位置会影响缓存以及CPU必须进行多少次内存往返。
user3710044 2015年

虽然这个答案是正确的,但实际上并没有解释为什么它在这种情况下变慢,而是在说“它变慢,因为在某些情况下它可能变慢”。
奥利弗查尔斯沃思

我是说,在相同的情况下,包括用于复制/移动基准的相同的内存布局,将是相同的,因为实现是相同的。问题出在微基准测试中。
user3710044 2015年

2

“ memcpy比memmove更有效率。” 在您的情况下,运行两个函数时,您很可能没有做完全相同的事情。

通常,仅在必要时使用MEMMEM。当源区域和目标区域非常重叠时,可以使用它。

参考:https : //www.youtube.com/watch?v= Yr1YnOVG-4g杰里·凯恩(Jerry Cain)博士,(斯坦福大学简介系统讲座-7)时间:36:00

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.