现在的位置: 首页 > 综合 > 正文

闲着没事测了一下memcpy

2013年08月03日 ⁄ 综合 ⁄ 共 5797字 ⁄ 字号 评论关闭

前阵的时候领导提出要对memcpy做一个测试。也没有交代具体的细节。我就直接自已写了个常规memcpy,然后再把内核的__memcpyglibcmemcpy整出来,对三者进行比较,不比不知道,一比吓一大跳。

测试结论是:

kernel和glibc的memcpy性能良好,glibc比内核的略高。自已写的就惨不忍睹了。

测试程序大概是先定义两块1G的全局内存。然后先用memset将其全置为1或者其它数。原理道上的兄弟都知道,linux内核很聪明,只要在你实际要用的到的时候才会产生缺页中断为你分配内存,否则只会在虚拟内存映射表中占个位置。

接下来就是循环拷贝了。

通过对每次拷贝的buffersize设置不同的值,发现不论是linux还是glibc中的memcpy效率都会有惊人的差异。

另外可以看来glibcmemcpy在用户空间几乎是完胜内核的memcpy的。我后面看了一下glibc的代码,发现glibcmemcpy对传入的长度做了精细的判断再调用不同的内嵌汇编代码。linux内核的memcpy相比glibc就简洁多了。

 

其实我们感兴趣的主要有两个:

1.内核和glibcmemcpy为什么比我们自已写的性能高这么多?

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 <mymemcpy+24>

   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 <mymemcpy+5> //立即数操作

   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 <return> to continue, or q <return> to quit---

   0x080483b9 <+60>:    pop    %edi

   0x080483ba <+61>:    pop    %ebp

   0x080483bb <+62>:    ret    

由上面的红字就可知以性能的主要差异在哪了!我们自已写的挫memcpy一次一个字节,中规中矩的慢慢拷,我们内核的memcpy一次四个充分利用总线位宽。再者辅助计数的变量全都通过操作寄存器完成。glibc就不分析了,虽然他的性能比内核在用户空间的表现更好,但是这个性能差异已经很小了。主要是哥懒,要是把这两个函数都整到内核里去再测试,鹿死谁手还真不一定。

 

第二个问题:

为什么buffersize不同差异会这么大?

这个问题我想原因可能是多方面的,我尽量把我的见解说出来,也希望大家一起来补充。呵呵。

1.但buffersize较小的时候,比如小于总线位宽时,这不用说了,人家本来就一次能搬四个字节,硬是要把四个字节分两次搬那肯定慢。

2.当buffersize过大时,大范围的寻址需要花费更多的时间。

关于这一点我做了一个测试:

场景大概如下:

1:

for(;i<times;++i){

                memcpy_test(buf,bufsrc,bufsize);

}

2:

for(;i<times;++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 <string.h>

#include <stdio.h>

#include <sys/time.h>

#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};

抱歉!评论已关闭.