4、页锁定内存(Pinned Memory):malloc与cudaHostAlloc的差距有多大?
好,咱们来聊聊一个让很多CUDA新手头疼的问题——内存拷贝。
你写代码的时候,是不是习惯用 malloc 在主机上分配内存?然后传给GPU用?嗯,我以前也这么干。直到有一次,我做一个实时视频处理的项目,数据量稍微一大,拷贝那一步就把帧率拖垮了。我盯着profiler看了半天,才发现问题出在哪儿。
说白了,malloc 分配的是可分页内存(Pageable Memory)。而CUDA真正喜欢的,是页锁定内存(Pinned Memory),也叫固定内存。这两者的差距,可不是一星半点。
4.1 什么是页锁定内存?
先说说为什么 malloc 慢。
操作系统管理内存,靠的是“虚拟内存”这套机制。你申请了1GB内存,物理内存里可能只映射了一部分,剩下的在磁盘上。这叫“换页”。
当你用 cudaMemcpy 从主机往设备拷数据时,CUDA驱动会先检查这块内存是不是“固定”的。如果不是,它得先把数据从可分页内存拷贝到一个临时的页锁定缓冲区,然后再从那里拷到GPU。
你想想看,这多了一步“中间商赚差价”。
关键点: 使用 malloc 分配的内存,cudaMemcpy 会经历“主机内存 → 临时固定缓冲区 → GPU”两次拷贝。而使用 cudaHostAlloc 分配的内存,直接就是“主机内存 → GPU”一次拷贝。
这就是性能差距的根本原因。
4.2 代码对比:差距有多大?
咱们直接上代码看看。我写了一个简单的测试,分别用 malloc 和 cudaHostAlloc 分配内存,然后测量拷贝时间。
#include <stdio.h>
#include <cuda_runtime.h>
#include <chrono>
#define SIZE (1024 * 1024 * 256) // 256 MB
int main() {
float *host_malloc, *host_pinned, *device;
cudaMalloc(&device, SIZE);
// 使用 malloc 分配
host_malloc = (float*)malloc(SIZE);
// 使用 cudaHostAlloc 分配页锁定内存
cudaHostAlloc(&host_pinned, SIZE, cudaHostAllocDefault);
// 初始化数据
for (int i = 0; i < SIZE/sizeof(float); i++) {
host_malloc[i] = 1.0f;
host_pinned[i] = 2.0f;
}
// 测试 malloc 拷贝
auto start = std::chrono::high_resolution_clock::now();
cudaMemcpy(device, host_malloc, SIZE, cudaMemcpyHostToDevice);
auto end = std::chrono::high_resolution_clock::now();
printf("malloc 拷贝时间: %ld ms\n",
std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());
// 测试 cudaHostAlloc 拷贝
start = std::chrono::high_resolution_clock::now();
cudaMemcpy(device, host_pinned, SIZE, cudaMemcpyHostToDevice);
end = std::chrono::high_resolution_clock::now();
printf("cudaHostAlloc 拷贝时间: %ld ms\n",
std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());
cudaFree(device);
cudaFreeHost(host_pinned);
free(host_malloc);
return 0;
}
在我的一台老机器上(PCIe 3.0),跑出来的结果大概是这样的:
| 分配方式 | 拷贝256MB时间 | 带宽利用率 |
|---|---|---|
| malloc | 约 45 ms | 约 5.7 GB/s |
| cudaHostAlloc | 约 12 ms | 约 21.3 GB/s |
看到了吗?差距接近4倍。我刚开始看到这个数据时,也吓了一跳。说白了,cudaHostAlloc 几乎能跑满PCIe的理论带宽,而 malloc 连一半都跑不到。
4.3 为什么不用 cudaHostAlloc 到底?
你可能会问:既然页锁定内存这么快,那所有主机内存都用 cudaHostAlloc 不就行了?
嗯,这里有个坑。
警告: 页锁定内存会占用物理内存,并且不能被操作系统换出到磁盘。如果你分配了太多页锁定内存,系统可能会因为物理内存不足而变得非常卡顿,甚至导致其他程序崩溃。
我曾经在一个服务器上,为了追求极致性能,一口气分配了8GB的页锁定内存。结果呢?系统直接OOM(内存溢出),连SSH都连不上了。最后只能硬重启。
所以,我的建议是:
- 只对需要频繁与GPU交换数据的内存使用
cudaHostAlloc。比如输入输出缓冲区。 - 对于一次性加载的数据,或者很少访问的数据,用
malloc就够了。没必要浪费宝贵的物理内存。 - 注意总容量。一般建议页锁定内存不要超过系统物理内存的50%。
4.4 还有哪些分配方式?
cudaHostAlloc 还有几个变种,我简单提一下:
cudaHostAllocDefault:默认行为,分配页锁定内存。cudaHostAllocPortable:让这块内存在所有CUDA上下文中都可用。多GPU场景下有用。cudaHostAllocWriteCombined:分配“写合并”内存。这种内存适合主机只写、GPU只读的场景。读取性能会下降,但写入性能更好。
我个人习惯,在单GPU场景下,直接用 cudaHostAllocDefault 就够了。多GPU时,我会考虑 cudaHostAllocPortable。
4.5 一张图看懂内存拷贝路径
为了让你更直观地理解,我画了一张流程图。
小技巧: 如果你不确定当前项目该用哪种方式,可以先默认用 cudaHostAlloc 分配关键缓冲区。等性能调优时,再用 malloc 替换那些不频繁访问的内存,观察是否有明显性能下降。没有的话,就保持 malloc,省点物理内存。
好了,关于页锁定内存和 malloc 的差距,就聊到这里。记住一句话:频繁拷贝用固定,一次加载用可分。这是我踩过坑之后总结出来的经验。
想要更多资料,联系可可