CUDA编程之限定词(2)

1. 内存空间限定符(决定数据住哪里)

  • __shared__:声明在 共享内存 中。它位于片上,速度极快(比全局内存快 ~100 倍),仅对同一个线程块(Block)内的所有线程可见,用于线程间通信和数据重用。声明时加 extern 可动态分配大小(即 <<<>> 里的第 3 个参数)。

  • __constant__:声明在 常量内存 中。只读,有缓存加速,对所有网格(Grid)中的所有线程可见。适合存储所有线程都会用到的只读系数(如滤波器权重),容量仅 64KB。

  • __managed__:声明在 托管内存 中。这是最"省事"的限定词,CPU 和 GPU 都能直接通过指针访问,数据在底层自动迁移。适合简化复杂数据结构(如链表)的传输,但需注意隐式同步带来的性能开销。

实例 __shared__ ------ 线程块内共享缓存(数组归约求和)

cpp 复制代码
__global__ void sum_reduce_kernel(const float* input, float* output, int n) {
    // 声明静态共享内存(大小固定,这里假设 Block 大小为 256)
    __shared__ float sdata[256];
    
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    int tid = threadIdx.x;

    // 加载数据到共享内存(合并访问)
    sdata[tid] = (idx < n) ? input[idx] : 0.0f;
    __syncthreads(); // 确保所有线程都加载完毕

    // 循环归约(树形加法)
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
        if (tid < stride) {
            sdata[tid] += sdata[tid + stride];
        }
        __syncthreads(); // 每轮归约后同步,确保数据一致
    }

    // 将当前 Block 的最终结果写回全局内存
    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}

关键点__shared__ 让数据留在片上,延迟从 ~400 周期(全局)降到 ~5 周期(共享),且 __syncthreads() 是它的"黄金搭档"。

实例 :__constant__ ------ 只读常量缓存(滤波器系数)

场景:所有线程共享一组固定的滤波系数,存于常量内存(64KB 限制)并享受专用缓存加速。

cpp 复制代码
// 在文件作用域声明(全局可见)
__constant__ float filter_coeff[5] = {0.1f, 0.2f, 0.4f, 0.2f, 0.1f};

__global__ void convolution_kernel(const float* input, float* output, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n && idx >= 2) {
        float sum = 0.0f;
        // 所有线程并发读取常量内存(有缓存,且广播给 Warp 内线程)
        for (int i = -2; i <= 2; ++i) {
            sum += input[idx + i] * filter_coeff[i + 2];
        }
        output[idx] = sum;
    }
}

关键点 :若在 CPU 端修改该数组,需用 cudaMemcpyToSymbol(filter_coeff, host_coeff, sizeof(host_coeff))

实例 :__managed__ ------ 自动迁移内存(简化链表/指针结构)

场景 :让 CPU 和 GPU 共享同一个复杂结构体指针,免去手动 cudaMemcpy

cpp 复制代码
// 声明托管内存变量(设备端和主机端均可直接读写)
__device__ __managed__ int managed_data[100];

__global__ void add_kernel() {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < 100) {
        managed_data[idx] *= 2; // GPU 直接修改
    }
}

int main() {
    // CPU 直接初始化(无需 cudaMemcpy)
    for (int i = 0; i < 100; ++i) managed_data[i] = i;

    // 启动内核(数据会自动迁移到 GPU)
    add_kernel<<<1, 100>>>();
    cudaDeviceSynchronize(); // 必须同步,等待自动迁移完成

    // CPU 直接读取结果(GPU 修改后自动迁回)
    printf("managed_data[10] = %d\n", managed_data[10]); // 输出 20
}

关键点:极大简化编码,但页错误迁移有开销,适合数据访问频率不高或指针结构复杂的场景。

2. 编译器优化提示符(决定跑多快)

  • __restrict__(极其重要):用于修饰指针,向编译器承诺:该指针是访问某块内存区域的唯一入口(不存在别名)。这允许编译器激进地优化(如向量化加载指令 LDS.128),是榨取性能最常用的手段。不加的话,编译器会保守地插入额外同步指令。

  • __forceinline____noinline__:强制建议编译器对 __device__ 函数进行内联或禁止内联。默认情况下,小函数会被内联以消除调用开销;但若函数体过大,强制内联会膨胀寄存器占用,导致并行度下降,此时用 __noinline__ 反而能提升性能。

3. 资源引导符(决定能开多少线程)

  • __launch_bounds__:放在 __global__ 内核声明前,例如 __launch_bounds__(256, 4)。它告诉编译器:"我将用 256 线程/块 启动,且最多占用 4 个块 的寄存器资源。"这能让编译器精确控制寄存器分配,避免因寄存器溢出导致占用率暴跌。建议在确定启动配置后加上。

实例:__restrict__ ------ 编译器优化"王牌"(指针别名消除)

场景 :向编译器承诺三个指针指向不同的内存区域,让编译器放心地将多条加载指令合并为单条向量化指令。

cpp 复制代码
// 不加 __restrict__:编译器会保守地假设 a 和 b 可能指向同一地址
__global__ void add_no_restrict(const float* a, const float* b, float* c, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n) c[idx] = a[idx] + b[idx];
}

// 加 __restrict__:性能可提升 20%~50%
__global__ void add_restrict(const float* __restrict__ a, 
                             const float* __restrict__ b, 
                             float* __restrict__ c, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if (idx < n) c[idx] = a[idx] + b[idx];
}

关键点 :仅当你确定 传入的 abc 在显存中没有重叠(非同一数组别名)时才能用。如果乱用,会导致计算结果错误,相当危险。


实例 :__forceinline__ + __launch_bounds__(高级优化组合)

场景:小工具函数强制内联消除调用开销;提前告知编译器线程配置,控制寄存器分配。

cpp 复制代码
// 强制内联:消除函数调用指令(适合极小函数)
__device__ __forceinline__ float warp_reduce_add(float val) {
    // 单 Warp 内归约(无需 __syncthreads)
    val += __shfl_xor_sync(0xFFFFFFFF, val, 16);
    val += __shfl_xor_sync(0xFFFFFFFF, val, 8);
    val += __shfl_xor_sync(0xFFFFFFFF, val, 4);
    val += __shfl_xor_sync(0xFFFFFFFF, val, 2);
    val += __shfl_xor_sync(0xFFFFFFFF, val, 1);
    return val;
}

// 告知编译器:该内核以 256 线程/块 启动,最多占据 4 个块
__global__ __launch_bounds__(256, 4) void compute_kernel(float* data, int n) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    float sum = 0.0f;
    if (idx < n) sum = data[idx];
    sum = warp_reduce_add(sum); // 内联展开
    // ... 后续逻辑
}

关键点__launch_bounds__ 中的参数需与实际启动的 blockDim.x 一致。它能防止编译器为节省寄存器而分配过多,导致 Occupancy(占用率)下降。

总结

限定词 使用时机 反面教材
__shared__ 块内数据复用(如滑窗、归约) 声明过大会导致动态分配失败
__constant__ 所有线程共用的只读小数据 存放会频繁修改的变量(性能暴跌)
__managed__ 复杂数据结构(链表/树) 在极致性能热路径中频繁访问(页迁移开销大)
__restrict__ 全局内存指针运算 传入同一数组的不同偏移(别名冲突)
__forceinline__ 小于 10 条指令的小函数 大函数强制内联会爆寄存器(用 __noinline__ 代替)