4. 拷贝优化策略:循环展开、SIMD指令集、缓存行对齐、大页内存
内存拷贝这事儿,看着简单,其实坑不少。我早年做网络协议栈优化时,就发现一个尴尬现象:CPU占用率飙到90%,但吞吐死活上不去。后来一查,问题全出在memcpy上。说白了,拷贝操作本身不复杂,复杂的是怎么让CPU和内存子系统配合好。
今天咱们就聊聊四个实战中非常有效的优化策略。每个我都踩过坑,也尝过甜头。
4.1 循环展开:让CPU少做判断
先看最基础的拷贝实现。很多人写循环拷贝,习惯这样:
void naive_copy(char* dst, char* src, size_t n) {
for (size_t i = 0; i < n; i++) {
dst[i] = src[i];
}
}
这个写法没问题,但效率不高。为什么?因为每次循环都要做三件事:比较i是否小于n、递增i、跳转回循环头。你想想看,如果拷贝1MB数据,CPU得做100万次无意义的判断和跳转。
循环展开的思路很简单——一次循环处理多个元素,减少循环次数。
void unrolled_copy(char* dst, char* src, size_t n) {
size_t i = 0;
// 一次处理8个字节
for (; i + 7 < n; i += 8) {
dst[i] = src[i];
dst[i+1] = src[i+1];
dst[i+2] = src[i+2];
dst[i+3] = src[i+3];
dst[i+4] = src[i+4];
dst[i+5] = src[i+5];
dst[i+6] = src[i+6];
dst[i+7] = src[i+7];
}
// 处理剩余字节
for (; i < n; i++) {
dst[i] = src[i];
}
}
4.2 SIMD指令集:一次干更多活
循环展开只是减少了循环开销,但每次还是只处理一个字节。SIMD就不一样了——它让CPU一次处理16、32甚至64个字节。
以x86平台的SSE2为例,一次能搬16字节:
#include <emmintrin.h>
void simd_copy(char* dst, char* src, size_t n) {
size_t i = 0;
// 一次处理16字节
for (; i + 15 < n; i += 16) {
__m128i data = _mm_loadu_si128((__m128i*)(src + i));
_mm_storeu_si128((__m128i*)(dst + i), data);
}
// 处理剩余字节
for (; i < n; i++) {
dst[i] = src[i];
}
}
如果你用的是现代CPU,AVX2能一次处理32字节,AVX-512更是能到64字节。我在项目中用AVX2优化过视频帧拷贝,吞吐直接翻了一倍多。
4.3 缓存行对齐:别让CPU做无用功
现代CPU的缓存行通常是64字节。什么意思?CPU从内存读数据,一次最少读64字节。如果你拷贝的数据跨了两个缓存行,CPU就得读两次。
看个例子:
// 不对齐的情况
char buffer[100];
char* misaligned_ptr = buffer + 1; // 地址不是64的倍数
// 读这个指针指向的数据,CPU要读两个缓存行
解决办法很简单——让数据地址对齐到缓存行边界:
// 使用aligned_alloc分配对齐内存
char* buf = (char*)aligned_alloc(64, 1024); // 64字节对齐
// 或者用posix_memalign
void* ptr;
posix_memalign(&ptr, 64, 1024);
我在做高性能日志系统时,把日志缓冲区都做了64字节对齐。配合SIMD拷贝,写入延迟从微秒级降到了纳秒级。说白了,对齐这件事,成本几乎为零,收益却实实在在。
| 对齐方式 | 拷贝吞吐(GB/s) | CPU开销 |
|---|---|---|
| 不对齐 | 8.2 | 高 |
| 16字节对齐 | 10.5 | 中 |
| 64字节对齐 | 12.1 | 低 |
4.4 大页内存:减少TLB缺失
这个策略稍微进阶一点,但效果非常明显。TLB(页表缓存)是CPU用来加速虚拟地址到物理地址转换的。默认的4KB小页,意味着1GB内存需要256K个页表项。TLB根本装不下。
大页内存(通常是2MB或1GB)能大幅减少页表项数量。1GB内存用2MB大页,只需要512个页表项,TLB基本不会缺失。
Linux下启用大页内存:
// 首先确认系统支持
cat /proc/meminfo | grep HugePages
// 分配大页(需要root权限)
echo 1024 > /proc/sys/vm/nr_hugepages
// 在代码中使用
#include <sys/mman.h>
void* buf = mmap(NULL, 2 * 1024 * 1024,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
知识体系总览
下面这张图把四种策略的关系梳理了一下,方便你理解:
这四种策略不是互斥的,实际项目中我经常组合使用。比如在做视频帧拷贝时,我会用大页分配内存,确保缓存行对齐,然后用AVX2指令做循环展开拷贝。效果嘛,比原始memcpy快了将近3倍。
最后提醒一句:优化前一定要先测量。我见过太多人一上来就搞SIMD、搞大页,结果瓶颈根本不在拷贝上。先profiling,再优化,这是铁律。
想要更多资料,联系可可