等号赋值与memcpy的效率问题

时间:2022-08-27 22:55:34

转自:http://www.aiuxian.com/article/p-1309055.html

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

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

先给出一个程序:

01 #include <stdio.h>
02 #define TESTSIZE        128
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  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 <memcpy>:
  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 <memcpy+0x16>
  973a45:       a4                      movsb  %ds:(%esi),%es:(%edi)
  973a46:       d1 e9                   shr    %ecx
  973a48:       73 02                   jae    973a4c <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             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:
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 timememcpy timememcpy.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线程并发执行。本人感觉运行时间这么短的程序,要是使用一个线程去计数的话,由于进程调度机制并无法保证计数线程和该程序运行时间相同,误差会更大,所以,在本文的程序中摒弃理论这种计数方式。

等号赋值与memcpy的效率问题的更多相关文章

  1. &period;net 表达式返回值和等号赋值的区别

    .net 7.0的新特性中,有一个使用表达式体返回值的操作.请看如下代码: private string _userName=""; public string UserName{ ...

  2. &lbrack;Python&rsqb; 等号赋值&comma; copy&comma; deepcopy的区别

    参考链接: 1. 介绍python中的可变类型与不可变类型:https://blog.csdn.net/answer3lin/article/details/86430074 (也可以参考转载博客 P ...

  3. Java中自增(&plus;&plus;)和赋值(&equals;)运算效率比较

    前言   将一个int型数组x[]从初值0变成1.有两种做法: // 只考虑后自增 int length = x.length; for (int i = 0; i < length; i++) ...

  4. js对象等号赋值的bug

    var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x); console.log(b.x); 有道题是这样的,觉得很奇葩,分析一下 ...

  5. SQL 两表关联查询 where 条件中等号两端字段顺序对效率的影响

    现有两表A(大).B(小)作关联查询,SQL语句如下: SQL1:select * from A,B where A.id = B.id SQL2:select * from A,B where B. ...

  6. memmove和memcpy

    1.memmove 函数原型:void *memmove(void *dest, const void *source, size_t count) 返回值说明:返回指向dest的void *指针 参 ...

  7. memmove和memcpy 以及strcmp strcpy几个库函数的实现

    memmove和memcpy 1.memmove 函数原型:void *memmove(void *dest, const void *source, size_t count) 返回值说明:返回指向 ...

  8. C的memset&comma;memcpy&comma;strcpy 的区别 及memset memcpy memmove源码

    extern void *memcpy(void *dest,void *src,unsigned int count);#include <string.h>   功能:由src所指内存 ...

  9. C&plus;&plus;,对象的 &equals;赋值 以及 复制构造函数赋值

    1. C++默认实现了 = 号赋值:operator=只要将一个对象的内容的内容逐位复制给另外一个对象即可. 2. C++默认实现了复制构造函数:同样,只要将一个对象的内容的内容逐位复制给另外一个对象 ...

随机推荐

  1. HTML5新标签 w3c

    w3c标准下的HTML5新标签 ,做个归纳总结: H5标签 定义和用法 兼容性 <artical> 规定独立的自包含内容, 支持html中的全局属性, 支持html中的事件属性 IE: 支 ...

  2. 老外还是喜欢Ubuntu的

    有图为证? 开效果应该是Ubuntu的界面了,当然,不知是不是backtrack. 这里面的Hacker用的电脑都不是水果.是没有给钱么.

  3. Emmet使用手册

    语法:   1.后代:> 缩写:nav>ul>li   < nav>     < ul>         < li></ li >   ...

  4. SpringDataMongoDB介绍(一)-入门

    SpringDataMongoDB介绍(一)-入门 本文介绍如何应用SpringDataMongoDB操作实体和数据库,本文只介绍最基本的例子,复杂的例子在后面的文章中介绍. SpringDataMo ...

  5. 最长不下降子序列nlogn算法详解

    今天花了很长时间终于弄懂了这个算法……毕竟找一个好的讲解真的太难了,所以励志我要自己写一个好的讲解QAQ 这篇文章是在懂了这个问题n^2解决方案的基础上学习. 解决的问题:给定一个序列,求最长不下降子 ...

  6. C&num;中的&commat;符号

      C# 中的 @ 符号 C# 中的 @ 符号其实有很多的用法,我们来看看 @ 有什么神奇之处. 1. 限定字符串 用 @ 符号加在字符串前面表示其中的转义字符 “ 不 ” 被处理. 如果我们写一个文 ...

  7. mysql 中文乱码的解决办法

    I would not suggest Richies answer, because you are screwing up the data inside the database. You wo ...

  8. Struts1 中&dollar; 没有解析的问题

    如果发现你的代码中,${name} 没有解析,就这样显示在页面上,排除错误的情况下 可能是你的jsp缺少一种属性isELIgnored="false" 加上就能够显示了 <% ...

  9. Recursive - leetcode &lbrack;递归&rsqb;

    经验tips: Recursion is the best friend of tree-related problems. 一是只要遇到字符串的子序列或配准问题首先考虑动态规划DP,二是只要遇到需要 ...

  10. KinectFusion解析

      三维重建是指获取真实物体的三维外观形貌,并建立可复用模型的一种技术.它是当下计算机视觉的一个研究热点,主要有三方面的用途:1)相比于二维图像,可以获取更全面的几何信息:2)在VR/AR中,建立真实 ...