我正在调查一个应用程序中的性能热点,该应用程序在memmove(3)中花费了50%的时间。应用程序将数百万个4字节的整数插入到排序数组中,并使用memmove将数据“向右”移位,以便为插入的值腾出空间。
我的期望是复制内存是非常快的,我很惊讶在memmove上花了这么多时间。但是后来我有了一个想法,memmove之所以很慢,是因为它移动重叠的区域,这必须在一个紧密的循环中实现,而不是复制大量的内存页面。我写了一个小的微基准测试,看看memcpy和memmove之间是否存在性能差异,希望memcpy能轻松取胜。
我在两台机器(核心i5,核心i7)上运行我的基准测试,发现memmove实际上比memcpy更快,在更老的核心i7上甚至快了近两倍!现在我正在寻找解释。
这是我的基准测试。它使用memcpy复制100mb,然后使用memmove移动大约100mb;源和目标重叠。尝试了源和目标的各种“距离”。每个测试运行10次,打印平均时间。
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
这是在核心i5上的结果(Linux3.5.0-54-generic#81~Predicise1-Ubuntu SMP x86_64 GNU/Linux,gcc是4.6.3 (Ubuntu/Linaro4.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行以上)
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
我根据不同的答案运行了一些不同的测试。
当运行两次memcpy时,第二次运行比第一次快。当“接触”memcpy的目标缓冲区( faster.
结果如下:
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缓存更友好。
感谢所有精彩的回答和评论!
发布于 2015-02-20 15:56:07
当您的memcpy
源和目标完全不同时,您的memmove
调用会将内存混洗2到128个字节。不知何故,这就是性能差异的原因:如果你复制到相同的位置,你会看到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
变得不必要地变慢并没有错。
发布于 2015-02-20 16:53:43
当您使用memcpy
时,写操作需要进入缓存。当您使用memmove
时,当您向前复制一小步时,您正在复制的内存将已经在缓存中(因为它被读取了2、4、16或128个字节的“后退”)。尝试执行目标为几兆字节(> 4*缓存大小)的memmove
,我怀疑(但不必费心测试)您会得到类似的结果。
我保证,当您执行大内存操作时,所有操作都与缓存维护有关。
发布于 2015-02-20 16:06:33
从历史上看,memmove和memcopy是相同的功能。它们以相同的方式工作,具有相同的实现。然后人们意识到,memcopy不需要(通常也不是)定义来以任何特定的方式处理重叠区域。
最终的结果是,memmove被定义为以特定的方式处理重叠区域,即使这会影响性能。Memcopy应该使用可用于非重叠区域的最佳算法。实现通常是几乎相同的。
您遇到的问题是,x86硬件有如此之多的变体,以至于无法判断哪种转移内存的方法将是最快的。即使你认为在一种情况下你有一个结果,像在内存布局中有一个不同的“步幅”这样简单的事情也会导致巨大不同的缓存性能。
您可以对实际正在做的事情进行基准测试,也可以忽略该问题并依赖于为C库所做的基准测试。
编辑:哦,还有最后一件事;移动大量内存内容非常慢。我猜使用类似于简单的B-Tree实现来处理整数,您的应用程序会运行得更快。(哦,你是,好吧)
Edit2:在评论中总结我的扩展:微基准是这里的问题,它不是衡量你认为它是什么。分配给memcpy和memmove的任务彼此有很大的不同。如果使用memmove或memcpy多次重复指定给memcpy的任务,则最终结果将不取决于您使用哪个内存移位函数,除非区域重叠。
https://stackoverflow.com/questions/28623895
复制相似问题