GPU 的内存层次结构是其高性能的关键。理解 GPU 的内存层次结构对于编写高效的 CUDA 程序至关重要。本文将详细介绍 GPU 的内存层次结构及其优化策略。
GPU 内存层次概览
GPU 的内存层次结构从快到慢依次为:
| 内存类型 | 访问速度 | 容量 | 作用域 | 缓存 |
|---|---|---|---|---|
| Registers(寄存器) | 最快 | 极小(每个线程) | 线程私有 | 无 |
| Shared Memory(共享内存) | 极快 | 小(每个 Block) | Block 共享 | 无 |
| L1 Cache | 快 | 小(每个 SM) | Block 共享 | 有 |
| L2 Cache | 中等 | 中(整个 GPU) | 全局 | 有 |
| Global Memory(全局内存) | 慢 | 大(整个 GPU) | 全局 | 有(L1/L2) |
| Constant Memory(常量内存) | 快 | 小(64KB) | 全局 | 有 |
| Texture Memory(纹理内存) | 快 | 中 | 全局 | 有 |
寄存器(Registers)
寄存器是 GPU 上最快的存储器,每个线程都有自己的一组寄存器。
寄存器特性
- 速度:访问延迟约 1 个时钟周期
- 容量:每个 SM 有数千个寄存器,每个线程分配一部分
- 作用域:线程私有,其他线程无法访问
- 生命周期:与线程生命周期相同
寄存器使用示例
__global__ void kernel(float *data) {
// 局部变量通常存储在寄存器中
int idx = threadIdx.x + blockIdx.x * blockDim.x;
float temp = data[idx]; // temp 可能存储在寄存器中
temp *= 2.0f;
data[idx] = temp;
}
寄存器优化
- 减少寄存器使用可以提高占用率
- 使用
__launch_bounds__指定最大线程数 - 避免过多的局部变量
// 指定每个 Block 最多 256 个线程,每个 SM 最多 2 个 Block
__global__ void __launch_bounds__(256, 2) optimizedKernel(float *data) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
data[idx] = idx;
}
共享内存(Shared Memory)
共享内存是 SM 内部的高速存储器,同一个 Block 内的线程可以共享访问。
共享内存特性
- 速度:比全局内存快 100 倍以上
- 容量:通常 48KB-96KB(可配置)
- 作用域:Block 内线程共享
- 生命周期:与 Block 生命周期相同
共享内存使用示例
#define TILE_SIZE 16
__global__ void matrixMul(float *A, float *B, float *C, int N) {
// 声明共享内存
__shared__ float tileA[TILE_SIZE][TILE_SIZE];
__shared__ float tileB[TILE_SIZE][TILE_SIZE];
int bx = blockIdx.x, by = blockIdx.y;
int tx = threadIdx.x, ty = threadIdx.y;
int row = by * TILE_SIZE + ty;
int col = bx * TILE_SIZE + tx;
float sum = 0.0f;
for (int t = 0; t < N / TILE_SIZE; ++t) {
// 加载数据到共享内存
tileA[ty][tx] = A[row * N + t * TILE_SIZE + tx];
tileB[ty][tx] = B[(t * TILE_SIZE + ty) * N + col];
// 等待所有线程完成加载
__syncthreads();
// 使用共享内存中的数据进行计算
for (int k = 0; k < TILE_SIZE; ++k) {
sum += tileA[ty][k] * tileB[k][tx];
}
// 等待所有线程完成计算
__syncthreads();
}
C[row * N + col] = sum;
}
共享内存优化
- Bank Conflict:避免 Bank 冲突,提高访问效率
- Pad:使用 padding 避免 Bank 冲突
- 异步拷贝:使用
__cp_async异步加载数据
// 避免 Bank Conflict 的示例
__global__ void avoidBankConflict(float *data) {
__shared__ float s_data[32]; // 32 个 Bank,每个 Bank 4 字节
int tid = threadIdx.x;
s_data[tid] = data[tid]; // 每个线程访问不同 Bank,无冲突
__syncthreads();
data[tid] = s_data[tid];
}
// 使用 Padding 避免 Bank Conflict
__global__ void withPadding(float *data) {
__shared__ float s_data[32][33]; // 添加 Padding
int tid = threadIdx.x;
s_data[tid][0] = data[tid]; // 避免 Bank 冲突
__syncthreads();
data[tid] = s_data[tid][0];
}
全局内存(Global Memory)
全局内存是 GPU 上最大的存储空间,但访问速度最慢。
全局内存特性
- 速度:访问延迟高(数百个时钟周期)
- 容量:大(几 GB 到几十 GB)
- 作用域:全局,所有线程都可以访问
- 缓存:有 L1/L2 缓存
内存合并访问
当相邻线程访问连续的内存地址时,GPU 可以将多个内存请求合并为一个事务。
// 好的内存合并
__global__ void coalescedAccess(float *data) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
data[idx] = idx; // 连续访问,合并
}
// 不好的内存访问
__global__ void stridedAccess(float *data, int stride) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
data[idx * stride] = idx; // 跨步访问,不合并
}
全局内存优化
- 使用行优先存储(Row-major)
- 避免跨步访问(Strided Access)
- 使用共享内存减少全局内存访问
- 考虑数据对齐(Alignment)
常量内存(Constant Memory)
常量内存是只读内存,有缓存优化,适合存储常量数据。
常量内存特性
- 速度:快,有缓存
- 容量:64KB
- 作用域:全局
- 访问:只读
常量内存使用示例
// 声明常量内存
__constant__ float constData[256];
// 从主机复制数据到常量内存
cudaMemcpyToSymbol(constData, hostData, 256 * sizeof(float));
// 在 Kernel 中使用
__global__ void useConstant(float *data) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
data[idx] = constData[idx % 256]; // 从常量内存读取
}
纹理内存(Texture Memory)
纹理内存是专门为图形渲染优化的内存,也适用于某些计算场景。
纹理内存特性
- 速度:快,有缓存
- 容量:中
- 作用域:全局
- 特性:支持 2D/3D 寻址、插值、边界处理
纹理内存使用示例
// 声明纹理引用
texture texRef;
// 绑定纹理到全局内存
cudaBindTexture(NULL, texRef, d_data, width * height * sizeof(float));
// 在 Kernel 中使用纹理
__global__ void useTexture(float *output) {
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.y;
// 使用纹理坐标读取
float u = x / (float)width;
float v = y / (float)height;
output[y * width + x] = tex2D(texRef, u, v); // 纹理读取
}
// 解绑纹理
cudaUnbindTexture(texRef);
内存访问模式优化
1. 内存合并
确保相邻线程访问连续的内存地址。
2. 避免 Bank 冲突
在共享内存中,避免多个线程同时访问同一个 Bank。
3. 使用共享内存
利用共享内存减少全局内存访问次数。
4. 数据重用
将频繁使用的数据加载到共享内存或寄存器中。
内存管理函数
// 分配全局内存
float *d_data;
cudaMalloc(&d_data, N * sizeof(float));
// 释放全局内存
cudaFree(d_data);
// 内存拷贝:主机到设备
cudaMemcpy(d_data, h_data, N * sizeof(float), cudaMemcpyHostToDevice);
// 内存拷贝:设备到主机
cudaMemcpy(h_data, d_data, N * sizeof(float), cudaMemcpyDeviceToHost);
// 内存拷贝:设备到设备
cudaMemcpy(d_dest, d_src, N * sizeof(float), cudaMemcpyDeviceToDevice);
// 内存填充
cudaMemset(d_data, 0, N * sizeof(float));
// 异步内存拷贝
cudaMemcpyAsync(d_data, h_data, N * sizeof(float), cudaMemcpyHostToDevice, stream);
总结
GPU 的内存层次结构是其高性能的关键。理解不同内存类型的特性和访问模式,可以帮助我们编写高效的 CUDA 程序:
- 寄存器:最快的存储,用于局部变量
- 共享内存:Block 内共享,用于数据重用
- 全局内存:最大的存储,但访问慢,需要优化访问模式
- 常量/纹理内存:只读内存,有缓存优化
通过合理使用不同层次的内存,可以充分发挥 GPU 的计算能力,实现高性能的并行计算。