等号赋值与memcpy的效率问题(转)

转http://blog.csdn.net/pngynghay/article/details/17142401

偶尔看到一个说法,说,小内存的拷贝,使用等号直接赋值比memcpy快得多。结合自己搜集到的资料,整理成此文。

事实:strcpy等函数的逐字节拷贝,memcpy是按照机器字长逐字进行拷贝的,一个字等于4(32位机)或8(64位机)个字节。CPU存取一个字节和存取一个字一样,都是在一条指令、一个内存周期内完成的。显然,按字拷贝效率更高。

先给出一个程序:

[cpp]view plaincopy

#include 

#define TESTSIZE        128

structnode {

charbuf[TESTSIZE];

};

intmain()

{

charsrc[TESTSIZE] = {0};

chardst[TESTSIZE];

*(structnode*)dst = *(structnode*)src;

}

编译:gcc -g -o test test.c

获得汇编:objdump -S test

可以看到有这么一些汇编,对应的是等号赋值操作:

*(struct node*)dst = *(struct node*)src;

4004b6: 48 8d 85 00 ff ff ff  lea    0xffffffffffffff00(%rbp),%rax

4004bd: 48 8d 55 80           lea    0xffffffffffffff80(%rbp),%rdx

4004c1: 48 8b 0a              mov    (%rdx),%rcx

4004c4: 48 89 08              mov    %rcx,(%rax)

4004c7: 48 8b 4a 08           mov    0x8(%rdx),%rcx

4004cb: 48 89 48 08           mov    %rcx,0x8(%rax)

4004cf: 48 8b 4a 10           mov    0x10(%rdx),%rcx

4004d3: 48 89 48 10           mov    %rcx,0x10(%rax)

4004d7: 48 8b 4a 18           mov    0x18(%rdx),%rcx

4004db: 48 89 48 18           mov    %rcx,0x18(%rax)

4004df: 48 8b 4a 20           mov    0x20(%rdx),%rcx

4004e3: 48 89 48 20           mov    %rcx,0x20(%rax)

4004e7: 48 8b 4a 28           mov    0x28(%rdx),%rcx

4004eb: 48 89 48 28           mov    %rcx,0x28(%rax)

4004ef: 48 8b 4a 30           mov    0x30(%rdx),%rcx

4004f3: 48 89 48 30           mov    %rcx,0x30(%rax)

4004f7: 48 8b 4a 38           mov    0x38(%rdx),%rcx

4004fb: 48 89 48 38           mov    %rcx,0x38(%rax)

4004ff: 48 8b 4a 40           mov    0x40(%rdx),%rcx

400503: 48 89 48 40           mov    %rcx,0x40(%rax)

400507: 48 8b 4a 48           mov    0x48(%rdx),%rcx

40050b: 48 89 48 48           mov    %rcx,0x48(%rax)

40050f: 48 8b 4a 50           mov    0x50(%rdx),%rcx

400513: 48 89 48 50           mov    %rcx,0x50(%rax)

400517: 48 8b 4a 58           mov    0x58(%rdx),%rcx

40051b: 48 89 48 58           mov    %rcx,0x58(%rax)

40051f: 48 8b 4a 60           mov    0x60(%rdx),%rcx

400523: 48 89 48 60           mov    %rcx,0x60(%rax)

400527: 48 8b 4a 68           mov    0x68(%rdx),%rcx

40052b: 48 89 48 68           mov    %rcx,0x68(%rax)

40052f: 48 8b 4a 70           mov    0x70(%rdx),%rcx

400533: 48 89 48 70           mov    %rcx,0x70(%rax)

400537: 48 8b 52 78           mov    0x78(%rdx),%rdx

40053b: 48 89 50 78           mov    %rdx,0x78(%rax)

获得libc的memcpy汇编代码:objdump -S /lib/libc.so.6

00973a30 :

973a30:       8b 4c 24 0c             mov    0xc(%esp),%ecx

973a34:       89 f8                   mov    %edi,%eax

973a36:       8b 7c 24 04             mov    0x4(%esp),%edi

973a3a:       89 f2                   mov    %esi,%edx

973a3c:       8b 74 24 08             mov    0x8(%esp),%esi

973a40:       fc                      cld

973a41:       d1 e9                   shr    %ecx

973a43:       73 01                   jae    973a46

973a45:       a4                      movsb  %ds:(%esi),%es:(%edi)

973a46:       d1 e9                   shr    %ecx

973a48:       73 02                   jae    973a4c

973a4a:       66 a5                   movsw  %ds:(%esi),%es:(%edi)

973a4c:       f3 a5                   rep movsl %ds:(%esi),%es:(%edi)

973a4e:       89 c7                   mov    %eax,%edi

973a50:       89 d6                   mov    %edx,%esi

973a52:       8b 44 24 04             mov    0x4(%esp),%eax

973a56:       c3                      ret

973a57:       90                      nop

原来两者都是通过逐字拷贝来实现的。但是“等号赋值”被编译器翻译成一连串的MOV指令,而memcpy则是一个循环。“等号赋值”比memcpy快,并不是快在拷贝方式上,而是快在程序流程上。

测试发现,“等号赋值”的长度必须小于等于128,并且是机器字长的倍数,才会被编译成连续MOV形式,否则会被编译成调用memcpy。而同样的,如果memcpy复制的长度小于等于128且是机器字长的整数倍,会被编译成MOV形式。所以,无论你的代码中如何写,编译器都会做好优化工作。

而为什么同样是按机器字长拷贝,连续的MOV指令就要比循环MOV快呢?

在循环方式下,每一次MOV过后,需要:1、判断是否拷贝完成;2、跳转以便继续拷贝。

循环还是比较浪费的。如果效率要求很高,很多情况下,我们需要把循环展开(比如在本例中,每次循环拷贝N个字节),以避免判断与跳转占用大量的CPU时间。这算是一种以空间换时间的做法。GCC就有自动将循环展开的编译选项(如:-funroll-loops)。

循环展开也是应该有个度的,并不是越展开越好(即使不考虑对空间的浪费)。因为CPU的快速执行很依赖于cache,如果cache不命中,CPU将浪费不少的时钟周期在等待内存上(内存的速度一般比CPU低一个数量级)。而小段循环结构就比较有利于cache命中,因为重复执行的一段代码很容易被硬件放在cache中,这就是代码局部性带来的好处。而过度的循环展开就打破了代码的局部性。如果要拷贝的字节更多,则全部展开成连续的MOV指令的做法未必会很高效。

综上所述,“等号赋值”之所以比memcpy快,就是因为它省略了CPU对于判断与跳转的处理,消除了分支对CPU流水的影响。而这一切都是通过适度展开内存拷贝的循环来实现的。

如果将libc的memcpy换成时等号循环赋值,效率会如何,程序如下timememcpy.c:

[cpp]view plaincopy

#include 

#include 

#include 

#include 

#define LEN 0x20000

#define MYM 1

#define LIBM 0

char*dst;

char*src;

typedefstructmemcpy_data_size

{

inta[16];

}DATA_SIZE, *P_DATA_SIZE;

void*mymemcpy(void*to,constvoid*from,size_tsize)

{

P_DATA_SIZE dst = (P_DATA_SIZE)to;

P_DATA_SIZE src = (P_DATA_SIZE)from;

intnew_len  = size/sizeof(DATA_SIZE)-1;

intremain  = size%sizeof(DATA_SIZE)-1;

while(new_len >= 1)

{

*dst++ = *src++;

new_len--;

}

#if 0

while(new_len >= 2)

{

*dst++ = *src++;

*dst++ = *src++;

new_len = new_len -2;

}

if(new_len == 1)

{

*dst++ = *src++;

}

#endif

while(remain >= 0)

{

*((char*)dst + remain) = *((char*)src + remain);

remain--;

}

returnto;

}

intmain(intargc,charconst* argv[])

{

inttype = 0;

structtimeval start, end;

unsignedlongdiff;

gettimeofday(&start, NULL);

if(argc != 2){

printf("you should run it as : ./run 1(or 0)\n");

printf("1: run my memcpy\n");

printf("0: run lib memcpy\n");

exit(0);

}

type = atoi(argv[1]);

if(MYM != type && LIBM != type){

printf("you should run it as : ./run 1(or 0)\n");

printf("1: run my memcpy\n");

printf("0: run lib memcpy\n");

exit(0);

}

dst = malloc(sizeof(char)*LEN);

if(NULL == dst) {

perror("dst malloc");

exit(1);

}

src = malloc(sizeof(char)*LEN);

if(NULL == src) {

perror("src malloc");

exit(1);

}

if(MYM == type){

mymemcpy(dst, src, LEN);

printf("my memcpy:\n");

}

else{

memcpy(dst, src, LEN);

printf("lib memcpy:\n");

}

free(dst);

free(src);

gettimeofday(&end, NULL);

diff = 1000000*(end.tv_sec - start.tv_sec)+ end.tv_usec - start.tv_usec;

printf("run time is %ld us\n",diff);

return0;

}

被注释掉的几行代码本来是用来循环展开的,可测试结果并没发现有什么好处,故,先注释掉。

在测试程序中,经过多次测试,并无法真正确定libc和自己实现的memcpy效率谁优谁劣。可能是由于运行时间太短以及进程调度所致。

目前为止,还没找到更好的测试方法,去验证效率的优劣。

现将我的测试数据粘贴至此,仅供参考:

编译程序:gcc -g -o timememcpy timememcpy.c

执行测试脚本为:run.sh

[python]view plaincopy

#!/bin/sh

./timememcpy1

./timememcpy1

./timememcpy1

./timememcpy1

./timememcpy1

./timememcpy0

./timememcpy0

./timememcpy0

./timememcpy0

./timememcpy0

运行该脚本,得结果如下:

[root@SPA c]# ./run.sh

my memcpy:

run time is 435 us

my memcpy:

run time is 237 us

my memcpy:

run time is 249 us

my memcpy:

run time is 304 us

my memcpy:

run time is 300 us

lib memcpy:

run time is 262 us

lib memcpy:

run time is 222 us

lib memcpy:

run time is 335 us

lib memcpy:

run time is 281 us

lib memcpy:

run time is 247 us

脚本内容修改为:

[python]view plaincopy

#!/bin/sh

./timememcpy0

./timememcpy0

./timememcpy0

./timememcpy0

./timememcpy0

./timememcpy1

./timememcpy1

./timememcpy1

./timememcpy1

./timememcpy1

再次运行,得结果:

[root@SPA c]# ./run.sh

lib memcpy:

run time is 479 us

lib memcpy:

run time is 461 us

lib memcpy:

run time is 512 us

lib memcpy:

run time is 405 us

lib memcpy:

run time is 365 us

my memcpy:

run time is 399 us

my memcpy:

run time is 314 us

my memcpy:

run time is 309 us

my memcpy:

run time is 510 us

my memcpy:

run time is 324 us

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容