您的位置:首页 > 其它

等号赋值与memcpy的效率问题

2016-07-12 21:14 183 查看
转自:http://www.aiuxian.com/article/p-1309055.html

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

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

先给出一个程序:

01
#include <stdio.h>
02
#define TESTSIZE128
03
struct
node {
04
char
buf[TESTSIZE];
05
};
06
int
main()
07
{
08
char
src[TESTSIZE] = {0};
09
char
dst[TESTSIZE];
10
*(
struct
node*)dst =*(
struct
node*)src;
11
}
编译:gcc -g -o test test.c

获得汇编:objdump -S test

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



*(struct node*)dst =*(struct node*)src;
4004b6: 48 8d 85 00 ff ff ff lea0xffffffffffffff00(%rbp),%rax
4004bd: 48 8d 55 80 lea0xffffffffffffff80(%rbp),%rdx
4004c1: 48 8b 0a mov(%rdx),%rcx
4004c4: 48 89 08 mov%rcx,(%rax)
4004c7: 48 8b 4a 08 mov0x8(%rdx),%rcx
4004cb: 48 89 48 08 mov%rcx,0x8(%rax)
4004cf: 48 8b 4a 10 mov0x10(%rdx),%rcx
4004d3: 48 89 48 10 mov%rcx,0x10(%rax)
4004d7: 48 8b 4a 18 mov0x18(%rdx),%rcx
4004db: 48 89 48 18 mov%rcx,0x18(%rax)
4004df: 48 8b 4a 20 mov0x20(%rdx),%rcx
4004e3: 48 89 48 20 mov%rcx,0x20(%rax)
4004e7: 48 8b 4a 28 mov0x28(%rdx),%rcx
4004eb: 48 89 48 28 mov%rcx,0x28(%rax)
4004ef: 48 8b 4a 30 mov0x30(%rdx),%rcx
4004f3: 48 89 48 30 mov%rcx,0x30(%rax)
4004f7: 48 8b 4a 38 mov0x38(%rdx),%rcx
4004fb: 48 89 48 38 mov%rcx,0x38(%rax)
4004ff: 48 8b 4a 40 mov0x40(%rdx),%rcx
400503: 48 89 48 40 mov%rcx,0x40(%rax)
400507: 48 8b 4a 48 mov0x48(%rdx),%rcx
40050b: 48 89 48 48 mov%rcx,0x48(%rax)
40050f: 48 8b 4a 50 mov0x50(%rdx),%rcx
400513: 48 89 48 50 mov%rcx,0x50(%rax)
400517: 48 8b 4a 58 mov0x58(%rdx),%rcx
40051b: 48 89 48 58 mov%rcx,0x58(%rax)
40051f: 48 8b 4a 60 mov0x60(%rdx),%rcx
400523: 48 89 48 60 mov%rcx,0x60(%rax)
400527: 48 8b 4a 68 mov0x68(%rdx),%rcx
40052b: 48 89 48 68 mov%rcx,0x68(%rax)
40052f: 48 8b 4a 70 mov0x70(%rdx),%rcx
400533: 48 89 48 70 mov%rcx,0x70(%rax)
400537: 48 8b 52 78 mov0x78(%rdx),%rdx
40053b: 48 89 50 78 mov%rdx,0x78(%rax)



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



00973a30 <memcpy>:
973a30: 8b 4c 24 0c mov0xc(%esp),%ecx
973a34: 89 f8 mov%edi,%eax
973a36: 8b 7c 24 04 mov0x4(%esp),%edi
973a3a: 89 f2 mov%esi,%edx
973a3c: 8b 74 24 08 mov0x8(%esp),%esi
973a40: fc cld
973a41: d1 e9 shr%ecx
973a43: 73 01 jae973a46 <memcpy+0x16>
973a45: a4 movsb %ds:(%esi),%es:(%edi)
973a46: d1 e9 shr%ecx
973a48: 73 02 jae973a4c <memcpy+0x1c>
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 mov0x4(%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:

001
#include <stdio.h>
002
#include <string.h>
003
#include <stdlib.h>
004
#include <sys/time.h>
005
006
#define LEN 0x20000
007
#define MYM 1
008
#define LIBM 0
009
char
*dst;
010
char
*src;
011
012
typedef
struct
memcpy_data_size
013
{
014
int
a[16];
015
}DATA_SIZE, *P_DATA_SIZE;
016
017
void
*mymemcpy(
void
*to,
const
void
*from,
size_t
size)
018
{
019
P_DATA_SIZE dst =(P_DATA_SIZE)to;
020
P_DATA_SIZE src =(P_DATA_SIZE)from;
021
022
int
new_len  = size/
sizeof
(DATA_SIZE)-1;
023
int
remain  = size%
sizeof
(DATA_SIZE)-1;
024
 
025
while
(new_len >= 1)
026
{
027
*dst++ = *src++;
028
new_len--;
029
}
030
#if 0
031
while
(new_len >= 2)
032
{
033
*dst++ = *src++;
034
*dst++ = *src++;
035
new_len = new_len -2;
036
}
037
if
(new_len == 1)
038
{
039
*dst++ = *src++;
040
}
041
#endif
042
while
(remain >= 0)
043
{
044
*((
char
*)dst + remain) = *((
char
*)src + remain);
045
remain--;
046
}
047
048
return
to;
049
}
050
051
052
int
main(
int
argc,
char
const
* argv[])
053
{
054
int
type =0;
055
struct
timeval start, end;
056
unsigned
long
diff;
057
058
gettimeofday(&start, NULL);
059
if
(argc != 2){
060
printf
(
"you should run it as : ./run 1(or 0)\n"
);
061
printf
(
"1: run my memcpy\n"
);
062
printf
(
"0: run lib memcpy\n"
);
063
exit
(0);
064
}
065
type =
atoi
(argv[1]);
066
if
(MYM != type && LIBM != type){
067
printf
(
"you should run it as : ./run 1(or 0)\n"
);
068
printf
(
"1: run my memcpy\n"
);
069
printf
(
"0: run lib memcpy\n"
);
070
exit
(0);
071
}
072
073
dst =
malloc
(
sizeof
(
char
)*LEN);
074
if
(NULL == dst) {
075
perror
(
"dst malloc"
);
076
exit
(1);
077
}
078
079
src =
malloc
(
sizeof
(
char
)*LEN);
080
if
(NULL == src) {
081
perror
(
"src malloc"
);
082
exit
(1);
083
}
084
if
(MYM == type){
085
mymemcpy(dst, src, LEN);
086
printf
(
"my memcpy:\n"
);
087
}
088
else
{
089
memcpy
(dst, src, LEN);
090
printf
(
"lib memcpy:\n"
);
091
}
092
free
(dst);
093
free
(src);
094
 
095
gettimeofday(&end, NULL);
096
diff = 1000000*(end.tv_sec - start.tv_sec)+ end.tv_usec - start.tv_usec;
097
printf
(
"run time is %ld us\n"
,diff);
098
 
099
return
0;
100
}
被注释掉的几行代码本来是用来循环展开的,可测试结果并没发现有什么好处,故,先注释掉。
在测试程序中,经过多次测试,并无法真正确定libc和自己实现的memcpy效率谁优谁劣。可能是由于运行时间太短以及进程调度所致。

目前为止,还没找到更好的测试方法,去验证效率的优劣。
现将我的测试数据粘贴至此,仅供参考:
编译程序:gcc -g -o timememcpytimememcpy.c
执行测试脚本为:run.sh

01
#!/bin/sh
02
.
/
timememcpy
1
03
.
/
timememcpy
1
04
.
/
timememcpy
1
05
.
/
timememcpy
1
06
.
/
timememcpy
1
07
.
/
timememcpy
0
08
.
/
timememcpy
0
09
.
/
timememcpy
0
10
.
/
timememcpy
0
11
.
/
timememcpy
0
运行该脚本,得结果如下:


[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



脚本内容修改为:

01
#!/bin/sh
02
.
/
timememcpy
0
03
.
/
timememcpy
0
04
.
/
timememcpy
0
05
.
/
timememcpy
0
06
.
/
timememcpy
0
07
.
/
timememcpy
1
08
.
/
timememcpy
1
09
.
/
timememcpy
1
10
.
/
timememcpy
1
11
.
/
timememcpy
1
再次运行,得结果:


[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



参考:
链接地址
注:这个程序是起了一个计数线程,与实际的memcpy线程并发执行。本人感觉运行时间这么短的程序,要是使用一个线程去计数的话,由于进程调度机制并无法保证计数线程和该程序运行时间相同,误差会更大,所以,在本文的程序中摒弃理论这种计数方式。

链接地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: