当前版本: 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
#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
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
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 看输出是否正确