等號賦值與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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容