最近同事给我看了一个内存拷贝测试程序,问我使用glibc库里的memcpy()函数为什么执行起来比较快,我被难住了,不知道该怎么回答,利用业余时间研究了一下代码,分析了一下memcpy为什么快,首先,我们先来看一下测试程序是怎么写的,考虑的篇幅的问题,只列出程序中关键的地方。
#define unsigned long long ULL /*按几个字节拷贝*/ forlooptest(UUL asize, ULL src, ULL dest) { UUL t; for(t = 0; t < asize; t++) b[t] = a[t]; } /*调用memcpy来拷贝*/ memcpy(dest, src, asize * sizeof(ULL)) /*按单字节拷贝*/ char *psrc, *pdest; psrc = (char *) src; pdest = (char *) dest; for(t=0; t < (asize * sizeof(long long)); t++) pdest[t] = psrc[t];
测试结果:
for-loop test: 1290.472 MiB/s memcpy function: 2844.896 MiB/s byte copy: 195.009 MiB/s
单字节拷贝这种方法比较慢的原因主要是由于增加了效率低下怎加了访存的次数,原因比较容易得出,我主要分析下memcpy和forlooptest这
两种方法,首先我们先看一下memcpy在glibc中的实现:
void * memcpy (dstpp, srcpp, len) void *dstpp; const void *srcpp; size_t len; { unsigned long int dstp = (long int) dstpp; unsigned long int srcp = (long int) srcpp; if(len >= OP_T_THRES) { /*拷贝前半部分不足一页的内存*/ len -= (-dstp) & OPSIZE; /*OPSIZE = 8*/ BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZE); /*尽可能多的拷贝页*/ PAGE_COPY_FWD_MAYBE(dstp, srcp, len, len); /*按字来拷贝剩下的部分*/ WORD_COPY_FWD (dstp, srcp, len, len); } BYTE_COPY_FWD (dstp, srcp, len); return dstpp; }
这个函数很容易理解,先判断本次次拷贝的长度有多长,OP_T_THRES 这个变量的值为16, 如果需要拷贝的长度大于16个字节的话,就尽可能多的
按页来拷贝,如果小于就按字节拷贝,由此也可以看出对域小块的内存拷贝可以不使用memcpy来实现.我们分别看一下BYTE_COPY_FWD(),PAGE_COPY_FWD_MAYBE(), WORD_COPY_FWD(),的实现:
#define unsigned char byte; #define BYTE_COPY_FWD(dst_bp, src_bp, nbytes) \ do \ { \ size_t __nbytes = (nbytes); \ while(__nbytes > 0) \ { \ byte __x = ((byte *) src_bp)[0]; \ rc_bp += 1; \ __nbytes -=1; \ ((byte *) dst_bp)[0] = __x; \ dst_bp += 1; \ } \ }while(0) \
下面的代码的大概意思是按照页大小来进行内存拷贝,不同的CPU页大小可能会不相同,比如龙芯就的页大小就是16k,而常见的x86 CPU的页大
小则是4k,在下面的代码可以看出以多大为页对齐则是根据不同系统决定的.
#define PAGE_OFFSET(n) ((n) & (PAGE_SIZE - 1)) #define PAGE_COPY_FWD_MAYBE(dstp, srcp, nbytes_left, nbytes) \ do \ { \ if(nbytes) >= PAGE_COPY_FWD_THRESHOLD && \ PAGE_OFFSET ((dstp) - (srctp) == 0) \ { \ size_t nbytes_before = PAGE_OFFSET (-(dstp)); \ if (nbytes_before != 0) \ { \ WORD_COPY_FWD(dstp, srcp, nbytes_left, nbytes_before); \ assert(nbytes_left == 0) \ nbytes -= nbytes_before; \ } \ PAGE_COPY_FWD(dstp, nbytes_left, nbytes); \ } \ }while(0) \
下面的代码大概的意思是按照机器字大小来进行内存拷贝,现在计算机基本上是32位或者64位的,所以就是按照8字节或者16字节来进行拷贝,总共拷贝8次每次根据机器字的不同,拷贝一个字或者两个字.
#define WORD_COPY_FWD(dst_bp, src_bp, nbytes_left, nbytes) \ do { size_t __nwords = (nbytes) / sizeof (op_t); \ size_t __nblocks = __nwords / 8 + 1; \ dst_bp -= (8 - __nwords % 8) * sizeof (op_t); \ src_bp -= (8 - __nwords % 8) * sizeof (op_t); \ switch (__nwords % 8) \ do \ { \ ((op_t *) dst_bp)[0] = ((op_t *) src_bp)[0]; \ case 7: \ ((op_t *) dst_bp)[1] = ((op_t *) src_bp)[1]; \ case 6: \ ((op_t *) dst_bp)[2] = ((op_t *) src_bp)[2]; \ case 5: \ ((op_t *) dst_bp)[3] = ((op_t *) src_bp)[3]; \ case 4: \ ((op_t *) dst_bp)[4] = ((op_t *) src_bp)[4]; \ case 3: \ ((op_t *) dst_bp)[5] = ((op_t *) src_bp)[5]; \ case 2: \ ((op_t *) dst_bp)[6] = ((op_t *) src_bp)[6]; \ case 1: \ ((op_t *) dst_bp)[7] = ((op_t *) src_bp)[7]; \ case 0: \ src_bp += 32; \ dst_bp += 32; \ __nblocks--; \ } \ while (__nblocks != 0); \ (nbytes_left) = (nbytes) % sizeof (op_t); \ } while (0)
综上所述,所有的拷贝实现归根揭底都是按单个字节来拷贝的,根据所要拷贝的内存大小进行不同的处理:
1.首先判断所需拷贝的内存大小有没有超过16K,如果没超过则直接按照单个字节进行拷贝,如果超过则按照2来执行。
2.尽可能的按照一页的大小来拷贝,之后按照进行3来进行。
3.按页拷贝完成之后,如果还有剩余的内存需要拷贝则根据机器字长来进行拷贝,如果没有memcpy就执行完成了。
为什么这么写?我觉得这么写的原因如下:
1.能对按字节拷贝进行汇编级的优化,主要是面向体系结构的。
2.根据不同的细粒度对拷贝进行拷贝,可以减少不必要的开销,如果太细,比如按字节,多次循环带来了额外的开销,太粗,又拷贝了太多的无用的内存,同样会造成额外的开销。