指针与引用:C++ 高性能编程的第一道坎
说实话,很多刚接触高性能计算的同学,都会在指针和引用上栽跟头。我自己带团队时,发现一个规律:能用引用解决的问题,千万别用指针。这不是教条,是我在无数个调试到凌晨的夜晚总结出来的血泪教训。
指针 vs 引用:到底选哪个?
先看个最简单的例子:
// 指针版本
void process_v1(float* data, size_t size) {
for (size_t i = 0; i < size; ++i) {
data[i] = data[i] * 2.0f;
}
}
// 引用版本
void process_v2(std::vector<float>& data) {
for (auto& val : data) {
val = val * 2.0f;
}
}
你可能会说:「这不都一样吗?」嗯,表面上看差不多,但实际区别大了去了。
核心区别:
- 指针可以为空,引用必须初始化。这意味着用指针时你总得检查 nullptr,而引用不需要。
- 指针可以重新赋值,引用绑定后不能改。这其实是个优点——引用更「安全」。
- 指针支持算术运算,引用不行。但说实话,在 CUDA 编程里指针算术用得更多。
我个人习惯是:函数参数用 const 引用传参,需要修改时用非 const 引用,只有在必须表示「可能为空」时才用指针。这个原则帮我避免了很多段错误。
避坑指南:引用绑定临时对象
我曾经在项目里写过这样的代码:
const std::vector<int>& get_data() {
return std::vector<int>{1, 2, 3}; // 悬空引用!
}
返回局部对象的引用,函数结束对象就销毁了。这个 bug 让我查了整整两天,最后用 AddressSanitizer 才定位到。记住:永远不要返回局部对象的引用或指针。
内存管理:new/delete 的正确打开方式
说到内存管理,我想起刚入行时的一个项目。那时候我写了个图像处理库,满屏的 new 和 delete,结果内存泄漏得跟筛子似的。后来我学乖了——能用栈就别用堆,能用智能指针就别裸 new。
new/delete 的底层逻辑
你想想看,new 到底干了什么?
- 调用
operator new分配内存(底层走 malloc) - 调用构造函数初始化对象
- 返回指向该对象的指针
delete 则是反过来:先调用析构函数,再释放内存。这个过程其实挺重的,尤其是在高性能场景下。
小技巧:在 CUDA 编程中,我经常用 cudaMallocManaged 替代 new,它能自动管理 CPU 和 GPU 之间的数据迁移。但要注意,统一内存有性能开销,不是万能药。
RAII:C++ 最优雅的设计
说白了,RAII 就是「资源获取即初始化」。用构造函数获取资源,用析构函数释放资源。这样你永远不用担心忘记释放。
class CUDABuffer {
float* d_ptr;
size_t size;
public:
CUDABuffer(size_t n) : size(n) {
cudaMalloc(&d_ptr, n * sizeof(float));
}
~CUDABuffer() {
if (d_ptr) cudaFree(d_ptr);
}
// 禁止拷贝,允许移动
CUDABuffer(const CUDABuffer&) = delete;
CUDABuffer& operator=(const CUDABuffer&) = delete;
CUDABuffer(CUDABuffer&& other) noexcept
: d_ptr(other.d_ptr), size(other.size) {
other.d_ptr = nullptr;
}
};
我在项目中用这个模式封装了所有 CUDA 资源,内存泄漏率直接降到了零。嗯,这就是 RAII 的魅力。
std::vector vs std::array:性能对决
很多新手会问:「vector 和 array 到底哪个快?」我的回答是:看场景。但如果你要写 CUDA 内核,答案就很明确了。
性能对比表
| 特性 | std::vector | std::array |
|---|---|---|
| 内存位置 | 堆(heap) | 栈(stack) |
| 大小是否可变 | 是 | 否(编译期固定) |
| 访问速度 | 稍慢(间接寻址) | 极快(直接寻址) |
| 适合 CUDA | 不适合(主机端) | 适合(可拷贝到设备) |
| 内存开销 | 3 个指针(24 字节) | 仅数据本身 |
为什么 array 更快?
原因很简单:std::array 的数据在栈上,编译器能直接算出地址偏移量。而 std::vector 的数据在堆上,每次访问都要先解引用指针。在循环里,这个差异会被放大。
// 看看汇编你就明白了
std::array<float, 1024> arr;
std::vector<float> vec(1024);
// array 版本:直接 mov 指令
for (auto& v : arr) v *= 2.0f;
// vector 版本:先取指针,再 mov
for (auto& v : vec) v *= 2.0f;
我在优化 SwiGLU 内核时,把主机端的查找表从 vector 改成了 array,性能提升了 15%。别小看这点提升,在 GPU 上放大到百万线程,效果就很可观了。
实战建议:
- 编译期大小已知 → 用
std::array - 大小动态变化 → 用
std::vector - CUDA 设备端 → 用
std::array或裸数组 - 需要频繁 resize → 用
std::vector
知识体系总览
下面这张图是我自己总结的,涵盖了本章的核心知识点。你可以把它当作思维导图来用。
我的经验之谈:学 C++ 基础不是为了考试,而是为了写出更高效的 CUDA 内核。指针、引用、内存管理这些概念,在 GPU 编程中会以各种形式出现。比如,CUDA 的 __shared__ 内存本质上就是栈上的数组,而 cudaMalloc 分配的就是堆内存。理解了这些,你就能更好地驾驭 GPU 的存储层次。
好了,这一章的内容就到这里。记住:基础不牢,地动山摇。把这些概念吃透了,后面写 SwiGLU 内核时你会感谢自己的。