指针与引用: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 的正确打开方式

说到内存管理,我想起刚入行时的一个项目。那时候我写了个图像处理库,满屏的 newdelete,结果内存泄漏得跟筛子似的。后来我学乖了——能用栈就别用堆,能用智能指针就别裸 new

new/delete 的底层逻辑

你想想看,new 到底干了什么?

  1. 调用 operator new 分配内存(底层走 malloc)
  2. 调用构造函数初始化对象
  3. 返回指向该对象的指针

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++ 基础回顾 指针与引用 • 指针可为空,引用必须初始化 • 引用更安全,指针更灵活 • 函数参数优先用 const 引用 内存管理 • new/delete 底层走 malloc/free • RAII:资源获取即初始化 • 智能指针替代裸指针 容器性能对比 • vector:堆分配,动态大小 • array:栈分配,编译期固定 • array 访问速度更快 核心原则:安全第一,性能第二,但两者可以兼得 —— 用 RAII 管理资源,用 array 提升性能

我的经验之谈:学 C++ 基础不是为了考试,而是为了写出更高效的 CUDA 内核。指针、引用、内存管理这些概念,在 GPU 编程中会以各种形式出现。比如,CUDA 的 __shared__ 内存本质上就是栈上的数组,而 cudaMalloc 分配的就是堆内存。理解了这些,你就能更好地驾驭 GPU 的存储层次。

好了,这一章的内容就到这里。记住:基础不牢,地动山摇。把这些概念吃透了,后面写 SwiGLU 内核时你会感谢自己的。


专注资料整理