轉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