⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 csdn_文档中心_内存拷贝的优化方法.htm

📁 csdn10年中间经典帖子
💻 HTM
📖 第 1 页 / 共 3 页
字号:
          <TD align=middle width=500></TD></TR></TBODY></TABLE><!--文章说明信息结束//-->
      <TABLE border=0 width=600>
        <TBODY>
        <TR>
          <TD align=left><BR>
            <P><A 
            href="http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&amp;id=1577430">http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&amp;id=1577430</A><BR><A 
            href="http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&amp;id=1577440">http://www.blogcn.com/blog/cool/main.asp?uid=flier_lu&amp;id=1577440</A></P>
            <P>在复杂的底层网络程序中,内存拷贝、字符串比较和搜索操作很容易成为性能瓶颈所在。编译器自带的此类函数虽然做了一些通用性的优化工作,但因为在使用指令集方面受到兼容性的约束,远远没有达到最大限度利用硬件能力的地步。而通过针对特定硬件平台的优化,可以大大提高此类操作的性能。下面我将以P4平台下内存拷贝操作为例,根据AMD提供的一份优化文档中的例子,简要介绍一下如何通过特定指令集,优化内存带宽的使用。虽然因为硬件限制没有达到AMD文档中所说memcpy函数300%的性能提升,但在我机器上实测也有%175-%200的明显性能提升(此数据可能根据机器情况不同)。<BR><BR><A 
            href="http://cdrom.amd.com/devconn/events/gdc_2002_amd.pdf" 
            target=_blank><FONT color=#0000ff>Optimizing Memory 
            Bandwidth</FONT></A> from <A href="http://www.amd.com/" 
            target=_blank>AMD</A><BR><BR>按照众所周知的“摩尔”定律,CPU的运算速度每18个月翻一翻,但与此同时内存和外存(硬盘)的速度并无法达到同步增长。这就造成高速CPU与相对低速的内存和外设之间的不同步发展,成为很多程序的瓶颈所在。而如何最大限度提升对现有硬件的利用程度,是算法以下层面优化的主要途径。对内存拷贝操作来说,了解和合理使用Cache是最关键的一点。为追求性能,我们将以牺牲兼容性为代价,因此以下讨论和代码都以P4及以上级别CPU为主,AMD芯片虽然实现上有所区别,但在指令集和整体结构上相同。<BR><BR>首先我们来看一个最简单的memcpy的汇编实现:<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>;<BR>; Flier Lu 
                      (flier@nsfocus.com)<BR>;<BR>; nasmw.exe -f win32 
                      fastmemcpy.asm -o fastmemcpy.obj<BR>;<BR>; extern "C" 
                      {<BR>; extern void fast_memcpy1(void *dst, const void 
                      *src, size_t size);<BR>; }<BR>;<BR>cpu p4<BR><BR>segment 
                      .text use32<BR><BR>global _fast_memcpy1<BR><BR>%define 
                      param esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy1:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len]<BR><BR>rep 
                      movsb<BR><BR>pop edi<BR>pop 
                esi<BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>这里我为了代码可移植性,使用的是<A href="http://nasm.sourceforge.net/" 
            target=_blank><FONT color=#0000ff>NASM</FONT></A>格式的汇编代码。<A 
            href="http://nasm.sourceforge.net/" target=_blank><FONT 
            color=#0000ff>NASM</FONT></A>是一个非常出色的开源汇编编译器,支持各种平台和中间格式,被开源项目广泛使用,这样可以避免同时使用 
            VC 的嵌入式汇编和 GCC 中麻烦的 unix 风格 AT&amp;T 格式汇编 :P<BR><BR>代码初始的cpu 
            p4定义使用p4指令集,因为后面的很多优化工作使用了P4指令集和相关特性;接着的segment .text 
            use32定义此代码在32位代码段;然后global定义标签_fast_memcpy1为全局符号,使得C++代码中可以LINK其.obj后访问此代码;最后%define定义多个宏,用于访问函数参数。<BR><BR>在C++中只需要定义fast_memcpy1函数格式并链接nasm编译生成的.obj文件即可。NASM编译时 
            -f 参数指定生成中间文件格式为 MS 的 32 位 COFF 格式,-o 
            参数指定输出文件名。<BR><BR>上面这段代码非常简单,适合小内存块的快速拷贝。实际上VC编译器在处理小内存拷贝时,会自动根据情况使用 
            rep movsb 直接替换 memcpy 函数,通过忽略函数调用和堆栈操作,优化代码长度和性能。<BR><BR>不过在 32 位的 
            x86 架构下,完全没有必要逐字节进行操作,使用 movsd 替换 movsb 是必然的选择。<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy2<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy2:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len]<BR>shr ecx, 2 ; 
                      convert to DWORD count<BR><BR>rep movsd<BR><BR>pop 
                      edi<BR>pop 
            esi<BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>为了展示方便,这里假设源和目标内存块本身长度都是64字节的整数倍,并且已经4K页对齐。前者保证单条指令不会出现跨CACHE行访问的情况;后者保证测试速度时不会因为跨页操作影响测试结果。等会分析CACHE时再详细解释为什么要做这种假设。<BR><BR>不过因为现代CPU大多使用了很长的指令流水线,多条指令并行工作往往比一条指令效率更高,因此 
            AMD 文档中给出了这样的优化:<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy3<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy3:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len]<BR>shr ecx, 2 ; 
                      convert to DWORD count<BR><BR>.copyloop:<BR>mov eax, dword 
                      [esi]<BR>mov dword [edi], eax<BR><BR>add esi, 4<BR>add 
                      edi, 4<BR><BR>dec ecx<BR>jnz .copyloop<BR><BR>pop 
                      edi<BR>pop 
            esi<BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>标签.copyloop中那段循环实际上完成跟rep 
            movsd指令完全相同的工作,但是因为是多条指令,理论上CPU指令流水线可以并行处理之。故而在AMD的文档中指出能有1.5%的性能提高,不过就我实测效果不太明显。相对而言,当年从486向pentium架构迁移时,这两种方式的区别非常明显。记得Delphi 
            3还是4中就只是通过做这一种优化,其字符串处理性能就有较大提升。而目前主流CPU厂商,实际上都是通过微代码技术,内核中使用RISC微指令模拟CISC指令集,因此现在效果并不明显。<BR><BR>然后,可以通过循环展开的优化策略,增加每次处理数据量并减少循环次数,达到性能提升目的。<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy4<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy4:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len]<BR>shr ecx, 4 ; 
                      convert to 16-byte size count<BR><BR>.copyloop:<BR>mov 
                      eax, dword [esi]<BR>mov dword [edi], eax<BR><BR>mov ebx, 
                      dword [esi+4]<BR>mov dword [edi+4], ebx<BR><BR>mov eax, 
                      dword [esi+8]<BR>mov dword [edi+8], eax<BR><BR>mov ebx, 
                      dword [esi+12]<BR>mov dword [edi+12], ebx<BR><BR>add esi, 
                      16<BR>add edi, 16<BR><BR>dec ecx<BR>jnz 
                      .copyloop<BR><BR>pop edi<BR>pop 
                  esi<BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>但这种操作就 AMD 文档上评测反而有 %1.5 
            性能降低,呵呵。其自己的说法是需要将读取内存和写入内存的操作分组,以使CPU可以一次性搞定。改称以下分组操作就可以比_fast_memcpy3提高3% 
            -_-b<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy5<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy5:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len]<BR>shr ecx, 4 ; 
                      convert to 16-byte size count<BR><BR>.copyloop:<BR>mov 
                      eax, dword [esi]<BR>mov ebx, dword [esi+4]<BR>mov dword 
                      [edi], eax<BR>mov dword [edi+4], ebx<BR><BR>mov eax, dword 
                      [esi+8]<BR>mov ebx, dword [esi+12]<BR>mov dword [edi+8], 
                      eax<BR>mov dword [edi+12], ebx<BR><BR>add esi, 16<BR>add 
                      edi, 16<BR><BR>dec ecx<BR>jnz .copyloop<BR><BR>pop 
                      edi<BR>pop 
            esi<BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>可惜我在P4上实在测不出什么区别,呵呵,大概P4和AMD实现流水线的思路有细微的出入吧 
            :D<BR><BR>既然进行循环展开,为什么不干脆多展开一些呢?虽然x86下面通用寄存器只有那么几个,但是现在有MMX啊,呵呵,大把的寄存器啊 
            :D 
            改称使用MMX寄存器后,一次载入/写入操作可以处理64字节的数据,呵呵,比_fast_memcpy5可以再有7%的性能提升。<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy6<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy6:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len] ; number of QWORDS 
                      (8 bytes) assumes len / CACHEBLOCK is an integer<BR>shr 
                      ecx, 3<BR><BR>lea esi, [esi+ecx*8] ; end of source<BR>lea 
                      edi, [edi+ecx*8] ; end of destination<BR>neg ecx ; use a 
                      negative offset as a combo 
                      pointer-and-loop-counter<BR><BR>.copyloop:<BR>movq mm0, 
                      qword [esi+ecx*8]<BR>movq mm1, qword [esi+ecx*8+8]<BR>movq 
                      mm2, qword [esi+ecx*8+16]<BR>movq mm3, qword 
                      [esi+ecx*8+24]<BR>movq mm4, qword [esi+ecx*8+32]<BR>movq 
                      mm5, qword [esi+ecx*8+40]<BR>movq mm6, qword 
                      [esi+ecx*8+48]<BR>movq mm7, qword 
                      [esi+ecx*8+56]<BR><BR>movq qword [edi+ecx*8], mm0<BR>movq 
                      qword [edi+ecx*8+8], mm1<BR>movq qword [edi+ecx*8+16], 
                      mm2<BR>movq qword [edi+ecx*8+24], mm3<BR>movq qword 
                      [edi+ecx*8+32], mm4<BR>movq qword [edi+ecx*8+40], 
                      mm5<BR>movq qword [edi+ecx*8+48], mm6<BR>movq qword 
                      [edi+ecx*8+56], mm7<BR><BR>add ecx, 8<BR>jnz 
                      .copyloop<BR><BR>emms<BR><BR>pop edi<BR>pop 
                      esi<BR><BR>ret<BR></BLOCKQUOTE></TD></TR></TBODY></TABLE></BLOCKQUOTE>
            <P><BR><BR>优化到这个份上,常规的优化手段基本上已经用尽,需要动用非常手段了,呵呵。<BR>让我们回过头来看看P4架构下的Cache结构。<BR><BR><A 
            href="http://developer.intel.com/design/pentium4/manuals/253668.htm" 
            target=_blank>The IA-32 Intel Architecture Software Developer's 
            Manual, Volume 3: System Programming 
            Guide</A><BR><BR>Intel的系统变成手册中第十章介绍了IA32架构下的内存缓存控制。因为CPU速度和内存速度的巨大差距,CPU厂商通过在CPU中内置和外置多级缓存提高频繁使用数据的访问速度。一般来说,在CPU和内存之间存在L1, 
            L2和L3三级缓存(还有几种TLB缓存在此不涉及),每级缓存的速度有一个数量级左右的差别,容量也有较大差别(实际上跟$有关,呵呵),而L1缓存更是细分为指令缓存和数据缓存,用于不同的目的。就P4和Xeon的处理器来说,L1指令缓存由Trace 
            Cache取代,内置在NetBust微架构中;L1数据缓存和L2缓存则封装在CPU中,根据CPU档次不同,分别在8-16K和256-512K之间;而L3缓存只在Xeon处理器中实现,也是封装在CPU中,512K-1M左右。<BR>可以通过查看CPU信息的软件如<A 
            href="http://www.pcanalyser.com/eng/" target=_blank><FONT 
            color=#0000ff>CPUInfo</FONT></A>查看当前机器的缓存信息,如我的系统为:<BR>P4 1.7G, 8K 
            L1 Code Cache, 12K L1 Data Cache, 256K L2 
            Cache。<BR><BR>而缓存在实现上是若干行(slot or 
            line)组成的,每行对应内存中的一个地址上的连续数据,由高速缓存管理器控制读写中的数据载入和命中。其原理这里不多罗嗦,有兴趣的朋友可以自行查看Intel手册。需要知道的就是每个slot的长度在P4以前是32字节,P4开始改成64字节。而对缓存行的操作都是完整进行的,哪怕只读一个字节也需要将整个缓存行(64字节)全部载入,后面的优化很大程度上基于这些原理。<BR><BR>就缓存的工作模式来说,P4支持的有六种之多,这里就不一一介绍了。对我们优化有影响的,实际上就是写内存时缓存的表现。最常见的WT(Write-through)写通模式在写数据到内存的同时更新数据到缓存中;而WB(Write-back)写回模式,则直接写到缓存中,暂不进行较慢的内存读写。这两种模式在操作频繁操作(每秒百万次这个级别)的内存变量处理上有较大性能差别。例如通过编写驱动模块操作MTRR强行打开WB模式,在Linux的网卡驱动中曾收到不错的效果,但对内存复制的优化帮助不大,因为我们需要的是完全跳过对缓存的操作,无论是缓存定位、载入还是写入。<BR><BR>好在P4提供了MOVNTQ指令,使用WC(Write-combining)模式,跳过缓存直接写内存。因为我们的写内存操作是纯粹的写,写入的数据一定时间内根本不会被使用,无论使用WT还是WB模式,都会有冗余的缓存操作。优化代码如下:<BR></P>
            <BLOCKQUOTE>
              <TABLE border=0 cellPadding=6 cellSpacing=0 class=ubb_quote 
              width="100%">
                <TBODY>
                <TR>
                  <TD><B>以下为引用:</B> 
                    <BLOCKQUOTE><BR>global _fast_memcpy7<BR><BR>%define param 
                      esp+8+4<BR>%define src param+0<BR>%define dst 
                      param+4<BR>%define len 
                      param+8<BR><BR>_fast_memcpy7:<BR>push esi<BR>push 
                      edi<BR><BR>mov esi, [src] ; source array<BR>mov edi, [dst] 
                      ; destination array<BR>mov ecx, [len] ; number of QWORDS 
                      (8 bytes) assumes len / CACHEBLOCK is an integer<BR>shr 
                      ecx, 3<BR><BR>lea esi, [esi+ecx*8] ; end of source<BR>lea 
                      edi, [edi+ecx*8] ; end of destination<BR>neg ecx ; use a 
                      negative offset as a combo 
                      pointer-and-loop-counter<BR><BR>.copyloop:<BR>movq mm0, 
                      qword [esi+ecx*8]<BR>movq mm1, qword [esi+ecx*8+8]<BR>movq 
                      mm2, qword [esi+ecx*8+16]<BR>movq mm3, qword 
                      [esi+ecx*8+24]<BR>movq mm4, qword [esi+ecx*8+32]<BR>movq 
                      mm5, qword [esi+ecx*8+40]<BR>movq mm6, qword 
                      [esi+ecx*8+48]<BR>movq mm7, qword 
                      [esi+ecx*8+56]<BR><BR>movntq qword [edi+ecx*8], 
                      mm0<BR>movntq qword [edi+ecx*8+8], mm1<BR>movntq qword 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -