cuda 中的__restrict__ 作用详解

cuda 中的_restrict_ 作用详解

在了解受限指针之前,先看C/C++中的受限指针内容

C/C++ 中的restrict关键字

参考: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并行能力降低。因为同时执行的寄存器数量是有限的,如果每个块占用的寄存器数量过多,便会导致同一时间执行的线程块数量减少,也就影响到并行能力了)

二者需要使用者进行平衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃肉夹馍不要夹馍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值