memcpy的效率问题

最近又听到有人在讲memcpy的效率问题,实在是忍不住了,基本上很多人对memcpy的原理都是一知半解, 大部分认为memcpy是一个char到char的拷贝的循环,担心它的效率。
实际上,memcpy是一个效率最高的内存拷贝函数,他不会那么傻,来做一个一个字节的内存拷贝,在地址不对齐 的情况下,他是一个字节一个字节的拷,地址对齐以后,就会使用CPU字长来拷(和dma类似),32bit或64bit,还会根据cpu的类型来选择一些 优化的指令来进行拷贝。
总的来说,memcpy的实现是和CPU类型、操作系统、cLib相关的。毫无疑问,它是内存拷贝里效率最高 的,请放心使用。
 
在网上找到一片相应的文章,可说明情况:
 
龙芯版 memcpy 的实现
作者: comcat   发表日期: 2007-06-15 17:50   复制链接
 
 
memcpy 是为最常用之函数,多媒体编解码过程中调用频繁,属调用密集型函数,对其性能优化很有意义。
1. 概述
memcpy 所做的操作是把内存中的一块数据复制到内存的另一个地方,也就是内存到
内存的数据拷贝,这个过程需要CPU的参与,即: 先从内存取数据到CPU的寄存器,然
后再从寄存器写到内存中。可以用类似如下C 代码实现:
char *dest = (char *)to;
char *src = (char *)from;
int i = size -1;
while(i >= 0)
{
    *(dest + i) = *(src + i);
    i--;
}
这个在size 比较小时,性能尚可。倘若size 很大,这种每次取一个字节的方式,远
没有充分发挥CPU的数据带宽。作为一种改进 方式,我们可以每次取4个字节进行写入,
不足4字节的部分,依然每次取一个字节写入:
int *dest = (int *)to;
int *src = (int *)from;
int word_num = size/4 - 1;
int slice = size%4 - 1;
while(word_num >= 0)
{
    *dest= *src;
    dest += 4;
    src += 4;
    word_num--;
}
while(slice >= 0)
{
    *((char *)dest + slice) = *((char *)src + slice);
    slice--;
}
上面这个就是 memcpy 优化的基本思想。
龙芯2E因为是64位,可以使用ld指令,每次取8个字节写入目标地址。
上面的例子,为了说明问题的方便,没有考虑指针的是否对齐。因为不对齐访问会触发
异常,所以使用汇编码写高性能 memcpy 时,对指针是否对齐要分情况予以周密的处理。

2. glibc 对memcpy的优化分析
先看看glibc 对memcpy的实现。

  1     #include <endian.h>
  2     #include <sys/asm.h>
  3     #include "regdef.h"

  4     /* void *memcpy(void *dest, const void *src, size_t n);
  5       a0 <--- dest; a1 <--- src; a2 <--- n    
  6     */

  7         .global     memcpy
  8         .ent     memcpy
  9     memcpy:
  10         .set     noreorder
  11         .set     mips3
  12         slti     t0, a2, 16         # 小于16字节,则跳转到last16处,由其进行处理
  13         bne     t0, zero, last16
  14         move     v0, a0               # 目标指针作为返回值写入v0,注意此指令位于延迟槽中
  15         xor     t0, a1, a0               # 都对齐,或者都不对齐(低3位相同)
  16         andi     t0, 0x7
  17         bne     t0, zero, shift         # 低3位不相同,则跳转。则肯定有一个不对齐,或者都不对齐(低3位不同)
  18         subu     t1, zero, a1
  19                                             # 都对齐,或者都不对齐(低3位相同)
  20         andi     t1, 0x7               # t1 的值实际上为 8 - (a1 & 0x7),即a1 加上 t1 就对齐了。
  21         beq     t1, zero, chk8w         # a1 aligned, then branch
  22         subu     a2, t1                  
                                              # 不跳转则 a1 不对齐,此时 a0 肯定不对齐,而且他们的低3位相同
  23         ldr     t0, 0(a1)         # 把低 t1 个字节,使用非对齐访问指令特别照顾
  24         addu     a1, t1               # 指针前移,a1 对齐矣    
  25         sdr     t0, 0(a0)         # 把低 t1 个字节,写到 a0 开始处的 t1 字节的空间里
  26         addu     a0, t1               # 指针前移,a0 对齐矣
  27     chk8w:
  28         andi     t0, a2, 0x3f         # t0 = a2 % 64
  29         beq     t0, a2, chk1w         # 小于 64 字节则跳转到chk1w
  30         subu     a3, a2, t0         # a3 = a2 / 64
  31         addu     a3, a1               # a3 = end address of loop
  32         move     a2, t0               # a2 = what will be left after loop,是为不足64字节部分
  33     lop8w:    
  34         ld     t0, 0(a1)             # 每次循环处理64个字节
  35         ld     t1, 8(a1)
  36         ld     t2, 16(a1)
  37         ld     t3, 24(a1)
  38         ld     ta0, 32(a1)
  39         ld     ta1, 40(a1)
  40         ld     ta2, 48(a1)
  41         ld     ta3, 56(a1)
  42         addiu     a0, 64               # 指针前移 64 字节
  43         addiu     a1, 64               # 同上
  44         sd     t0, -64(a0)
  45         sd     t1, -56(a0)
  46         sd     t2, -48(a0)
  47         sd     t3, -40(a0)
  48         sd     ta0, -32(a0)
  49         sd     ta1, -24(a0)
  50         sd     ta2, -16(a0)
  51         bne     a1, a3, lop8w
  52         sd     ta3, -8(a0)
  53     chk1w:
  54         andi     t0, a2, 0x7         # 8 or more bytes left?
  55         beq     t0, a2, last16         # less than 8 bytes, then jump to last16
  56         subu     a3, a2, t0         # Yes, handle them one dword at a time
  57         addu     a3, a1               # a3 again end address
  58         move     a2, t0
  59     lop1w:
  60         ld     t0, 0(a1)
  61         addiu     a0, 8
  62         addiu     a1, 8
  63         bne     a1, a3, lop1w
  64         sd     t0, -8(a0)
  65     last16:                                 # 可改进
  66         blez     a2, lst16e         # Handle last 16 bytes, one at a time
  67         addu     a3, a2, a1         # a3 为终止标志
  68     lst16l:
  69         lb     t0, 0(a1)
  70         addiu     a0, 1
  71         addiu     a1, 1
  72         bne     a1, a3, lst16l
  73         sb     t0, -1(a0)
  74     lst16e:
  75         jr     ra                       # Bye, bye
  76         nop
  77     shift:                                         # 为何没有考虑 a1 对齐,a0 不对齐的情况??
  78         subu     a3, zero, a0         # a1 不对齐,a0 可能对齐,可能不对齐
  79         andi     a3, 0x7               # (unoptimized case...)
  80         beq     a3, zero, shft1         # a0 对齐,a1 不对齐,则跳转到shft1处
  81                                               # 此时 a0不对齐,a1 未知,且低3位不同
                                            # 当a1对齐时,可否直接对a1使用对齐访问,a0使用不对齐访问
  82         subu     a2, a3               # a2 = bytes left
  83         ldr     t0, 0(a1)         # Take care of first odd part
  84         ldl     t0, 7(a1)
  85         addu     a1, a3
  86         sdr     t0, 0(a0)
  87         addu     a0, a3               # 到这里,a0 对齐了,a1 反正也不想使其对齐
  88     shft1:                                 # 此种情况亦可添加 64 字节为单位的处理块
  89         andi     t0, a2, 0x7
  90         subu     a3, a2, t0         # a3 为8的倍数
  91         addu     a3, a1               # a3 = end address of loop
  92     shfth:
  93         ldr     t1, 0(a1)         # Limp through, dword by dword
  94         ldl     t1, 7(a1)
  95         addiu     a0, 8
  96         addiu     a1, 8
  97         bne     a1, a3, shfth
  98         sd     t1, -8(a0)
  99         b     last16               # Handle anything which may be left
  100         move     a2, t0
  101         .set     reorder
  102         .end     memcpy
下面详细分析之:
1. 12-14行,首先对要复制的数据大小进行判断,小于 16 字节的直接跳转到 last16 处进行处理。
2. 15-17行,在于区分 dest 与 src 指针对齐的情况。两者低 3 位相同(异或为0),则要么都对齐,
  要么都不对齐,这两种情况可以合并起来处理。若两者低 3 位不同(异或不为0),则肯定有一个
  不对齐,或者都不对齐,这种情况要跳转到 shift 处去处理。
3. 18-21行,对 dest 和 src 都对齐或都不对齐的情况予以区分,因为二者对齐情况相同,故而只判断
  a1 的情况即可。如果 a1 对齐,则直接跳转到 chk8w 处,先以 8 字节为单位,取数据写入之。
  注意 18 行执行完了,t1 中是 -a1 的补码,等价于 ~a1 + 1,只有 a1 低 3 位都为 0 时,t1 的低
  3 位才都为 0。
4. 22-26行,处理 dest 和 src 都不对齐的情况。注意此时 dest 与 src 的低 3 位是相同的。直接使用
  非对齐访问指令,获取 t1 个字节,写入目的地。则 dest 与 src 就可以跨到第一个对齐地址处(dest
  +t1, src+t1),此后的处理方式就和对齐的指针一样了。
5. 27-52行,处理数据块大于64字节的情况,每次循环写入 64 字节的数据。a0,a1,a2 同步移动,该
  程序块运行后,不足64字节部分交由 chk1w 处理。
6. 53-64行,处理数据块小于64字节的情况,每次循环写入 8字节。a0,a1,a2 同步移动。经其处理后,
  不足8字节部分,则交由 last16 开始的程序块处理。
7. 65-76行,以字节为单位写入数据。负责处理最后的16字节。可以将循环展开,优化之。
8. 78-80行,判断 a0 是否对齐,对齐则跳转到 shft1 处执行。
9. 81-87行,使a0对齐。
10. 88-100行,以8字节为单位,处理 a0 对齐,a1不对齐的情况。不足 8 字节的部分,交由 last16 处理。
注意 77-100 行这一块,是对a0,a1低3位不同的情况进行的处理。当a0,a1低3位不同时,可能有以下几
种情况:
I. a0 对齐,a1 不对齐
II. a0 不对齐,a1 不对齐(低3位不同)
III. a0 不对齐,a1 对齐
特别留意一下,其对第三种情况的处理,是首先将其转化为第一种情况,然后再对其进行处理的。
对第二种情况的处理也是这样的。
 
3. 针对龙芯的改进
A. 细化各种情况
81行处,当a0不对齐,a1对齐时,可直接对a1使用对齐访问,a0使用不对齐访问
这样会减少82-87行的6条指令,多一条分支判断语句。
这样,取数据使用对齐访问指令,写数据使用非对齐访问指令对,效率应该和取数据使用非对齐访问指令对(93,
94 行),写数据使用对齐访问指令相当(97 行)。

B. 短循环展开
该改进的思想参见《龙芯汇编语言的艺术》
65-73 行负责处理小于16字节的部分,可以展开成如下代码:
    last16:
        blez     a2, 2f
        addiu     a2, a2, -1
        sll         a2, a2, 2         # a2 <-- a2 * 4
        la         a3, 1f
        subu     a3, a3, a2
        lb         $15, 0(a1)
        jr         a3
          addiu a3, 2f-1f
       
        lb         $16, 15(a1)
        lb         $17, 14(a1)
        lb         $18, 13(a1)
        lb         $19, 12(a1)
        lb         $20, 11(a1)
        lb         $21, 10(a1)
        lb         $22, 9(a1)
        lb         $23, 8(a1)
        lb         $8, 7(a1)
        lb         $9, 6(a1)
        lb         $10, 5(a1)
        lb         $11, 4(a1)
        lb         $12, 3(a1)
        lb         $13, 2(a1)
        lb         $14, 1(a1)
    1:     jr         a3
          sw   $15, 0(a0)
       
        sb         $16, 15(a0)
        sb         $17, 14(a0)
        sb         $18, 13(a0)
        sb         $19, 12(a0)
        sb         $20, 11(a0)
        sb         $21, 10(a0)
        sb         $22, 9(a0)
        sb         $23, 8(a0)
        sb         $8, 7(a0)
        sb         $9, 6(a0)
        sb         $10, 5(a0)
        sb         $11, 4(a0)
        sb         $12, 3(a0)
        sb         $13, 2(a0)
        sb         $14, 1(a0)
    2: jr         ra
        nop
注意:16~23 号寄存器,使用前需保存,完了要恢复。

C. 提升不对齐时大块数据复制的效率
原有实现当检测到a0、a1都不对齐时(低3位不同),将a0整对齐后,直接以8字节为单位,复制数据(见92-98行)
这个应该亦可 以引入先以64字节为单位复制数据,然后再进入以8字节为单位的处理流程,这个应该可以提升大快数据复制时的效率,目前这个也是猜想,需要进一步的大量测 试。

注: 目前只测试了last16处短循环展开的改进,功能正确,效率尚未评测。测试程序见 http://people.openrays.org/~comcat/misc/memcpy.tar , 内有 2 个文件 godson_memcpy.S mem_test.c,如下命令编译之:
gcc godson_memcpy.S mem_test.c -o mtest

./mtest 看输出是否正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值