当前版本: 0.1
完成日期: 2007-6-15
作者: Dajie Tan <jiankemeng@gmail.com>
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. 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
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 看输出是否正确