内存拷贝的优化方法(1)[转]

 在复杂的底层网络程序中,内存拷贝、字符串比较和搜索操作很容易成为性能瓶颈所在。编译器自带的此类函数虽然做了一些通用性的优化工作,但因为在使用指令集方面受到兼容性的约束,远远没有达到最大限度利用硬件能力的地步。而通过针对特定硬件平台的优化,可以大大提高此类操作的性能。下面我将以P4平台下内存拷贝操作为例,根据AMD提供的一份优化文档中的例子,简要介绍一下如何通过特定指令集,优化内存带宽的使用。虽然因为硬件限制没有达到AMD文档中所说memcpy函数300%的性能提升,但在我机器上实测也有%175-%200的明显性能提升(此数据可能根据机器情况不同)。

     Optimizing Memory Bandwidth from AMD

     按照众所周知的“摩尔”定律,CPU的运算速度每18个月翻一翻,但与此同时内存和外存(硬盘)的速度并无法达到同步增长。这就造成高速CPU与相对低速的内存和外设之间的不同步发展,成为很多程序的瓶颈所在。而如何最大限度提升对现有硬件的利用程度,是算法以下层面优化的主要途径。对内存拷贝操作来说,了解和合理使用Cache是最关键的一点。为追求性能,我们将以牺牲兼容性为代价,因此以下讨论和代码都以P4及以上级别CPU为主,AMD芯片虽然实现上有所区别,但在指令集和整体结构上相同。

     首先我们来看一个最简单的memcpy的汇编实现:
以下为引用:

;
; Flier Lu (flier@nsfocus.com)
;
; nasmw.exe -f win32 fastmemcpy.asm -o fastmemcpy.obj
;
; extern "C" {
;    extern void fast_memcpy1(void *dst, const void *src, size_t size);
; }
;
cpu p4

segment .text use32

global _fast_memcpy1

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy1:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]

   rep movsb

   pop edi
   pop esi
   ret


     这里我为了代码可移植性,使用的是 NASM格式的汇编代码。 NASM是一个非常出色的开源汇编编译器,支持各种平台和中间格式,被开源项目广泛使用,这样可以避免同时使用 VC 的嵌入式汇编和 GCC 中麻烦的 unix 风格 AT&T 格式汇编

     代码初始的cpu p4定义使用p4指令集,因为后面的很多优化工作使用了P4指令集和相关特性;接着的segment .text use32定义此代码在32位代码段;然后global定义标签_fast_memcpy1为全局符号,使得C++代码中可以LINK其.obj后访问此代码;最后%define定义多个宏,用于访问函数参数。

     在C++中只需要定义fast_memcpy1函数格式并链接nasm编译生成的.obj文件即可。NASM编译时 -f 参数指定生成中间文件格式为 MS 的 32 位 COFF 格式,-o 参数指定输出文件名。

     上面这段代码非常简单,适合小内存块的快速拷贝。实际上VC编译器在处理小内存拷贝时,会自动根据情况使用 rep movsb 直接替换 memcpy 函数,通过忽略函数调用和堆栈操作,优化代码长度和性能。

     不过在 32 位的 x86 架构下,完全没有必要逐字节进行操作,使用 movsd 替换 movsb 是必然的选择。
以下为引用:

global _fast_memcpy2

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy2:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]
   shr ecx, 2                   ; convert to DWORD count

   rep movsd

   pop edi
   pop esi
   ret


     为了展示方便,这里假设源和目标内存块本身长度都是64字节的整数倍,并且已经4K页对齐。前者保证单条指令不会出现跨CACHE行访问的情况;后者保证测试速度时不会因为跨页操作影响测试结果。等会分析CACHE时再详细解释为什么要做这种假设。

     不过因为现代CPU大多使用了很长的指令流水线,多条指令并行工作往往比一条指令效率更高,因此 AMD 文档中给出了这样的优化:
以下为引用:

global _fast_memcpy3

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy3:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]
   shr ecx, 2                   ; convert to DWORD count

.copyloop:
   mov eax, dword [esi]
   mov dword [edi], eax

   add esi, 4
   add edi, 4

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret


     标签.copyloop中那段循环实际上完成跟rep movsd指令完全相同的工作,但是因为是多条指令,理论上CPU指令流水线可以并行处理之。故而在AMD的文档中指出能有1.5%的性能提高,不过就我实测效果不太明显。相对而言,当年从486向pentium架构迁移时,这两种方式的区别非常明显。记得Delphi 3还是4中就只是通过做这一种优化,其字符串处理性能就有较大提升。而目前主流CPU厂商,实际上都是通过微代码技术,内核中使用RISC微指令模拟CISC指令集,因此现在效果并不明显。

     然后,可以通过循环展开的优化策略,增加每次处理数据量并减少循环次数,达到性能提升目的。
以下为引用:

global _fast_memcpy4

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy4:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]
   shr ecx, 4                   ; convert to 16-byte size count

.copyloop:
   mov eax, dword [esi]
   mov dword [edi], eax

   mov ebx, dword [esi+4]
   mov dword [edi+4], ebx

   mov eax, dword [esi+8]
   mov dword [edi+8], eax

   mov ebx, dword [esi+12]
   mov dword [edi+12], ebx

   add esi, 16
   add edi, 16

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret


     但这种操作就 AMD 文档上评测反而有 %1.5 性能降低,呵呵。其自己的说法是需要将读取内存和写入内存的操作分组,以使CPU可以一次性搞定。改称以下分组操作就可以比_fast_memcpy3提高3% -_-b
以下为引用:

global _fast_memcpy5

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy5:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]
   shr ecx, 4                   ; convert to 16-byte size count

.copyloop:
   mov eax, dword [esi]
   mov ebx, dword [esi+4]
   mov dword [edi], eax
   mov dword [edi+4], ebx

   mov eax, dword [esi+8]
   mov ebx, dword [esi+12]
   mov dword [edi+8], eax
   mov dword [edi+12], ebx

   add esi, 16
   add edi, 16

   dec ecx
   jnz .copyloop

   pop edi
   pop esi
   ret


     可惜我在P4上实在测不出什么区别,呵呵,大概P4和AMD实现流水线的思路有细微的出入吧

     既然进行循环展开,为什么不干脆多展开一些呢?虽然x86下面通用寄存器只有那么几个,但是现在有MMX啊,呵呵,大把的寄存器啊 改称使用MMX寄存器后,一次载入/写入操作可以处理64字节的数据,呵呵,比_fast_memcpy5可以再有7%的性能提升。
以下为引用:

global _fast_memcpy6

%define param        esp+8+4
%define src          param+0
%define dst          param+4
%define len          param+8

_fast_memcpy6:
   push esi
   push edi

   mov esi, [src]               ; source array
   mov edi, [dst]               ; destination array
   mov ecx, [len]               ; number of QWORDS (8 bytes) assumes len / CACHEBLOCK is an integer
   shr ecx, 3

   lea esi, [esi+ecx*8]         ; end of source
   lea edi, [edi+ecx*8]         ; end of destination
   neg ecx                      ; use a negative offset as a combo pointer-and-loop-counter

.copyloop:
   movq mm0, qword [esi+ecx*8]
   movq mm1, qword [esi+ecx*8+8]
   movq mm2, qword [esi+ecx*8+16]
   movq mm3, qword [esi+ecx*8+24]
   movq mm4, qword [esi+ecx*8+32]
   movq mm5, qword [esi+ecx*8+40]
   movq mm6, qword [esi+ecx*8+48]
   movq mm7, qword [esi+ecx*8+56]

   movq qword [edi+ecx*8], mm0
   movq qword [edi+ecx*8+8], mm1
   movq qword [edi+ecx*8+16], mm2
   movq qword [edi+ecx*8+24], mm3
   movq qword [edi+ecx*8+32], mm4
   movq qword [edi+ecx*8+40], mm5
   movq qword [edi+ecx*8+48], mm6
   movq qword [edi+ecx*8+56], mm7

   add ecx, 8
   jnz .copyloop

   emms

   pop edi
   pop esi

   ret


     优化到这个份上,常规的优化手段基本上已经用尽,需要动用非常手段了,呵呵。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值