前阵的时候领导提出要对memcpy做一个测试。也没有交代具体的细节。我就直接自已写了个常规memcpy,然后再把内核的__memcpy和glibc的memcpy整出来,对三者进行比较,不比不知道,一比吓一大跳。
测试结论是:
kernel和glibc的memcpy性能良好,glibc比内核的略高。自已写的就惨不忍睹了。
测试程序大概是先定义两块1G的全局内存。然后先用memset将其全置为1或者其它数。原理道上的兄弟都知道,linux内核很聪明,只要在你实际要用的到的时候才会产生缺页中断为你分配内存,否则只会在虚拟内存映射表中占个位置。
接下来就是循环拷贝了。
通过对每次拷贝的buffersize设置不同的值,发现不论是linux还是glibc中的memcpy效率都会有惊人的差异。
另外可以看来glibc的memcpy在用户空间几乎是完胜内核的memcpy的。我后面看了一下glibc的代码,发现glibc中memcpy对传入的长度做了精细的判断再调用不同的内嵌汇编代码。linux内核的memcpy相比glibc就简洁多了。
其实我们感兴趣的主要有两个:
1.内核和glibc的memcpy为什么比我们自已写的性能高这么多?
2.为什么buffersize不一样,性能差异会这么大?
OK,我们先来分析第一个问题.
请看。
常规的memcpy是:
inline int mymemcpy(char *dest,char *src,int len)
{
while(len--){
*(dest++) = *(src++);
}
return 0;
}
在不加优化选项的情况下反汇编后如下:
0x08048354 <+0>: push %ebp
0x08048355 <+1>: mov %esp,%ebp
0x08048357 <+3>: jmp 0x804836c
0x08048359 <+5>: mov 0xc(%ebp),%eax //内存操作
0x0804835c <+8>: movzbl (%eax),%edx //内存操作
0x0804835f <+11>: mov 0x8(%ebp),%eax //内存操作
0x08048362 <+14>: mov %dl,(%eax) //内存操作
0x08048364 <+16>: addl $0x1,0x8(%ebp) //内存操作
0x08048368 <+20>: addl $0x1,0xc(%ebp) //内存操作
0x0804836c <+24>: subl $0x1,0x10(%ebp) //内存操作
0x08048370 <+28>: cmpl $0xffffffff,0x10(%ebp) //内存操作
0x08048374 <+32>: jne 0x8048359
0x08048376 <+34>: mov $0x0,%eax
0x0804837b <+39>: pop %ebp
0x0804837c <+40>: ret
内核的memcpy:
static void *__memcpy(void *to, const void *from, int n)
{
int d0, d1, d2;
asm volatile("rep ; movsl/n/t"
"movl %4,%%ecx/n/t"
"andl $3,%%ecx/n/t"
"jz 1f/n/t"
"rep ; movsb/n/t"
"1:"
: "=&c" (d0), "=&D" (d1), "=&S" (d2)
: "0" (n / 4), "g" (n), "1" ((long)to), "2" ((long)from)
: "memory");
return to;
}
在不加优化选项的情况下反汇编后如下(内嵌的汇编代码加优化也没用):
0x0804837d <+0>: push %ebp
0x0804837e <+1>: mov %esp,%ebp
0x08048380 <+3>: push %edi
0x08048381 <+4>: push %esi
0x08048382 <+5>: sub $0x10,%esp
0x08048385 <+8>: mov 0x10(%ebp),%edx
0x08048388 <+11>: mov %edx,%eax
0x0804838a <+13>: sar $0x1f,%eax
0x0804838d <+16>: shr $0x1e,%eax
0x08048390 <+19>: add %edx,%eax
0x08048392 <+21>: sar $0x2,%eax
0x08048395 <+24>: mov %eax,%ecx
0x08048397 <+26>: mov 0x8(%ebp),%edi
0x0804839a <+29>: mov 0xc(%ebp),%esi
0x0804839d <+32>: rep movsl %ds:(%esi),%es:(%edi) //内存操作,但一次传4字节
0x0804839f <+34>: mov 0x10(%ebp),%ecx
0x080483a2 <+37>: and $0x3,%ecx
0x080483a5 <+40>: je 0x80483a9 <__memcpy+44>
0x080483a7 <+42>: rep movsb %ds:(%esi),%es:(%edi) //内存操作
0x080483a9 <+44>: mov %ecx,-0x14(%ebp)
0x080483ac <+47>: mov %edi,-0x10(%ebp)
0x080483af <+50>: mov %esi,-0xc(%ebp)
0x080483b2 <+53>: mov 0x8(%ebp),%eax
0x080483b5 <+56>: add $0x10,%esp
0x080483b8 <+59>: pop %esi
---Type
0x080483b9 <+60>: pop %edi
0x080483ba <+61>: pop %ebp
0x080483bb <+62>: ret
由上面的红字就可知以性能的主要差异在哪了!我们自已写的挫memcpy一次一个字节,中规中矩的慢慢拷,我们内核的memcpy一次四个充分利用总线位宽。再者辅助计数的变量全都通过操作寄存器完成。glibc就不分析了,虽然他的性能比内核在用户空间的表现更好,但是这个性能差异已经很小了。主要是哥懒,要是把这两个函数都整到内核里去再测试,鹿死谁手还真不一定。
第二个问题:
为什么buffersize不同差异会这么大?
这个问题我想原因可能是多方面的,我尽量把我的见解说出来,也希望大家一起来补充。呵呵。
1.但buffersize较小的时候,比如小于总线位宽时,这不用说了,人家本来就一次能搬四个字节,硬是要把四个字节分两次搬那肯定慢。
2.当buffersize过大时,大范围的寻址需要花费更多的时间。
关于这一点我做了一个测试:
场景大概如下:
1:
for(;i
memcpy_test(buf,bufsrc,bufsize);
}
2:
for(;i
memcpy_test(buf+(bufsize*i),bufsrc+(bufsize*i),bufsize);
}
差异其实就在buf的地址。
经过测试发现这个确实影响速度,我帖一点数据出来看看大家就明白了:
第一种:
bufsize(byte) calltimes(times) totalbufsize(MB) usetime(s)
2 536870912 1024 8.210747
4 268435456 1024 2.697762
8 134217728 1024 1.407657
16 67108864 1024 0.762430
32 33554432 1024 0.439889
64 16777216 1024 0.278593
128 8388608 1024 0.197969
256 4194304 1024 0.157624
512 2097152 1024 0.137467
1024 1048576 1024 0.107227
2048 524288 1024 0.082952
4096 262144 1024 0.070795
8192 131072 1024 0.064739
16384 65536 1024 0.061701
32768 32768 1024 0.120136
65536 16384 1024 0.181142
131072 8192 1024 0.180566
262144 4096 1024 0.209755
524288 2048 1024 0.264183
1048576 1024 1024 0.264153
2097152 512 1024 0.272311
4194304 256 1024 0.602269
8388608 128 1024 0.626959
16777216 64 1024 0.617420
33554432 32 1024 0.647506
67108864 16 1024 0.656302
134217728 8 1024 0.641960
268435456 4 1024 0.685802
536870912 2 1024 0.694582
1073741824 1 1024 0.684133 //可以看出所占用的时间差异非常大
第二种:
bufsize(byte) calltimes(times) totalbufsize(MB) usetime(s)
2 536870912 1024 11.951526
4 268435456 1024 4.222065
8 134217728 1024 2.250977
16 67108864 1024 1.298292
32 33554432 1024 0.986299
64 16777216 1024 0.912032
128 8388608 1024 0.948817
256 4194304 1024 0.804965
512 2097152 1024 0.786521
1024 1048576 1024 0.730296
2048 524288 1024 0.716415
4096 262144 1024 0.712225
8192 131072 1024 0.707980
16384 65536 1024 0.705494
32768 32768 1024 0.704431
65536 16384 1024 0.703835
131072 8192 1024 0.703518
262144 4096 1024 0.703595
524288 2048 1024 0.703723
1048576 1024 1024 0.703498
2097152 512 1024 0.703159
4194304 256 1024 0.703178
8388608 128 1024 0.703410
16777216 64 1024 0.703766
33554432 32 1024 0.703611
67108864 16 1024 0.703771
134217728 8 1024 0.703650
268435456 4 1024 0.703635
536870912 2 1024 0.703814
1073741824 1 1024 0.703455 //可以看出所占用的时间是非常平均的
从上面的数据很容易看来,当数据块很大的时候,所耗费的时间差异主要就在寻址上。
结论:
内核和glibc的Memcpy和传入的块尺寸大小有密切关系。
主要表现在两方面:
1.当块尺寸过小的时候一次可以拷完的分多次拷,加之块过小导致频率的函数调用,这个开销也不容易忽视。(这个是针对本测试用例来说的)
2.当块尺寸过大时,也给寻址带来了一定的压力,也有一定的开销。不过相对于前者来说就很小了。
最后,给出测试的源码:
#include
#include
#include
#define MB (1024*1024)
#define TOTALSIZE (MB*1024)
#define BUFSIZE (TOTALSIZE)
#define TIMES(buf_size) ((TOTALSIZE)/(buf_size))
char buf[BUFSIZE] = {0};
char bufsrc[BUFSIZE] = {0};
static void *kernel_memcpy(void *to, const void *from, size_t n)
{
int d0, d1, d2;
asm volatile("rep ; movsl/n/t"
"movl %4,%%ecx/n/t"
"andl $3,%%ecx/n/t"
"jz 1f/n/t"
"rep ; movsb/n/t"
"1:"
: "=&c" (d0), "=&D" (d1), "=&S" (d2)
: "0" (n / 4), "g" (n), "1" ((long)to), "2" ((long)from)
: "memory");
return to;
}
int memtest(int bufsize,void *(memcpy_test)(void *to, const void *from, size_t n))
{
int i = 0;
int times = 0;
float timeuse = 0.0;
struct timeval tpstart = {0};
struct timeval tpend = {0};
times = TIMES(bufsize);
gettimeofday(&tpstart,NULL);
for(;i
#ifdef TESTTWO
memcpy_test(buf,bufsrc,bufsize);
#else
memcpy_test(buf+(bufsize*i),bufsrc+(bufsize*i),bufsize);
#endif
}
gettimeofday(&tpend,NULL);
timeuse = 1000000*(tpend.tv_sec-tpstart.tv_sec)+tpend.tv_usec-tpstart.tv_usec;
timeuse = timeuse / 1000000;
printf("bufsize:%d(byte) calltimes:%d(times) totalbufsize:%d(MB) usetime:%f(s)/n",
bufsize,times,((times*bufsize)/(1024*1024)),timeuse);
return 0;
}
int main()
{
int shiftbit = 1;
memset(buf,1,sizeof(buf));
memset(bufsrc,2,sizeof(bufsrc));
printf("kernel's memcpy test start:----------------------------------------------------------/n");
for(;shiftbit<=30;shiftbit++ ){
memtest(1<
}
printf("kernel's memcpy test end:------------------------------------------------------------/n");
shiftbit = 1;
printf("glibc's memcpy test start:-----------------------------------------------------------/n");
for(;shiftbit<=30;shiftbit++ ){
memtest(1<
}
printf("glibc's memcpy test end:-------------------------------------------------------------/n");
return 0;
}