CUDA:寄存器详解

CUDA:寄存器详解

前言 1

与CPU不同,GPU的每个SM(流多处理器)有上千个寄存器。CPU与GPU架构的一个主要区别就是CPU与GPU映射寄存器的方式。CPU通过使用寄存器重命名和栈来执行多线程。为了运行一个新任务,CPU需要进行上下文切换,将当前所有寄存器的状态保存到栈上,然后从栈中恢复当前需要执行的新线程上次的执行状态。这些操作需要花费上百个CPU时钟周期。如果在CPU上开启过多的线程,时间将主要花费在上下文的切换上。因此,如果在CPU上开启过多的线程,有效工作的吞吐量将会快速降低。

然而GPU却恰恰相反。GPU利用多线程隐藏了内存获取与指令执行带来的延迟。GPU不使用寄存器重命名机制,而是致力于为每一个线程都分配一个真实的寄存器(毕竟每个SM有上千个)。因此,当需要上下文切换时,所需要的操作就是将指向当前寄存器组的选择器或指针更新,以指向下一个执行的线程束的寄存器组,开销几乎为零。从而可以看出,在GPU上开启过少的线程反而会因为等待内存事务使GPU处于闲置状态。(这里用到的线程束概念,即同时调度的一组线程,包含32个线程)。

每个SM能够调度若干个线程块。在SM层,线程块即若干个线程束的逻辑组。编译时会计算出每个内核线程需要的寄存器数目。所有的线程块都具有相同的大小,并拥有已知的数目的线程,每个线程块需要的寄存器数目也就是已知的和固定的。如果一个内核函数中的每个线程需要的寄存器过多,在每个SM中GPU能调度的线程块的数量就会受到限制,因此总的执行的线程数量也会受到限制。此时,开启的线程数量过少会造成硬件无法被充分利用,性能下降。
举例来说,如果一个应用程序先前使用了4个线程块,现在改用更多的寄存器,可能导致只有3个线程块可供调度,这样GPU的吞吐量将会降低1/4。

和CPU一样,访问寄存器的速度是非常快的,所以应尽量优先使用寄存器。无论是CPU还是GPU,通过寄存器的优化方式都会使程序的执行速度得到很大提高。
举一个例子:
for (int i = 0; i < size; ++i)
{
sum += array[i];
}
如果变量sum存于全局内存中,则需要32次读/写内存操作。但如果将变量sum设置为局部变量,编译器会将其放入寄存器中,在寄存器中而不是在主存中做操作,然后将结果协会主内存中,因此可以节省31次全局内存读/写操作。

本地内存和与寄存器

说到寄存器就不得不提本地内存。
本地内存(Local memory)
Local memory和寄存器类似,也是线程私有的,是片外内存,其读写的代价和全局变量一样高,它是由编译器在寄存器全部使用完的时候自动分配的。在优化程序的时候可以考虑减少block的线程数量以使每个线程有更多的寄存器可使用,这样可减少Local memory的使用,从而加快运行速度。2

本地内存是如此命名的,因为它的范围对于线程来说是本地的,而不是因为它的物理位置。 事实上,本地内存是片外的。 因此,访问本地内存与访问全局内存一样昂贵。 换句话说,名称中的术语“本地”并不意味着访问速度更快。
本地内存仅用于保存自动变量。这是由nvcc编译器在确定没有足够的寄存器空间来保存变量时完成的。可能放置在本地内存中的自动变量是大型结构或数组,会占用太多的寄存器空间,而编译器确定的数组可能会动态编制索引。3

寄存器和本地内存的关系

  1. 都存储静态变量,都是在编译阶段确定大小的。
    静态变量在编译时即知道其大小,一个块4内的寄存器和本地内存大小是在编译时就被识别和计算的,在编译阶段,编译器就已经决定了块是怎么在SM上进行分配的。
  2. 寄存器和本地内存都是隐式声明。
    共享内存、设备内存、常量内存和纹理内存,它们声明变量的方式:
    其声明:

    • __shared__ a
    • __device__ a
    • __constant__ a
    • texture<Type, Dim, ReadMode> texRef;

寄存器和本地内存不像共享内存、设备内存、常量内存和纹理内存显式声明,它们的声明不加任何限定符,就如普通变量一样。如 int a
3. 寄存器和本地内存共同定义了核函数中所有静态变量
寄存器主要承担Scalar variables(标量变量)5和编译时已知的静态索引的数组6
本地内存主要承担静态的大型数组和结构体。本地内存存储于栈中(stack frame)。
4. 寄存器影响占用率,本地内存影响内存访问速度。
寄存器是SM中是稀有资源,它影响线程块在SM上的分配,即影响占用率。本地内存不影响占用率。寄存器片上资源,访问速度极快,本地内存片外资源,访问速度很慢。

寄存器和本地内存的权衡

影响核函数速度的关键之一就是寄存器和本地内存的权衡(合理分配)。寄存器和本地内存基本共同定义了核函数中所有局部或中间变量,且它们的分配对核函数的性能是有影响的(寄存器影响占用率,本地内存影响内存访问速度),不幸的是它们对性能的影响是矛盾的。
寄存器和本地内存的矛盾:
有时,较少的使用寄存器可获得较高的占用率即SM的中线程的利用率较高,对程序性能提升,但局部变量和中间变量的数量是一定,较少的寄存器意味着较多的使用本地内存,本地内存的内存访问延迟很高,对程序性下降。所以,寄存器和本地内存的大小分配是相矛盾的,也就是说,高的占用率不一定有更好的性能。

权衡寄存器和本地内存的使用
由于寄存器和本地内存的在性能上的矛盾,没有确定的普适性的办法,只有根据实际情况具体分析,所遵循的原则是:在占用率不是太低(50%以上),的情况下尽量多使用片上的寄存器资源,但当中间数据比较多时,使用本地内存。
即:
当局部变量或中间数据不多时,尽量使用片上的寄存器资源。
当局部变量或中间数据较多时,仍使用寄存器资源,但要使用一定的策略:可通过减小块的大小的方式来增加每块的寄存器资源,或使用片上的共享内存作为中间数据的临时储存区域,减小寄存器的使用。
当局部变量或中间数据非常大时,这时需要较多的利用寄存器资源,但迫不得已的使用本地内存。

寄存器资源的大小和限制

https://blog.csdn.net/fb_help/article/details/80378503 -’寄存器资源的限制‘

怎样使用寄存器资源和本地资源。


  1. https://blog.csdn.net/u010335328/article/details/52649231.
  2. https://blog.csdn.net/nyg8945/article/details/52874225.
  3. Local memory
    Local memory is so named because its scope is local to the thread, not because of its physical location. In fact, local memory is off-chip. Hence, access to local memory is as expensive as access to global memory. In other words, the term local in the name does not imply faster access.
    Local memory is used only to hold automatic variables. This is done by the nvcc compiler when it determines that there is insufficient register space to hold the variable. Automatic variables that are likely to be placed in local memory are large structures or arrays that would consume too much register space and arrays that the compiler determines may be indexed dynamically.
    [from https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html]
  4. 为什么以块为单位分配寄存器和本地内存?
    因为,在SM中分配线程或warp时,为保证共享内存可以在块内通信,共享内存要求每个块必须在一个SM中,所以,为保证同一块内的线程在同一个SM中,SM在分配或承载线程或warp时是以块为最基本单元的。
  5. https://stackoverflow.com/questions/12167926/forcing-cuda-to-use-register-for-a-variable.
  6. https://blog.csdn.net/bruce_0712/article/details/65664840

猜你喜欢

转载自blog.csdn.net/fb_help/article/details/80389222
今日推荐