我们有Core2机器(戴尔T5400)与XP64。
我们观察到,在运行32位进程时,memcpy的性能在1.2GByte / s左右; 但是64位进程的memcpy达到了2.2GByte / s(或者Intel编译器的CRT的memcpy为2.4GByte / s)。 虽然最初的反应可能只是解释这一点,因为在64位代码中有更广泛的寄存器可用,但是我们观察到我们自己的memcpy-like SSE汇编代码(它应该使用128位宽的加载存储,而不pipe32 /进程的64位)在它实现的复制带宽上展现出类似的上限。
我的问题是,这有什么不同呢? 32位进程是否需要通过一些额外的WOW64箍来获取内存? 这是与TLB或prefetchers或…什么?
感谢您的任何见解。
也在英特尔论坛上提出。
当然,您真的需要查看在memcpy最内层循环内执行的实际机器指令,通过调试器进入机器代码。 别的什么都只是猜测。
我的问题是,它本身可能与32位和64位没有任何关系。 我的猜测是,更快的库例程是使用SSE非临时存储编写的。
如果内部循环包含传统加载存储指令的任何变化,则目标内存必须读入机器的高速缓存中,修改并写回。 由于读取是完全没有必要的 – 正在读取的位会立即被覆盖 – 您可以使用绕过缓存的“非暂时性”写入指令来节省一半的内存带宽。 这样一来,目的地存储器就写成了一个单向的内存而不是往返。
我不知道英特尔编译器的CRT库,所以这只是一个猜测。 没有什么特别的理由,为什么32位libCRT不能做同样的事情,但你引用的加速是在我所期望的只是通过将movdqa指令转换为movnt …
由于memcpy没有进行任何计算,所以总是受限于读取和写入内存的速度。
我认为以下可以解释它:
要将数据从存储器复制到寄存器并返回到存储器,请执行
mov eax, [address] mov [address2], eax
这将32位(4字节)从地址移动到地址2。 在64位模式下也是一样的
mov rax, [address] mov [address2], rax
这从地址到地址2移动64位,2个字节。 根据英特尔的规格,“mov”本身无论是64位还是32位都有0.5的延迟和0.5的吞吐量。 延迟是指令流经管道的时间周期数,吞吐量是CPU再次接受相同的指令之前需要等待的时间。 正如你所看到的,它可以在每个时钟周期执行两个mov,但是,它必须在两个mov之间等待半个时钟周期,因此每个时钟周期只能执行一个mov(或者我在这里错误地误解了这些条件?详情请参阅PDF )。
当然, mov reg, mem
可以超过0.5个周期,具体取决于数据是在第一级还是第二级缓存中,或者根本不在缓存中,需要从内存中抓取。 然而,上面的等待时间忽略了这个事实(因为PDF是我上面链接的),它假定mov所需的所有数据都已经存在了(否则等待时间会增加多少时间才能从任何地方获取数据现在 – 这可能是几个时钟周期,并且完全独立于正在执行的命令,见第482页/ C-30页上的PDF)。
有趣的是,mov是32位还是64位不起作用。 这意味着,除非内存带宽成为限制因素,否则64位mov对32位mov的速度相当快,而且由于使用64位时,只需要一半mov将相同数量的数据从A移到B,吞吐量就可以达到(理论上)要高一倍(事实并非如此,可能是因为记忆不是无限快的)。
好吧,现在你想在使用更大的SSE寄存器时,你应该得到更快的吞吐量,对吧? AFAIK xmm寄存器不是256,而是128位宽,BTW( 参考维基百科 )。 但是,您是否考虑过延迟和吞吐量? 要移动的数据是128位对齐还是不对齐。 根据不同,您可以使用它来移动它
movdqa xmm1, [address] movdqa [address2], xmm1
或者如果不对齐
movdqu xmm1, [address] movdqu [address2], xmm1
那么,movdqa / movdqu的延迟是1,吞吐量是1.所以指令需要执行两倍的时间,并且指令后的等待时间是正常mov的两倍。
还有一些我们甚至没有考虑到的事实是CPU实际上将指令分成微操作并且可以并行地执行这些指令。 现在开始变得非常复杂了,甚至对我来说太复杂了。
无论如何,从经验中我知道从xmm寄存器加载数据比从正常寄存器加载数据要慢得多,因此使用xmm寄存器来加速传输的想法从一开始就注定要失败。 我真的很惊讶,最后上证所的移动并没有比正常的慢很多。
我终于到了这个底部(而Sente的回答是正确的,谢谢)
在下面,dst和src是512 MByte std :: vector。 我正在使用英特尔10.1.029编译器和CRT。
在64bit上
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
并且仅以〜1350MByte / 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(并证明32bit是不是有限的带宽)。
我撤回声明,我自己的类似memcpy的SSE代码在32位版本中遭受类似的约1300 MByte /限制; 我现在没有任何问题获得> 2GByte /秒32或64位; 技巧(如上面的结果提示)是使用非时间(“流”)存储(例如_mm_stream_ps
固有的)。
32位“ dst.size()
”memcpy最终没有调用更快的“ movnt
”版本,这似乎有点奇怪(如果你步入memcpy,那么CPUID
检查和启发式逻辑的数量是最不可思议的,例如比较字节数被复制的高速缓冲存储器的大小等,然后它到任何地方靠近你的实际数据),但至少我理解现在观察到的行为(而不是SysWow64或H / W相关)。
我非常期待的猜测是,64位进程正在使用处理器本身的64位内存大小,从而优化了内存总线的使用。
感谢您的积极反馈! 我想我可以部分解释这里发生了什么。
使用非临时存储memcpy绝对是禁食, 如果你只是计时的memcpy调用。
另一方面,如果您正在对某个应用程序进行基准测试,那么movdqa存储区具有将目标内存保存在缓存中的好处。 或者至少是适合缓存的部分。
所以,如果你正在设计一个运行时库,并且如果你可以假定名为memcpy的应用程序将在memcpy调用之后立即使用目标缓冲区,那么你就需要提供movdqa版本。 这有效地优化了从内存返回到遵循movntdq版本的CPU的行程,并且所有跟在调用之后的指令将运行得更快。
但另一方面,如果目标缓冲区与处理器的缓存相比较大,则该优化不起作用,movntdq版本将为您提供更快的应用程序基准。
所以memcpy的想法将有多个版本。 当目标缓冲区比处理器的缓存小时,使用movdqa,否则,目标缓冲区比处理器的缓存大,请使用movntdq。 这听起来像是在32位库中发生的事情。
当然,这与32位和64位之间的差异没有任何关系。
我的猜测是64位库不够成熟。 开发者们还没有开始在这个版本的库中提供这两个例程。
我在前面没有提及,所以我对时间/指示不是绝对肯定的,但我仍然可以给出理论。 如果你在32位模式下进行内存移动,你会做一些类似“rep movsd”的操作,每个时钟周期移动一个32位的值。 在64位模式下,您可以执行一个“rep movsq”,它在每个时钟周期执行一次64位移动。 该指令不适用于32位代码,因此您需要执行2 x rep movsd(每个循环一次),执行速度只有一半。
忽略所有内存带宽/对齐问题等,但这是一切开始的地方…
下面是一个专门针对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/