cuda 中的_restrict_ 作用详解
在了解受限指针之前,先看C/C++中的受限指针内容
C/C++ 中的restrict关键字
restrict关键字用于限定和约束指针,表示这个指针只访问这块内存的唯一方式。也就是告诉编译器,这块内存的内容操作都只会通过这个指针,而不存在其他进行修改操作的途径;帮助编译器更好的进行代码优化。
也就是告诉编译器,该指针指向的数据是不变的,可以存储到寄存器或者别的缓存中。
另外也是告诉程序员,指向的这段内存需要满足restrict规则。
比如函数:
void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
void * memmove(void * s1, const void * s2, size_t n);
两个函数都是从s2 复制n字节数据到s1 指向位置,二者直接差别是memcpy
的函数参数被restrict
关键字修饰。假定两块区域没有别的重叠。需要使用者来保证
void test(){
char *ptr = (char*)malloc(15 * sizeof(char));
char *tmp = ptr + 3;
memset(ptr, '\0', 15);
snprintf(ptr, 12*sizeof(char), "%s\n", "HelloWorld");
printf("ptr: %s\n", ptr);
printf("tmp: %s\n", tmp);
printf("memcpying\n");
memcpy(tmp, ptr, 5);
printf("ptr: %s\n", ptr);
printf("tmp: %s\n", tmp);
}
cuda 中的 _restrict_
nvcc 通过 _restrict_ 关键字支持受限指针。
简而言之, _restrict_ 就像是一份由程序员和编译器签订的协议,大致内容是:“程序员仅仅只会使用这个指针来访问这部分内存数据”。 从编译器的角度看,一个重要的事是指针别名,指针别名会阻碍编译器做各种各样的优化。
所以,_restrict_ 对于编译器优化的目的来说是非常有用的。
对于计算能力大于3.5的设备,这些设备还有一份区分于L1 cache的独立内存称之为只读内存(read only cache, 通过 __ldg,...
, 等函数记载)
如果您同时使用 restrict 和 const 来修饰传递给内核的全局指针,那么在为 cc3.5 及更高版本的设备生成代码时,这也向编译器发出了强烈的提示,使这些全局内存负载流经只读缓存。这可以提供应用程序性能优势,通常只需很少的其他代码重构。这并不能保证只读缓存的使用,并且如果编译器能够满足必要条件,即使您不使用这些修饰器,它也会经常尝试积极使用只读缓存。
和__constant__的区别:
__constant__
对所有GPU设备都可用,read-only cache仅对cc3.5 以上设备可用- 使用
__constant__
分配内存限制最大不超过64KB, 只读缓存没有这样限制。 并且不应该把__restrict__
放在内存分配的语句中,一般只用来修饰指针。 - read-only 中的数据具有典型的全局内存访问考虑因素。通常希望通过只读缓存进行相邻和连续的访问,以便更好的合并全局内存读取。另一方面,
__constatnt__
机制需要所谓的统一访问来获得最快的性能。。 统一访问本质上意味着warp中每个线程都从相同的位置/地址/索引请求数据。
官方文档里面也有说明
比如下面的例子中:
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的部分内存。那样通过写入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标记为restrict指针,程序员告诉编译器,这些指针实际上没有别名。也就是说通过c不可能修改a,b的元素。
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代码中的一个关键问题,使用受限指针可能会因warp占用率降低从而导致cuda代码带来负面影响。
(官方文档这么写,猜测因为寄存器压力增加,可能导致cuda并行能力降低。因为同时执行的寄存器数量是有限的,如果每个块占用的寄存器数量过多,便会导致同一时间执行的线程块数量减少,也就影响到并行能力了)
二者需要使用者进行平衡。