3. 显存类型与特性:全局内存、共享内存、寄存器、本地内存、常量内存、纹理内存

CUDA 编程模型将 GPU 上的可寻址存储空间划分为多种类型,每种类型在容量、延迟、带宽和可见性上差异显著。理解这些差异是编写高性能内核的前提——数据放置的位置,往往比计算指令本身更决定性能上限。本章逐一剖析六种核心显存类型,并给出实际使用建议。

3.1 全局内存(Global Memory)

全局内存是 GPU 中容量最大、最基础的存储空间,所有线程均可读写,并可在主机端通过 cudaMemcpy 访问。它位于显存(DRAM)中,通常为 8~80 GB。其访问延迟高达 400~800 个时钟周期,但吞吐量极大(数百 GB/s 至数 TB/s)。

全局内存的访问效率高度依赖合并访问(Coalesced Access):同一 warp(32 线程)访问的地址应尽量落在连续的 128 字节内存段内。若线程间地址跳跃,则会产生多次事务,浪费带宽。

关键参数:

  • 容量:最大(GB 级)
  • 延迟:400~800 周期
  • 缓存:L2 缓存(所有 SM 共享),部分架构有 L1 只读路径
  • 作用域:设备端所有线程 + 主机端
// 合并访问示例:连续索引
__global__ void copy_kernel(float *in, float *out, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) out[i] = in[i];  // 相邻线程访问相邻地址
}

3.2 共享内存(Shared Memory)

共享内存是每个线程块(Block)内部的高速可编程缓存,位于 SM 内部,延迟约为 20~30 个周期,远低于全局内存。它用于线程块内线程间数据共享与协作,典型容量为 48 KB~228 KB(可配置)。

共享内存被划分为 32 个存储体(Bank),每个存储体宽度 4 字节。当多个线程同时访问不同存储体时,可并行完成;若访问同一存储体(广播除外),则产生存储体冲突(Bank Conflict),导致访问串行化,性能下降数倍。

使用技巧:利用共享内存做数据复用(如矩阵分块、归约),可减少全局内存访问次数。声明方式:__shared__ float tile[32][32];。注意同步:__syncthreads() 确保块内线程看到彼此写入的数据。

3.3 寄存器(Registers)

寄存器是 GPU 上最快的存储单元,位于 SM 内部,延迟为 0(单周期访问)。每个线程的私有变量(如循环索引、临时计算值)默认存储在寄存器中。每个 SM 拥有 64K 个 32 位寄存器,分摊到每个线程通常为 32~255 个。

寄存器是稀缺资源:若每个线程使用过多寄存器,会降低 SM 上可同时驻留的线程块数量(Occupancy),从而影响延迟隐藏能力。编译器通过 -maxrregcount 或 __launch_bounds__ 控制寄存器分配。

注意:寄存器是线程私有的,线程间无法直接访问彼此的寄存器。若发生“寄存器溢出”(Register Spill),数据会被压入本地内存(Local Memory),性能急剧下降。

3.4 本地内存(Local Memory)

本地内存在物理上位于显存(DRAM)中,但逻辑上为每个线程私有。它用于存储寄存器放不下的数据:大型数组、动态索引的局部变量、递归调用栈等。访问延迟与全局内存相当(数百周期),且不经过 L1 缓存(仅 L2)。

本地内存的滥用通常源于:

  • 在核函数内声明大型局部数组(如 float arr[1024])
  • 使用非常量索引访问局部数组
  • 编译优化失败导致变量被强制放入本地内存

通过编译选项 -Xptxas -v 可查看内核的寄存器与本地内存使用量。若发现 local 字节数过大,应重构代码,改用共享内存或全局内存显式管理。

3.5 常量内存(Constant Memory)

常量内存是全局内存的只读缓存视图,容量 64 KB,专用于存储内核执行期间不变的参数(如权重、系数、查找表)。它拥有独立的常量缓存(Constant Cache),当 warp 内所有线程访问同一地址时,该访问被广播为单次事务,速度极快。

适用场景:所有线程读取同一个值(如标量参数、滤波器系数)。若线程访问不同地址,则退化为串行访问,性能反而低于全局内存。

// 常量内存声明(必须在文件作用域)
__constant__ float c_filter[128];

// 主机端复制
cudaMemcpyToSymbol(c_filter, h_filter, sizeof(float) * 128);

3.6 纹理内存(Texture Memory)

纹理内存是一种通过纹理缓存(Texture Cache)访问的只读内存,最初为图形渲染设计,现也用于通用计算。它支持二维/三维空间局部性优化,适合图像处理、插值计算等访问模式。纹理缓存针对空间邻近性做了优化,当线程访问相邻像素时命中率极高。

纹理内存还提供硬件插值(线性滤波)、边界处理(Clamp/Wrap)等功能,但通用计算中通常仅利用其缓存特性。现代架构(Volta 及以上)中,纹理内存与全局内存的 L1 路径已部分统一,但其只读缓存仍对特定访问模式有效。

存储类型 容量 延迟(周期) 缓存 作用域 典型用途
寄存器 每 SM 64K×32bit 0 无 线程私有 临时变量、索引
共享内存 48~228 KB/SM 20~30 无(显式管理) 线程块内 数据复用、块内通信
全局内存 GB 级 400~800 L2 所有线程+主机 主数据存储
本地内存 同全局 400~800 L2(无L1) 线程私有 寄存器溢出、大数组
常量内存 64 KB 广播时≈L1 常量缓存 所有线程只读 参数、系数
纹理内存 同全局 空间局部≈L1 纹理缓存 所有线程只读 图像、空间数据
GPU 显存层次与数据流路径 主机内存 CPU 侧数据源 全局内存(Global Memory)— 显存 DRAM,GB 级,延迟 400~800 周期 常量内存 (64KB) 纹理内存 本地内存 cudaMemcpy 流式多处理器 SM(Streaming Multiprocessor) 寄存器文件(Registers) 64K × 32bit / SM 延迟:0 周期 线程私有 共享内存(Shared) 48~228 KB / SM 延迟:20~30 周期 线程块内共享 Block 0 (线程 0~255) Thread 0..255 Block 1 (线程 0~255) Thread 0..255 全局→共享/寄存器 常量缓存 纹理缓存 寄存器溢出 图例与关键路径: 全局内存(主数据) 寄存器(最快) 共享内存(块内共享) 纹理/常量(只读缓存) 本地内存(溢出)

3.7 案例:矩阵乘法中的显存选择

以 1024×1024 矩阵乘法为例,展示不同显存策略的性能差异。朴素实现直接访问全局内存,每个输出元素需读取 2×1024 个浮点数,全局内存访问次数为 2×1024³ 次。采用共享内存分块(Tile 32×32)后,每个线程块仅需加载 2×32×32 个数据到共享内存,复用 32 次,全局内存访问量降低 32 倍。

// 共享内存分块矩阵乘法核心代码
__global__ void matmul_tiled(float *A, float *B, float *C, int N) {
    __shared__ float As[32][32];
    __shared__ float Bs[32][32];
    int row = blockIdx.y * 32 + threadIdx.y;
    int col = blockIdx.x * 32 + threadIdx.x;
    float sum = 0.0f;

    for (int tile = 0; tile < N/32; ++tile) {
        As[threadIdx.y][threadIdx.x] = A[row * N + tile * 32 + threadIdx.x];
        Bs[threadIdx.y][threadIdx.x] = B[(tile * 32 + threadIdx.y) * N + col];
        __syncthreads();
        for (int k = 0; k < 32; ++k)
            sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        __syncthreads();
    }
    C[row * N + col] = sum;
}

实测中,该优化在 NVIDIA A100 上可将吞吐量从约 1.2 TFLOPS 提升至 19.5 TFLOPS(接近理论峰值 19.5 TFLOPS 的 80%)。关键收益来自:共享内存减少全局内存压力,寄存器缓存中间累加值,避免本地内存溢出。

3.8 常见误区与性能复盘

误区 1:所有数据都放共享内存就一定快。共享内存容量有限,且过度使用会降低 Occupancy。当块内线程数较少或数据复用率低时,直接使用全局内存(依赖 L2 缓存)反而更优。

误区 2:忽略寄存器溢出。在核函数中声明 float local[64] 且用变量索引访问,编译器可能将其放入本地内存。应使用 -Xptxas -v 检查,必要时改用共享内存或重构算法。

误区 3:常量内存随意使用。若 warp 内线程访问不同常量地址,常量缓存会串行化访问,性能下降。只有“所有线程读同一地址”时才高效。

复盘建议:使用 NVIDIA Nsight Compute 分析内核的 Memory Workload Analysis 页面,查看各存储类型的吞吐量与利用率。重点关注:全局内存合并率(应接近 100%)、共享内存 Bank Conflict 次数(应为 0)、寄存器溢出量(应为 0)。若共享内存利用率低于 60%,考虑增大分块尺寸;若寄存器溢出,减少每线程寄存器占用或调整算法。

总结:六种显存类型构成 GPU 的存储层次——寄存器与共享内存是性能关键,全局内存是数据主体,常量与纹理内存针对特定访问模式优化,本地内存则是需要避免的“性能陷阱”。合理规划数据在各级存储中的流动,是 GPU 性能优化的核心艺术。