背景

memcpy 是 glibc(GNU C Library)中最基础、最常用的函数之一,也是最值得持续深挖的性能热点。在搜推、大数据等核心业务中,memcpy 经常位列性能热点前五,哪怕提升几个百分点都能产生可观收益。

ARM64 通用 memcpy 面向 ARM架构 基线设计,保证兼容性但未针对微架构极致调优。不同 ARM64 处理器的分支预测精度、访存对齐敏感性、向量扩展能力差异显著,通用实现难以在所有平台上最优。鲲鹏950支持 SVE(Scalable Vector Extension) 扩展,这为小字节无分支复制提供了硬件基础。

本文针对华为950处理器,从小字节、中等长度和大块长度三个区间对 memcpy 的关键数据路径展开深度优化,在 bench-memcpy-random 基准上取得 33% 的性能提升。该特性已合入 glibc 上游社区(https://sourceware.org/glibc/),并回合至 openEuler 社区,随 24.03 LTS 版本正式发布。

优化总览

01.png

VL 是什么:SVE的设计理念是向量长度无关(Vector Length Agnostic,VLA),同一条指令在不同硬件上可以操作不同宽度的向量寄存器,VL 即为硬件实际实现的向量长度。鲲鹏950上 VL 通常为 256bit(32B),因此 2×VL=64B;其他 SVE 硬件的 VL 可能不同,但代码无需修改即可适配。

优化一:小字节复制——以SVE谓词消除逐级分支

问题:通用实现的小字节路径控制流过于分散

通用 memcpy 在小字节场景下采用逐级分支判断,这也是它在短路径上最明显的额外开销来源:

02.png

这类实现存在多级条件分支,形成逐级判断链。每次分支跳转都面临两个问题:

1. 分支预测开销:即使预测正确,跳转本身也有额外延迟;预测失败则触发流水线清空,代价更为严重

2. 代码路径分散:不同大小走不同路径,icache(指令缓存)利用率低,指令缓存碎片化

方案:SVE whilelo 谓词将 2×VL 以内的拷贝收敛到一条路径

03.png

核心机制:whilelo p0.b, xzr, count 指令将复制长度 count 与向量元素索引逐个比较,自动生成谓词寄存器 p0。只有 count 范围内的元素对应的谓词位被置1,ld1b/st1b 仅操作这些活跃元素。该路径覆盖 2×VL 以内的拷贝请求,在鲲鹏950常见的 VL=32B 配置下即 64B。

效果对比:

04.png

为什么这很重要:小字节 memcpy 在实际工作负载中占比极高(字符串操作、结构体赋值、协议头解析等),分支预测失败的惩罚在小字节场景下尤为显著——复制本身耗时很短,一次预测失败带来的流水线清空代价可能远超复制本身。

优化二:中等字节复制——消除96B冗余分支,统一处理中路径

问题:96字节分支让中等长度区间继续分流

05.png

这个分支的问题在于:

1. 分支预测压力:在 >2×VL 且 ≤128B 这段区间内,通用实现还会继续按 96B 再分一次流,进一步增加分支预测压力

2. 存储被延迟:stp A_q, B_q 等存储指令位于分支之后,而 A_q、B_q 在上一阶段就已加载就绪,分支未解析前这些存储可能无法及时发射

方案:用统一的数据搬运路径替代额外分支

06.png

关键变化:

● 移除96字节分支:无论实际长度落在 >2×VL 且 ≤128B 的哪个位置,都执行相同的主体指令序列。对于较小的那部分长度区间,尾部 stp G_q, H_q 会与 stp C_q, D_q 有部分重叠,但最终目标区域中的结果仍然正确;从实现取舍上看,这种重复写回的代价通常小于再引入一次额外条件分支

● 指令重排:将加载和存储分别集中排列,简化控制流,提升指令调度效率

优化三:大字节复制——对齐策略升级 + Pre-indexed寻址融合

大字节复制(>128B)是吞吐最敏感的核心路径,这里也是通用实现和鲲鹏950优化版差异最明显的部分。

3.1 目标地址32B对齐:把优化重点放到目标端写回

通用实现:源地址16B对齐

07.png

通用实现优先处理源地址对齐,保证 ldp 访问16B对齐,但 存储端完全没有对齐保证。

问题:未对齐 stp 会破坏写回路径的规整性

08.png

当 stp 写回跨越 cache line 边界时,会带来额外开销:

1. 实现复杂度增加:跨 cache line 的写回路径不如对齐写回规整,需要额外处理跨 line 边界的写回

2. 存储效率降低:与对齐的 stp 相比,跨边界写回往往会带来额外延迟

优化实现:目标地址32B对齐

09.png

为什么对齐dst而非src:

● 在这类大块拷贝场景下,未对齐 load 的代价通常相对可控,而未对齐 store,特别是连续 stp 的写回,惩罚往往更明显。因此,相比继续优化源端读取,对齐目标地址能带来更直接的收益。这也是这里选择优先对齐 dst、而不是 src 的主要原因。

● 32B对齐而非16B:stp 操作宽度为32B(2×16B),32B对齐有助于让 stp 的写回布局更规整,降低不理想对齐带来的额外开销

3.2 Pre-indexed寻址:把地址推进融合进访存指令

通用实现:地址更新依赖显式 add

10.png

独立的 add 指令带来两个问题:

1. 增加显式地址维护开销:每次循环都需要额外执行地址更新指令,循环体更长

2. 额外占用ALU发射端口:add 本身需要占用ALU(算术逻辑单元)发射资源,与 subs 等指令共享这部分执行资源

3. 增加指令数量:每次循环多2条 add 指令,增加前端取指/译码压力

优化实现:Pre-indexed寻址压缩循环体

11.png

[base, #imm]! 语法:! 后缀表示 pre-indexed 寻址——先更新基址寄存器(base += imm),再使用更新后的地址进行访存。一条指令同时完成地址更新和数据传输。

依赖链对比:

12.png

收益量化:

13.png

循环体从7条指令缩减到5条,减少了约 29% 的指令数量,同时减少了循环中的显式地址维护开销和ALU发射端口占用。

性能数据

基准结果反映了三类优化在不同场景下的收益差异,表中数值为各测试项的几何平均数(越低越好):

14.png

● bench-memcpy:固定大小测试,主要覆盖中小字节场景,SVE 谓词消除分支与重叠写回贡献显著

● bench-memcpy-large:大块复制测试,32B 对齐与 Pre-indexed 寻址带来稳定提升

● bench-memcpy-random:随机大小测试,三类优化综合生效,提升最为显著

总结

这次优化看起来分别落在三个区间,本质上却是在做同一件事:减少额外控制开销,缩短数据搬运路径,让拷贝过程更加直接、紧凑。

1. SVE whilelo 谓词:用硬件谓词替代软件分支,让处理器不再等待分支预测

2. 消除冗余分支 + 指令重排:用重叠写回替代条件跳转,让中等长度区间的处理路径更加统一

3. 32B对齐 + Pre-indexed寻址:改善大块写回路径的规整性,并减少循环中的显式地址维护开销和ALU发射端口压力

这类优化思路对其他热点内存操作同样有参考价值,尤其适合分析那些分支较多、地址维护较重、访存路径不够规整的场景。

附录:IFUNC——优化如何真正落到用户机器上

针对特定处理器做定制优化,用户代码是否需要修改?不需要。glibc 通过 IFUNC(Indirect Function) 机制实现了运行时自动选择——同一份 glibc 二进制包含多种 memcpy 实现,程序加载时动态链接器根据处理器型号自动挑选最优版本。

其工作流程为:程序启动 → 动态链接器解析 memcpy 符号 → 发现 IFUNC 属性,调用选择器函数 → 根据当前平台特征返回对应实现指针 → 后续所有 memcpy 调用直接跳转到选定实现,无额外开销。

在鲲鹏950平台上,IFUNC分发机制会将 memcpy 解析到优化后的实现版本。用户无需重新编译应用,升级系统即可获得性能提升。


「免责声明」:以上页面展示信息由第三方发布,目的在于传播更多信息,与本网站立场无关。我们不保证该信息(包括但不限于文字、数据及图表)全部或者部分内容的准确性、真实性、完整性、有效性、及时性、原创性等。相关信息并未经过本网站证实,不对您构成任何投资建议,据此操作,风险自担,以上网页呈现的图片均为自发上传,如发生图片侵权行为与我们无关,如有请直接微信联系g1002718958。 

更多推荐