CUDA内存访问优化详解
CUDA内存访问优化详解
CUDA内存访问优化是提升GPU计算性能的关键技术之一。本文详细介绍了GPU内存访问模式、对齐与合并访存、AoS与SoA数据结构选择以及性能调优策略,对于从事GPU编程和并行计算的开发者具有重要的参考价值。
一、内存访问模式
GPU对片外DRAM的访问延迟大,带宽低,导致其成为很多应用的性能瓶颈。因此对DRAM访问的进一步优化可以有效改善程序性能。优化之前,首先看一下GPU对内存的访问模式
如上图所示,DRAM内存的读写在物理上是从 片外DRAM -> 片上Cache -> 寄存器。 其中,片外DRAM到片上Cache是主要性能瓶颈。DRAM 到 Cache 之间的一次传输(transaction)设计为32、64或者128字节,并且内存地址按照32、64或者128字节的间隔对齐
注意:后文中 transaction 特指设备内存(DRAM)到片内存储(Cache)的传输
以读数据为例,DRAM 数据首先进入 L2 Cache,之后根据 GPU 架构的不同,有些会继续传输到 L1 Cache,最后进入线程寄存器。L2 cache 是所有 SM 共有,而 L1 cache 是 SM 私有
若使用了 L1+L2 Cache,那么执行一次 DRAM 到 Cache 之间的传输(transaction),是 128 字节。若仅使用 L2 Cache,那么就是 32 字节
是否启用L1 Cache取决于GPU架构与编译选项
关闭L1 cache
-Xptxas -dlcm=cg
打开L1 cache
-Xptxas -dlcm=ca
由于采用 SIMT 的架构,GPU 对内存的访问指令是由 warp 发起的,即 warp 中每个线程同时执行内存操作指令,不过每个线程所访问的数据地址可以不同,GPU 会根据这些不同的地址发起一次或多次 DRAM -> Cache 的传输(transaction),直到所有线程都拿到各自所需的数据(Cache->Registers)。显然,可以通过减少DRAM->Cache的transaction次数来优化程序性能
warp 执行内存指令时会有很长的延迟,此时 warp 进入 Stalled 状态,warp 调度器调度其他 eligible warp 执行
内存指令延迟的原因:
访问设备内存本身存在大的延迟
SIMT 的执行模型,意味着只有当 warp 内所有32个线程都得到了数据后,才会从 Stalled 态转为 Eligible 态
最好的情况下,GPU发起一次 DRAM -> Cache 的transaction就把 warp 中所有线程所需的数据全部获取到 Cache,最坏的情况下,则需要 32 次 transaction
Read-Only cache
Read-Only cache原本用于纹理内存的缓存。GPU 3.5以上的版本可以使用该缓存替代L1,作为Global内存的缓存。此cache采用32字节对齐间隔,因此比原128字节的缓冲区更适合非对齐非合并的情况
二、对齐与合并访存
优化全局内存的访问性能,需要考虑下面两个方面,
内存对齐访问(Aligned memory access):DRAM -> Cache transaction,首地址是32或128字节的倍数
内存合并访问(Coalesced memory access): Warp 内线程访问连续内存块
理想情况
Warp 内每个线程需要 4 个字节的数据,且总共 32 * 4 = 128 个字节是连续的,起始地址为128。此时只需一次 128 字节的 memory transactioin (DRAM -> Cache)
非对齐、非合并的情况
这种情况下需要 3 次 128字节的 memory transaction,一个从内存地址 0 -> 127,为标记为 1 的线程取值,一个从 128 -> 255,为标记 2 的线程取值,一个从 256 -> 383,为标记 3 的线程取值
2.1 Global Memory Read
根据是否使用L1 cache,可分为两种情况讨论,
Cached load(使用 L1 cache)
Uncached load(不使用 L1 cache)
2.1.1 Cached load
使用 L1 cache,memory transaction 的以 L1 cache line 的大小128字节为间隔访存
合并对齐访问
warp 内线程访问的地址在 128~256 之间,每个线程需要 4 个字节,所有线程所需内存地址按照线程 ID 连续排列。只需执行一个 128 字节的 transaction 即可满足所有线程的需求。此时总线利用率为 100%,即在这次 transaction 中,内存带宽得以充分使用,没有多余的数据
warp 的线程访问的数据在 128~256 之间,每个线程需要 4 个字节,但是内存地址没有按照线程 ID 排序。与之前的例子相同,一次 128 字节的 transaction 即可满足要求,且总线利用率为 100%
合并、不对齐
上图 warp 需要连续的 128 字节,但是内存的首地址没有 128 字节对齐。此时需要两次 128 字节的 transaction,总线利用率为 50% (总共读了 256 个字节,实际使用 128 字节)
Warp内线程访问同一4字节数据
warp 内所有线程访问了相同 4 个字节,需要一次 128 字节的 transaction,总线利用率 4 / 128 = 3.125%
最坏情况
同样是请求32个4 bytes数据,但是每个地址分布的相当不规律,我们只想要需要的那128 bytes数据,但是,实际上下图这样的分布,却需要N∈(0,32)个cache line,也就是N次数据传输消耗
CPU的L1 cache是根据时间和空间局部性做出的优化,但是GPU的L1仅仅被设计成针对空间局部性而不包括时间局部性。频繁获取L1不会导致某些数据驻留在cache中,只要下次用不到,直接删
2.1.2 Uncached load
若不使用 L1 cache,一次内存传输由 1、2或4 个 segments 完成,每个 segment 为 32 字节, 并且按照 32 字节对齐。显然这种情况下数据传输得到了更精细的划分(最小 32 字节),这会带来更高效的非对齐,非合并的内存访问
理想情况
对齐合并的内存访问,只需一个 4 segment 的 transaction,总线利用率 100%
非对齐、合并
上图warp需要连续的128字节数据,但是首地址并未和32字节对齐,此时128字节的数据最多分布在5个segment内,因此总线利用率至少为80%。显然要比使用L1 cached的时候更好
Warp内线程访问同一4字节数据
此时的总线利用率为4/32 = 12.5%,也要比使用L1 cache时(3.125%)要好
最坏情况
32个线程所需的数据散落在至多32个segment里。散落在32个32字节的segment里显然要比散落在32个128字节的情况要好
2.2 Global Memory Write
内存写操作情况要简单的多,Fermi/Kelper 的 L1 cache 并不支持写操作。内存写仅通过 L2 cache 写入设备内存。与 Uncached Load 类似,transaction分为1, 2, 4个segment,每个segment32字节
理想情况
warp写入连续的128个字节,仅需一个4-segments的transaction
对齐,不合并
需要3个1-segment的transaction
对齐且地址在一个连续的64-byte范围内的情况
需要一个2-segment的transaction
三、AoS与SoA
array of structures(AoS,结构阵列)和structure of arrays(SoA,数组结构)
//Array of structures (AoS)
struct innerStruct {
float x;
float y;
};
struct innerStruct myAoS[N];
//Structure of array
struct innerArray {
float x[N];
float y[N];
};
struct innerArray moa;
上图为AoS,SoA的内存结构。采用AoS的形式,Warp 线程访问 x 时,会将 y 的值也载入 cache。浪费了50%的带宽。而 SoA 就不存在这个问题。因此推荐采用 SoA 的形式组织数据
四、性能调优
化设备内存带宽利用率有两个目标:
对齐及合并内存访问,以减少带宽的浪费
足够的并发内存操作,以隐藏内存延迟
影响设备内存操作性能的因素主要有两个:
有效利用设备DRAM和SM片上内存之间的字节移动:为了避免设备内存带宽的浪费,内存访问模式应该是对齐和合并的
当前的并发内存操作数
可通过以下两点实现最大化当前存储器操作数
展开,每个线程产生更多的独立内存访问
修改核函数启动的执行配置来使每个SM有更多的并行性