比memcpy更快的内存拷贝

简介:

偶然间看到一个叫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 <>
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流水的影响。而这一切都是通过适度展开内存拷贝的循环来实现的。


目录
相关文章
|
自然语言处理 安全 虚拟化
如何查看Windows所有软件的快捷键?
【2月更文挑战第12天】本文介绍在Windows电脑中,基于OpenArk工具,查看电脑操作系统与所有软件的快捷键,并对快捷键冲突加以处理的方法~
2861 9
如何查看Windows所有软件的快捷键?
|
7月前
|
人工智能 机器人
阿里大动作!CEO 亲自挂帅成立 Token Hub 事业群,押注 AGI 时代新赛道
阿里成立CEO吴泳铭亲掌的Alibaba Token Hub(ATH)事业群,以“Token”为战略核心,整合通义实验室、MaaS、千问、悟空及AI创新五大板块,构建“创造—输送—应用”全链路AI生态。首次亮相的悟空事业部聚焦B端AI原生工作平台,协同C端千问,打造双轮驱动商业化闭环,剑指AGI时代核心生产要素之争。(239字)
4763 6
|
数据挖掘 Python
Pandas时间序列处理:日期与时间
本文介绍Pandas在处理时间序列数据时的基础概念、常见问题及解决方案。涵盖时间戳、时间间隔和周期等概念,详细讲解日期格式转换、缺失值处理、时间间隔计算和重采样等操作,并通过代码示例说明如何解决`ParserError`和`OutOfBoundsDatetime`等常见报错。掌握这些知识有助于高效处理时间序列数据,提高数据分析的质量和效率。
992 75
|
网络协议 开发者
探讨UDP协议中connect函数的作用及影响
总结来看,虽然UDP是无连接的,`connect()` 函数的使用在UDP编程中是一种可选的技术,它可以带来编程上的便利和某些性能上的改进,同时它改变的是程序逻辑上的行为,而非UDP协议本身的无连接特性。在实际应用中,根据通信模式和需求的不同,开发者可以根据情况选择是否调用 `connect()` 函数。
554 8
|
机器学习/深度学习 编解码 计算机视觉
RT-DETR改进策略【Backbone/主干网络】| 替换骨干网络为:Swin Transformer,提高多尺度特征提取能力
RT-DETR改进策略【Backbone/主干网络】| 替换骨干网络为:Swin Transformer,提高多尺度特征提取能力
937 12
RT-DETR改进策略【Backbone/主干网络】| 替换骨干网络为:Swin Transformer,提高多尺度特征提取能力
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
2209 7
|
编解码 开发者 Python
【Python】已解决:UnicodeEncodeError: ‘gbk’ codec can’t encode character ‘\u0157’ in position 1: illegal m
【Python】已解决:UnicodeEncodeError: ‘gbk’ codec can’t encode character ‘\u0157’ in position 1: illegal m
2166 1
|
API 开发者 索引
Python中的省略号(Ellipsis)赋值方式
在Python中,省略号(`...`)是一种特殊对象,称为Ellipsis,虽不常用但在特定场景下非常实用,如函数占位、未实现方法示例及NumPy数组处理。本文通过示例介绍`a = ...`的用法。省略号类似于`None`,可用作代码结构的占位符,保持代码完整性和可读性,同时在API设计中标识待实现的方法。特别是在NumPy中,省略号用于表示多维数组的剩余维度,简化数组操作,提高代码灵活性和可读性。掌握这一技巧有助于提升Python编程能力。
777 1
|
监控 算法 Linux
Linux下工具tc详细讲解及限制IP和端口实例
TC (Traffic Control) 是Linux内核中提供的一个用于控制和管理网络流量的强大工具,它允许用户实现QoS(Quality of Service)策略,包括带宽限制、优先级控制、延迟保证等。TC基于内核的队列 discipline (qdisc) 和流量类别(class) 体系结构,允许对进入或离开网络接口的数据流进行复杂的整形和过滤。
2347 1
|
数据处理 Python
如何优化Python读取大文件的内存占用与性能
如何优化Python读取大文件的内存占用与性能
1405 0