变量内存空间指定符表示设备上变量的内存位置。
在设备代码中声明的自动变量,如果未使用本节描述的__device__、__shared__或__constant__内存空间限定符,通常存放在寄存器中。但在某些情况下,编译器可能会选择将其放置在本地内存中,这可能会对性能产生不利影响,具体细节请参阅设备内存访问。
7.2.1. device
__device__ 内存空间说明符用于声明一个驻留在设备上的变量。
最多可以使用接下来三节中定义的其他内存空间说明符中的一个与__device__一起使用,以进一步指明变量所属的内存空间。如果未指定任何说明符,则该变量:
-
驻留在全局内存空间中,
-
其生命周期与创建它的CUDA上下文相同,
-
每个设备都有一个独立的对象,
-
可通过网格内所有线程及主机端的运行时库访问(cudaGetSymbolAddress()/ cudaGetSymbolSize()/ cudaMemcpyToSymbol()/ cudaMemcpyFromSymbol())。
7.2.2. constant
__constant__ 内存空间限定符,可选择与 __device__ 一起使用,用于声明一个变量,该变量:
-
驻留在常量内存空间中,
-
其生命周期与创建它的CUDA上下文相同,
-
每个设备都有一个独立的对象,
-
可通过网格内所有线程及主机端的运行时库访问(cudaGetSymbolAddress()/ cudaGetSymbolSize()/ cudaMemcpyToSymbol()/ cudaMemcpyFromSymbol())。
在存在并发网格访问该常量的情况下,从主机端修改该常量的行为(在该网格生命周期的任何时刻)是未定义的。
7.2.3. shared
__shared__内存空间限定符(可选择与__device__一起使用)用于声明具有以下特性的变量:
-
驻留在线程块的共享内存空间中
-
生命周期与代码块相同,
-
每个块都有一个独立的对象,
-
仅可从块内的所有线程访问,
-
没有固定地址。
当在共享内存中声明一个变量作为外部数组时,例如
extern __shared__ float shared[];
数组的大小在启动时确定(参见执行配置)。以这种方式声明的所有变量在内存中起始地址相同,因此必须通过偏移量显式管理数组中变量的布局。例如,如果想要实现等同于
short array0[128];
float array1[64];
int array2[256];
在动态分配的共享内存中,可以通过以下方式声明和初始化数组:
extern __shared__ float array[];
__device__ void func() // __device__ or __global__ function
{
short* array0 = (short*)array;
float* array1 = (float*)&array0[128];
int* array2 = (int*)&array1[64];
}
请注意,指针需要与其指向的类型对齐,因此例如以下代码将无法工作,因为array1未按4字节对齐。
extern __shared__ float array[];
__device__ void func() // __device__ or __global__ function
{
short* array0 = (short*)array;
float* array1 = (float*)&array0[127];
}
7.2.4. grid_constant
对于计算架构大于或等于7.0的情况,__grid_constant__注解用于标注一个非引用类型的const限定__global__函数参数,该参数满足以下条件:
-
生命周期与网格相同,
-
对网格是私有的,即主机线程和其他网格(包括子网格)的线程无法访问该对象。
-
每个网格拥有独立的对象,即网格中的所有线程都访问相同的地址,
-
是只读的,即修改
__grid_constant__对象或其任何子对象属于未定义行为 ,包括mutable成员。
要求:
-
使用
__grid_constant__标注的内核参数必须具有const限定的非引用类型。 -
所有函数声明必须在任何
__grid_constant_参数方面保持一致。 -
函数模板特化必须与主模板声明在
__grid_constant__参数方面保持一致。 -
函数模板实例化指令必须与主模板声明在
__grid_constant__参数方面保持一致。
如果获取了__global__函数参数的地址,编译器通常会在线程本地内存中创建内核参数的副本,并使用该副本的地址,以部分支持C++语义(允许每个线程修改其自身的函数参数本地副本)。通过使用__grid_constant__注解__global__函数参数,可确保编译器不会在线程本地内存中创建内核参数的副本,而是直接使用参数本身的通用地址。避免本地副本可能带来性能提升。
__device__ void unknown_function(S const&);
__global__ void kernel(const __grid_constant__ S s) {
s.x += threadIdx.x; // Undefined Behavior: tried to modify read-only memory
// Compiler will _not_ create a per-thread thread local copy of "s":
unknown_function(s);
}
7.2.5. managed
__managed__内存空间说明符(可选择与__device__一起使用),用于声明一个具有以下特性的变量:
-
可以从设备和主机代码中引用,例如,可以获取其地址,或者可以直接从设备或主机函数中读取或写入。
-
生命周期与应用程序相同。
7.2.6. restrict
nvcc 通过 __restrict__ 关键字支持受限指针。
C99中引入了受限指针(restricted pointers),旨在缓解C类语言中存在的别名问题,该问题会阻碍从代码重排序到公共子表达式消除等各种优化。
以下是一个存在别名问题的示例,其中使用受限指针可以帮助编译器减少指令数量:
void foo(const float* a,
const float* b,
float* c)
{
c[0] = a[0] * b[0];
c[1] = a[0] * b[0];
c[2] = a[0] * b[0] * a[1];
c[3] = a[0] * a[1];
c[4] = a[0] * b[0];
c[5] = b[0];
...
}
在C类语言中,指针a、b和c可能存在别名关系,因此通过c的任何写入都可能修改a或b的元素。这意味着为了保证功能正确性,编译器不能将a[0]和b[0]加载到寄存器中相乘,然后将结果同时存储到c[0]和c[1],因为如果a[0]实际上与c[0]是同一内存位置,结果将与抽象执行模型不符。因此编译器无法利用这个公共子表达式。同样地,编译器也不能简单地将c[4]的计算重新排序到靠近c[0]和c[1]计算的位置,因为对c[3]的前置写入可能会改变c[4]计算的输入。
通过将a、b和c声明为受限指针,程序员向编译器声明这些指针实际上不存在别名问题,这意味着通过c的写入永远不会覆盖a或b的元素。这将函数原型修改如下:
void foo(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ c);
请注意,所有指针参数都需要设置为受限(restricted),以便编译器优化器能够从中获益。添加__restrict__关键字后,编译器现在可以自由地进行重排序和公共子表达式消除,同时保持与抽象执行模型完全相同的功能:
void foo(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ c)
{
float t0 = a[0];
float t1 = b[0];
float t2 = t0 * t1;
float t3 = a[1];
c[0] = t2;
c[1] = t2;
c[4] = t2;
c[2] = t2 * t3;
c[3] = t0 * t3;
c[5] = t1;
...
}
此处的影响是减少了内存访问次数和计算量。但由于"缓存"加载和公共子表达式导致寄存器压力增加,这之间需要取得平衡。
由于寄存器压力是许多CUDA代码中的关键问题,使用受限指针可能会对CUDA代码产生负面性能影响,因为会降低占用率。