以下是针对 2.2.3 GPU Device Memory Spaces 章节内容的详细知识点整理,包含所有内存类型的特性、使用方法及最佳实践。
2.2.3. GPU Device Memory Spaces (GPU设备内存空间)
内存类型概览表
| 内存类型 | 作用域 | 生命周期 | 物理位置 | 关键特性 |
|---|---|---|---|---|
| 全局内存 | 网格(所有线程) | 应用程序 | 设备 | 容量大,延迟高,所有线程可访问 |
| 常量内存 | 网格(所有线程) | 应用程序 | 设备 | 只读,缓存优化,延迟低 |
| 共享内存 | 块(块内线程) | 内核 | SM | 用户管理,低延迟,高带宽 |
| 局部内存 | 线程 | 内核 | 设备 | 逻辑线程局部,物理在全局内存 |
| 寄存器 | 线程 | 内核 | SM | 最快存储,编译器管理 |
2.2.3.1. Global Memory (全局内存)
基本概念
- 别名:设备内存(device memory)
- 类比:相当于CPU系统中的RAM
- 作用:内核中所有线程都可以访问的主要存储空间
关键特性
| 特性 | 说明 |
|---|---|
| 可访问性 | 网格内的所有线程都可读/写 |
| 持久性 | 分配后持续存在,直到显式释放或程序终止 |
| 生命周期 | 从分配到cudaFree()或cudaDeviceReset() |
分配与释放API
| 操作 | API | 说明 |
|---|---|---|
| 分配 | cudaMalloc() |
分配设备内存 |
| 分配(统一内存) | cudaMallocManaged() |
分配可由CPU/GPU访问的内存 |
| 拷贝 | cudaMemcpy() |
主机与设备间数据传输 |
| 释放 | cudaFree() |
释放设备内存 |
使用流程
cpp
// 1. 分配全局内存
cudaMalloc(&d_data, size);
// 2. 初始化数据(通过拷贝)
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
// 3. 内核中使用
kernel<<<grid, block>>>(d_data);
// 4. 将结果拷贝回主机
cudaMemcpy(h_result, d_data, size, cudaMemcpyDeviceToHost);
// 5. 释放内存
cudaFree(d_data);
注意事项
- 数据竞争:多个线程同时读写同一位置需同步
- 返回值 :内核是
void类型,只能通过全局内存将结果返回主机 - 持久性:数据在内核执行间持续存在,可被多个内核使用
示例:vecAdd内核
cpp
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
{
int workIndex = threadIdx.x + blockIdx.x * blockDim.x;
if(workIndex < vectorLength)
{
C[workIndex] = A[workIndex] + B[workIndex];
}
}
- A、B、C指针指向全局内存
- 所有线程可同时读取A、B,写入C
2.2.3.2. Shared Memory (共享内存)
基本概念
- 物理位置:每个SM内部
- 资源关系:与L1缓存共享同一物理资源
- 类比:用户管理的暂存器(scratchpad)
关键特性
| 特性 | 说明 |
|---|---|
| 可访问性 | 同一线程块内的所有线程 |
| 生命周期 | 内核执行期间 |
| 性能 | 比全局内存带宽更高、延迟更低 |
| 容量 | 较小,依GPU架构而异 |
同步机制:__syncthreads()
cpp
__syncthreads();
- 作用:阻塞块内所有线程,直到全部到达该调用点
- 用途:确保所有线程对共享内存的写入完成后再读取
- 注意:在条件分支中使用需谨慎(可能导致死锁)
同步示例
cpp
__global__ void example_syncthreads(int* input_data, int* output_data) {
__shared__ int shared_data[128];
// 每个线程写入共享内存的不同位置
shared_data[threadIdx.x] = input_data[threadIdx.x];
// 同步:确保所有写入完成
__syncthreads();
// 单个线程安全地读取所有共享数据
if (threadIdx.x == 0) {
int sum = 0;
for (int i = 0; i < blockDim.x; ++i) {
sum += shared_data[i];
}
output_data[blockIdx.x] = sum;
}
}
共享内存大小查询
| 属性 | 说明 |
|---|---|
sharedMemPerMultiprocessor |
每个SM的共享内存总量 |
sharedMemPerBlock |
每个线程块可用的最大共享内存 |
cpp
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device);
printf("Shared memory per SM: %zu\n", prop.sharedMemPerMultiprocessor);
printf("Shared memory per block: %zu\n", prop.sharedMemPerBlock);
缓存配置:cudaFuncSetCacheConfig()
cpp
cudaFuncSetCacheConfig(kernel, cudaFuncCachePreferShared);
// 选项:
// cudaFuncCachePreferNone - 无偏好
// cudaFuncCachePreferShared - 偏好更多共享内存
// cudaFuncCachePreferL1 - 偏好更多L1缓存
// cudaFuncCachePreferEqual - 偏好平衡
- 注意 :这只是给运行时的提示,不保证一定采用
- 运行时根据资源和内核需求自由决定
2.2.3.2.1. Static Allocation (静态分配)
语法
cpp
__shared__ float sharedArray[1024];
特点
- 在内核内部声明
- 使用
__shared__说明符 - 大小必须在编译时确定
- 所有块内线程都可访问
示例
cpp
__global__ void kernel() {
__shared__ int cache[256];
cache[threadIdx.x] = threadIdx.x;
__syncthreads();
// 使用cache...
}
2.2.3.2.2. Dynamic Allocation (动态分配)
启动时指定大小
cpp
// 第三个参数指定共享内存大小(字节)
kernel<<<grid, block, sharedMemoryBytes>>>();
内核内声明
cpp
extern __shared__ float sharedArray[];
多个动态数组的手动分区
正确方法(注意对齐):
cpp
extern __shared__ float array[];
short* array0 = (short*)array; // 2字节对齐
float* array1 = (float*)&array0[128]; // 4字节对齐
int* array2 = (int*)&array1[64]; // 4字节对齐
错误示例(未对齐):
cpp
extern __shared__ float array[];
short* array0 = (short*)array;
float* array1 = (float*)&array0[127]; // ❌ 未4字节对齐
对齐要求:
- 指针必须按指向类型对齐
short需要2字节对齐,float/int需要4字节对齐
2.2.3.3. Registers (寄存器)
基本概念
- 物理位置:SM内部
- 作用域:线程局部
- 管理方式:由编译器自动管理
关键特性
| 特性 | 说明 |
|---|---|
| 速度 | 最快的存储类型 |
| 容量 | 有限,依GPU架构而异 |
| 分配 | 编译器为每个线程分配 |
| 查询 | regsPerMultiprocessor 和 regsPerBlock |
寄存器查询
cpp
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, device);
printf("Registers per SM: %d\n", prop.regsPerMultiprocessor);
printf("Registers per block: %d\n", prop.regsPerBlock);
控制寄存器使用:-maxrregcount
bash
nvcc -maxrregcount=32 kernel.cu # 限制内核最多使用32个寄存器/线程
影响
- 减少寄存器使用 → 可在SM上调度更多线程块
- 过度减少 → 导致寄存器溢出(spilling),变量被移至局部内存
- 权衡:并行度 vs 每个线程的性能
2.2.3.4. Local Memory (局部内存)
基本概念
- 逻辑作用域:线程局部
- 物理位置 :全局内存中
- 命名来源:名称来自逻辑作用域,而非物理位置
使用场景
编译器将自动变量放入局部内存的情况:
| 场景 | 说明 |
|---|---|
| 索引不确定的数组 | 数组索引不是编译时常量 |
| 大型结构或数组 | 占用寄存器空间过大 |
| 寄存器溢出 | 内核使用寄存器超过可用数量 |
性能特性
| 特性 | 说明 |
|---|---|
| 延迟 | 与全局内存相同(高延迟) |
| 带宽 | 与全局内存相同 |
| 合并访问 | 连续线程访问连续32位字时可合并 |
局部内存的合并访问
局部内存组织方式:
- 连续32位字被连续线程ID访问
- 只要warp内所有线程访问相同相对地址 ,访问就是完全合并的
示例:
cpp
// 如果arr被编译器放入局部内存
int arr[4];
arr[0] = threadIdx.x; // 所有线程访问arr[0] → 合并访问
arr[1] = data[threadIdx.x]; // 不同索引 → 可能无法合并
内存类型对比总结
| 特性 | 寄存器 | 局部内存 | 共享内存 | 全局内存 | 常量内存 |
|---|---|---|---|---|---|
| 位置 | SM内部 | 设备内存 | SM内部 | 设备内存 | 设备内存 |
| 作用域 | 线程 | 线程 | 块 | 网格 | 网格 |
| 生命周期 | 内核 | 内核 | 内核 | 应用程序 | 应用程序 |
| 速度 | 最快 | 慢 | 快 | 慢 | 快(缓存时) |
| 管理 | 编译器 | 编译器 | 程序员 | 程序员 | 程序员 |
| 容量 | 很小 | 大 | 小 | 很大 | 小(但缓存) |
最佳实践总结
-
全局内存:
- 主要数据存储,注意数据竞争
- 合并访问以提高带宽利用率
-
共享内存:
- 用于块内线程协作和频繁访问的数据
- 使用
__syncthreads()确保数据一致性 - 动态分配时注意对齐要求
-
寄存器:
- 编译器自动管理,通常最优
- 必要时通过
-maxrregcount限制使用量 - 避免过度限制导致寄存器溢出
-
局部内存:
- 尽量使数组索引为编译时常量
- 注意合并访问模式
-
内存选择策略:
- 频繁访问的数据 → 共享内存
- 只读数据 → 常量内存
- 线程私有数据 → 寄存器
- 大容量、跨线程共享数据 → 全局内存
核心API总结表
| API | 用途 |
|---|---|
cudaMalloc() |
分配全局内存 |
cudaFree() |
释放全局内存 |
cudaMemcpy() |
主机-设备数据传输 |
cudaGetDeviceProperties() |
获取设备属性(内存大小等) |
cudaFuncSetCacheConfig() |
设置缓存/共享内存偏好 |
__syncthreads() |
块内线程同步 |