`
aigo
  • 浏览: 2648654 次
  • 性别: Icon_minigender_1
  • 来自: 宜昌
社区版块
存档分类
最新评论

比memcpy更快的内存拷贝:用赋值代替循环拷贝

阅读更多

原文是出自百度空间,百度空间早已关闭,所以原文出处无法查询了

 

相关衍生:

怎样写出一个更快的 memset/memcpy ?

https://www.zhihu.com/question/35172305 

 

以下内容转自:http://www.cnblogs.com/GoodGoodWorkDayDayUp/archive/2010/10/15/1852251.html

然间看到一个叫xmemcpy的工具,用做内存拷贝。号称在拷贝120字节以内时,比glibc提供的memcpy快10倍,并且有实验数据。

 

这让人感觉很诧异。一直以来都觉得memcpy是很高效的。相比于strcpy等函数的逐字节拷贝,memcpy是按照机器字长逐字进行拷贝的,一个字等于4(32位机)或8(64位机)个字节。CPU存取一个字节和存取一个字一样,都是在一条指令、一个内存周期内完成的。显然,按字拷贝效率更高。

 

那么,这个xmemcpy是靠什么来实现比memcpy“快10倍”的呢?

看了一下xmemcpy的实现,原来它速度快的根据是:“小内存的拷贝,使用等号直接赋值比memcpy快得多”。

这下就更纳闷了,内存拷贝不就是把一块内存一部分一部分地拷贝到另一块内存去吗?难道逐字拷贝还有性能提升的空间?

 

写了一段代码:

#include <stdio.h>

#define TESTSIZE        128

struct node {
	char buf[TESTSIZE];
};
void main()
{
	char src[TESTSIZE] = {0};
	char dst[TESTSIZE];
	*(struct node*)dst = *(struct node*)src;
}

 

 

然后反汇编:

......
00000000004004a8 <main>:
4004a8:       55                      push   %rbp
4004a9:       48 89 e5                mov    %rsp,%rbp
4004ac:       48 81 ec 00 01 00 00    sub    $0x100,%rsp
4004b3:       48 8d 7d 80             lea    0xffffffffffffff80(%rbp),%rdi
4004b7:       ba 80 00 00 00          mov    $0x80,%edx
4004bc:       be 00 00 00 00          mov    $0x0,%esi
4004c1:       e8 1a ff ff ff          callq 4003e0 <memset@plt>
4004c6:       48 8b 45 80             mov    0xffffffffffffff80(%rbp),%rax
4004ca:       48 89 85 00 ff ff ff    mov    %rax,0xffffffffffffff00(%rbp)
4004d1:       48 8b 45 88             mov    0xffffffffffffff88(%rbp),%rax
......
400564:       48 89 85 70 ff ff ff    mov    %rax,0xffffffffffffff70(%rbp)
40056b:       48 8b 45 f8             mov    0xfffffffffffffff8(%rbp),%rax
40056f:       48 89 85 78 ff ff ff    mov    %rax,0xffffffffffffff78(%rbp)
400576:       c9                      leaveq 
400577:       c3                      retq   
400578:       90                      nop    
......

 

 

再将libc反汇编,并找到memcpy的实现,以作比较:

......
0006b400 <memcpy>:
6b400:       8b 4c 24 0c             mov    0xc(%esp),%ecx
6b404:       89 f8                   mov    %edi,%eax
6b406:       8b 7c 24 04             mov    0x4(%esp),%edi
6b40a:       89 f2                   mov    %esi,%edx
6b40c:       8b 74 24 08             mov    0x8(%esp),%esi
6b410:       fc                      cld    
6b411:       d1 e9                   shr    %ecx
6b413:       73 01                   jae    6b416 <memcpy+0x16>
6b415:       a4                      movsb %ds:(%esi),%es:(%edi)
6b416:       d1 e9                   shr    %ecx
6b418:       73 02                   jae    6b41c <memcpy+0x1c>
6b41a:       66 a5                   movsw %ds:(%esi),%es:(%edi)
6b41c:       f3 a5                   repz movsl %ds:(%esi),%es:(%edi)
6b41e:       89 c7                   mov    %eax,%edi
6b420:       89 d6                   mov    %edx,%esi
6b422:       8b 44 24 04             mov    0x4(%esp),%eax
6b426:       c3                      ret    
6b427:       90                      nop    
......

 

 

原来两者都是通过逐字拷贝来实现的。但是“等号赋值”被编译器翻译成一连串的MOV指令,而memcpy则是一个循环。“等号赋值”比memcpy快,并不是快在拷贝方式上,而是快在程序流程上。

(另外,测试发现,“等号赋值”的长度必须小于等于128,并且是机器字长的倍数,才会被编译成连续MOV形式,否则会被编译成调用memcpy。当然,具体怎么做是编译器决定的。)

 

而为什么同样是按机器字长拷贝,连续的MOV指令就要比循环MOV快呢?

在循环方式下,每一次MOV过后,需要:1、判断是否拷贝完成;2、跳转以便继续拷贝。

每拷贝一个字长,CPU就需要多执行以上两个动作。

 

循环除了增加了判断和跳转指令以外,对于CPU处理流水产生的影响也是不可不计的。CPU将指令的执行分为若干个阶段,组成一条指令处理流水线,这样就能实现在一个CPU时钟周期完成一条指令,使得CPU的运算速度得以提升。

指令流水只能按照单一的指令路径来执行,如果出现分支(判断+跳转),流水就没法处理了。

为了缓解分支对于流水的影响,CPU可能会采取一定的分支预测策略。但是分支预测不一定就能成功,如果失败,其损失比不预测还大。

 

所以,循环还是比较浪费的。如果效率要求很高,很多情况下,我们需要把循环展开(比如在本例中,每次循环拷贝N个字节),以避免判断与跳转占用大量的CPU时间。这算是一种以空间换时间的做法。GCC就有自动将循环展开的编译选项(如:-funroll-loops)。

但是,循环展开也是应该有个度的,并不是越展开越好(即使不考虑对空间的浪费)。因为CPU的快速执行很依赖于cache,如果cache不命中,CPU将浪费不少的时钟周期在等待内存上(内存的速度一般比CPU低一个数量级)。而小段循环结构就比较有利于cache命中,因为重复执行的一段代码很容易被硬件放在cache中,这就是代码局部性带来的好处。而过度的循环展开就打破了代码的局部性,所以xmemcpy一开始就提到拷贝120字节以内。如果要拷贝的字节更多,则全部展开成连续的MOV指令的做法未必会很高效。

 

综上所述,“等号赋值”之所以比memcpy快,就是因为它省略了CPU对于判断与跳转的处理,消除了分支对CPU流水的影响。而这一切都是通过适度展开内存拷贝的循环来实现的。

 

 

分享到:
评论

相关推荐

    1. 拷贝构造与拷贝赋值1

    拷贝构造和拷贝赋值是面向对象编程中两种重要的对象复制方式,它们在C++、Java和Python等编程语言中都有所体现,但实现机制和处理方式有所不同。本文主要探讨的是C++中的拷贝构造和拷贝赋值,以及浅拷贝和深拷贝的...

    拷贝构造函数和赋值操作符号的重载.pdf

    在C++编程中,拷贝构造函数和赋值操作符重载是两个非常重要的概念,尤其是在处理包含动态分配内存的类时。拷贝构造函数和赋值操作符的正确实现对于确保对象之间的独立性和防止资源泄露至关重要。 拷贝构造函数是一...

    浅拷贝和深拷贝PPT学习教案.pptx

    【浅拷贝与深拷贝】是编程中的一个重要概念,主要...在实际编程中,尤其是在处理含有动态分配内存的对象时,理解并正确使用浅拷贝和深拷贝是非常重要的,可以防止内存泄漏和意外的数据修改,提高程序的稳定性和安全性。

    字符串拷贝函数 (2).pdf

    此外,如果拷贝的不是字符串而是其他数据类型,`memcpy`在大多数情况下比赋值运算符更高效,特别是在处理大块数据或结构体时。 在使用引用接收函数返回值时,需要注意引用的生存期。如果接收的是引用返回值,那么...

    C语言数组的多种赋值方式

    memcpy 是一个函数,用于将一个内存区域的内容拷贝到另一个内存区域。其函数原型为: ```c void *memcpy(void *dest, const void *src, size_t n); ``` 其中,`dest` 是目标内存区域,`src` 是源内存区域,`n` 是要...

    拷贝构造函数详解VC共12页.pdf.zip

    2. **数据复制**:复制源对象的数据成员到新对象中,这可以通过逐个复制或者使用`memcpy`等工具来完成。 3. **成员函数调用**:对于有特殊需求的成员变量(如智能指针),可能需要调用它们的拷贝构造函数或赋值操作...

    华为慧通笔试题 (2).pdf

    2. 内存拷贝的实现:可以使用memcpy函数来实现内存拷贝。需要注意内存拷贝的安全性和效率性。 五、代码实现 1. 字符串插入:可以使用指针操作来实现字符串的插入操作。 ```c void insert(char *s, char *t, int i...

    memcpy函数的实现代码 (2).pdf

    这个版本直接在循环体内进行赋值操作,并且更新计数器,避免了每次循环的条件检查,理论上可以提高性能。 在C语言中,标准库提供的memcpy函数追求的是速度,而不是安全性。因此,程序员在使用时应确保源和目标区域...

    通过指针读写内存3种方法源码

    2. **内存拷贝函数**:使用如`memcpy`这样的函数,可以一次性复制一段内存区域到另一个位置。这对于大块数据的读写非常有用。 3. **内存映射**:通过内存映射文件或内存区域,将磁盘上的数据直接映射到内存,实现...

    快速复制结构体变量和类实例

    如果确实需要深拷贝,可以使用`memcpy`函数或C++的`std::copy`,但请注意这仅适用于不包含任何非POD(Plain Old Data)类型的结构体。 总的来说,理解和正确处理浅拷贝与深拷贝是编写健壮、高效代码的关键。在涉及...

    C/C++ 浅拷贝和深拷贝的实例详解

    在C/C++编程中,了解浅拷贝和深拷贝的概念是非常重要的,特别是在处理包含指针的对象时。这两种拷贝方式决定了对象复制时内存管理的策略。...在涉及指针和动态内存管理时,深拷贝通常是更安全的选择。

    收集一些C语言内存相关的项目.zip

    学习这些内存管理知识点,并通过实践项目进行巩固,可以帮助C语言初学者更好地理解和控制程序的内存使用,避免常见的错误,提升程序的稳定性和性能。在"my_resource"文件中,可能会包含一些实例代码、练习题目和解答...

    三、C++面向对象-类和对象那些你不知道的细节原理.doc

    因此,当对象需要被拷贝时,应避免使用`memcpy`,而是使用赋值运算符(`=`)或拷贝构造函数来实现深拷贝,确保两个对象独立。例如,`CirQueue`类的赋值运算符和拷贝构造函数就实现了深拷贝。 最后,类和对象的实践...

    copyarr_copyarry_

    6. **性能考虑**:虽然`memcpy`和`memmove`是高效的,但自定义拷贝可能在某些情况下提供更好的性能,例如当知道源和目标数组不会重叠,且不需要进行元素级别的深拷贝时。 在提供的`copyarr.cpp`文件中,可能包含了...

    有趣的问题:C 的表达式 x == x,何时为假?!

    第一种方式直接赋值整数给浮点数,第二种方式使用`memcpy`将整数值复制到浮点数的内存位置。 - **输出结果解读**: - 对于直接赋值的情况,`x`被正确地视为一个浮点数,并能正常进行比较。 - 使用`memcpy`填充后,...

    一些c,c++,unix,RDBMS面试题,英文

    memcpy是内存拷贝函数,不关心字符是否是字符串。 21. strcpy的缺点:不检查目标缓冲区是否足够大,可能导致缓冲区溢出。使用strcpy_s或strncpy更安全。 22. 线程与进程:线程是进程内的执行流,共享进程的资源,...

    c python java 复制数组 深浅拷贝

    若想进行深拷贝,可以使用`memcpy()`函数,它会按字节复制内存区域。例如,`memcpy(p2, p1, sizeof(p1[0]) * N);`将`p1`的内容完全复制到`p2`。 Python中的浅拷贝可以通过直接赋值或使用`copy()`函数实现。如`b = a...

    C语言字符串操作函数.doc

    而在处理结构体或其他非字符串数据时,应使用`memcpy()`,以确保所有数据都被完整地拷贝。需要注意的是,这两个函数都不检查目标区域是否有足够的空间容纳源数据,因此在使用时要确保目标数组或结构体足够大,否则...

    Effective C++之2.构造析构赋值运算

    5. **拒绝编译器自动生成的函数**:如果不想使用编译器自动生成的构造函数、拷贝构造函数、拷贝赋值操作符或析构函数,可以将它们声明为`private`且不实现,或者在C++11及更高版本中使用`= delete`来明确禁止这些...

    简单的面试题

    - 堆:使用`malloc`、`calloc`、`realloc`、`new`等动态分配的内存,需要手动使用`free`、`delete`释放。 4. **C++的空类**: - 空类仍然会有一个默认构造函数、拷贝构造函数、析构函数、赋值运算符,即使没有...

Global site tag (gtag.js) - Google Analytics