CUDA Kernel中的Load/Store指令对L1/L2缓存的影响
CUDA Kernel中的Load/Store指令对L1/L2缓存的影响
硬件层面分析
在CUDA架构中,内存访问模式对性能有重大影响。NVIDIA GPU的存储层次结构包括:
- L1缓存:每个SM(流式多处理器)独享
- L2缓存:所有SM共享
- 全局内存:设备内存
关键的load/store指令及其缓存行为:
1. 常规加载/存储(默认行为)
cpp
float val = array[index]; // 加载
array[index] = val; // 存储
- 默认情况下,加载会尝试使用L1和L2缓存
- 存储默认绕过L1缓存,只使用L2缓存(写分配策略)
2. 使用修饰符的加载/存储
__ldg()
:强制通过纹理缓存(只读缓存)加载.cs
修饰符:强制通过L1缓存.cg
修饰符:绕过L1缓存,只使用L2缓存.ca
修饰符:强制缓存(L1和L2).cv
修饰符: volatile访问,绕过缓存
软件层面性能影响
- 缓存命中率:良好的空间局部性可以提高L1/L2命中率
- 带宽利用率:合并内存访问可提高带宽利用率
- bank冲突:共享内存中的bank冲突会降低性能
- 缓存行填充:不合理的访问模式会导致缓存行利用率低下
示例代码
cpp
__global__ void cacheAwareKernel(float* input, float* output, int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x >= width || y >= height) return;
// 常规加载 - 使用L1/L2缓存
float val = input[y * width + x];
// 使用__ldg()强制通过纹理缓存加载(只读)
float val2 = __ldg(&input[y * width + x]);
// 使用修饰符的加载
float val3;
asm volatile("ld.global.ca.f32 %0, [%1];" : "=f"(val3) : "l"(&input[y * width + x]));
// 常规存储 - 默认绕过L1,只使用L2
output[y * width + x] = val;
// 使用修饰符的存储 - 强制使用L1缓存
asm volatile("st.global.cs.f32 [%0], %1;" :: "l"(&output[y * width + x]), "f"(val2));
// 绕过缓存的存储(直接写入内存)
asm volatile("st.global.cg.f32 [%0], %1;" :: "l"(&output[y * width + x]), "f"(val3));
}
性能优化建议
-
合并内存访问:确保连续的线程访问连续的内存地址
-
合理使用共享内存:用于频繁重用的数据
-
选择适当的缓存策略:
- 对于只读数据,使用
__ldg()
或纹理内存 - 对于写入后很快再次读取的数据,考虑强制使用L1缓存
- 对于只写一次的数据,可以考虑绕过L1缓存
- 对于只读数据,使用
-
调整缓存配置 :可以使用
cudaDeviceSetCacheConfig()
调整L1/共享内存的比例
理解这些缓存行为可以帮助开发者编写更高效的CUDA内核,特别是在内存访问成为瓶颈的情况下。