等号赋值与memcpy的效率问题
2016-07-12 21:14
183 查看
转自:http://www.aiuxian.com/article/p-1309055.html
偶尔看到一个说法,说,小内存的拷贝,使用等号直接赋值比memcpy快得多。结合自己搜集到的资料,整理成此文。
事实:strcpy等函数的逐字节拷贝,memcpy是按照机器字长逐字进行拷贝的,一个字等于4(32位机)或8(64位机)个字节。CPU存取一个字节和存取一个字一样,都是在一条指令、一个内存周期内完成的。显然,按字拷贝效率更高。
先给出一个程序:
编译: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:
被注释掉的几行代码本来是用来循环展开的,可测试结果并没发现有什么好处,故,先注释掉。
在测试程序中,经过多次测试,并无法真正确定libc和自己实现的memcpy效率谁优谁劣。可能是由于运行时间太短以及进程调度所致。
目前为止,还没找到更好的测试方法,去验证效率的优劣。
现将我的测试数据粘贴至此,仅供参考:
编译程序:gcc -g -o timememcpytimememcpy.c
执行测试脚本为:run.sh
运行该脚本,得结果如下:
[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
脚本内容修改为:
再次运行,得结果:
[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线程并发执行。本人感觉运行时间这么短的程序,要是使用一个线程去计数的话,由于进程调度机制并无法保证计数线程和该程序运行时间相同,误差会更大,所以,在本文的程序中摒弃理论这种计数方式。
链接地址
偶尔看到一个说法,说,小内存的拷贝,使用等号直接赋值比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 | } |
获得汇编: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线程并发执行。本人感觉运行时间这么短的程序,要是使用一个线程去计数的话,由于进程调度机制并无法保证计数线程和该程序运行时间相同,误差会更大,所以,在本文的程序中摒弃理论这种计数方式。
链接地址
相关文章推荐
- 使用Clean() 去掉由函数自动生成的字符串中的双引号
- npm 私服(上)
- 写了一个合并的,大家可以参考下
- Codeforces Round #361 (Div. 2) C. Mike and Chocolate Thieves 二分
- python编写IP地址与十进制IP转换脚本
- 【Leetcode】371. Sum of Two Integers
- Android简易实战教程--第七话《在内存中存储用户名和密码》
- Android简易实战教程--第七话《在内存中存储用户名和密码》
- 如何通过linux ssh远程linux不用输入密码登入
- java动态代理框架
- 使用System.Drawing.Imaging.dll进行图片的合并
- <Spark>制作Spark-On-Yarn镜像
- HTTP的会话有四个过程,请选出不是的一个()----百度2016研发工程师笔试题(六)
- 台湾清华大学彭明辉教授的研究生手册(如何阅读论文)
- (Eclipse) 安装Subversion1.82(SVN)插件
- windows的DOS窗口如何修改大小
- 创建一个会滚动的textView
- 【Frame Animation 逐帧动画】
- 使用Junit进行单元测试代码
- CreateProcessWithToken 1058 error