CUDA的内存结构,通过实例展示寄存器和共享内存的使用

本章将介绍CUDA的内存结构,通过实例展示寄存器和共享内存的使用。

CUDA内存结构

GPU的内存结构和CPU类似,但也存在一些区别,GPU的内存中可读写的有:寄存器(registers)、Local memory、共享内存(shared memory)和全局内存(global memory),只读的有:常量内存(constant memory)和纹理内存(texture memory)。

CUDA Toolkit Document给出的的内存结构如下图所示:

CUDA内存结构
CUDA内存结构

每个线程都有独立的寄存器和Local memory,同一个block的所有线程共享一个共享内存,全局内存、常量内存和纹理内存是所有线程都可访问的。全局内存、常量内存和纹理内存对程序的优化有特殊作用。

寄存器

与CPU不同,GPU的每个SM(流多处理器)有成千上万个寄存器,在GPU技术简介中已经提到,SM类似于CPU的核,每个SM拥有多个SP(流处理器),所有的工作都是在SP上处理的,GPU的每个SM可能有8~192个SP,这就意味着,SM可同时运行这些数目的线程。

寄存器是每个线程私有的,并且GPU没有使用寄存器重命名机制,而是致力于为每一个线程都分配真实的寄存器,CUDA上下文切换机制非常高效,几乎是零开销。当然,这些细节对程序员是完全透明的。

和CPU一样,访问寄存器的速度是非常快的,所以应尽量优先使用寄存器。无论是CPU还是GPU,通过寄存器的优化方式都会使程序的执行速度得到很大提高。

举一个例子:

 for (int i = 0; i < size; ++i)
{
      sum += array[i];
}

sum如果存于内存中,则需要做size次读/写内存的操作,而如果把sum设置为局部变量,把最终结果写回内存,编译器会将其放入寄存器中,这样只需1次内存写操作,将大大节约运行时间。

Local memory

Local memory和寄存器类似,也是线程私有的,访问速度比寄存器稍微慢一点。事实上,是由编译器在寄存器全部使用完的时候自动分配的。在优化程序的时候可以考虑减少block的线程数量以使每个线程有更多的寄存器可使用,这样可减少Local memory的使用,从而加快运行速度。

共享内存

共享内存允许同一个block中的线程读写这一段内存,但线程无法看到也无法修改其它block的共享内存。共享内存缓冲区驻留在物理GPU上,所以访问速度也是很快的。事实上,共享内存的速度几乎在所有的GPU中都一致(而全局内存在低端显卡的速度只有高端显卡的1/10),因此,在任何显卡中,除了使用寄存器,还要更有效地使用共享内存。

共享内存的存在就可使运行线程块中的多个线程之间相互通信。共享内存的一个应用场景是线程块中多个线程需要共同操作某一数据。考虑一个矢量点积运算的例子:

(x1, x2, x3, x4 ) * (y1, y2, y3, y4) = x1y1 + x2y2 + x3y3 + x4y4

和矢量加法一样,矢量点积也可以在GPU上并行计算,每个线程将两个相应的元素相乘,然后移到下两个元素,线程每次增加的索引为总线程的数量,下面是实现这一步的代码:

const int N = 33 * 1024;
const int threadsPerBlock = 256;

__global__ void dot( float *a, float *b, float *c )
{
    __shared__ float cache[threadsPerBlock];
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    int cacheIndex = threadIdx.x;

    float   temp = 0;
    while (tid < N)
    {
        temp += a[tid] * b[tid];
        tid += blockDim.x * gridDim.x;
    }
    cache[cacheIndex] = temp;
}

CUDA C使用shared修饰符申明共享内存的变量。在每个线程中分别计算相应元素的乘积之和,并保存在共享内存变量cache对应的索引中,可以看出,如果只有一个block,那么所有线程结束后,对cache求和就是最终结果。当然,实际会有很多个block,所以需要对所有block中的cache求和,由于共享内存在block之间是不能访问的,所以需要在各个block中分部求和,并把部分和保存在数组中,最后在CPU上求和。block中分部求和代码如下:

__global__ void dot( float *a, float *b, float *c ) {
    __shared__ float cache[threadsPerBlock];
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    int cacheIndex = threadIdx.x;

    float   temp = 0;
    while (tid < N) {
        temp += a[tid] * b[tid];
        tid += blockDim.x * gridDim.x;
    }

    cache[cacheIndex] = temp;

    //同步
    __syncthreads();

   //分部求和
    int i = blockDim.x/2;
    while (i != 0) {
        if (cacheIndex < i)
            cache[cacheIndex] += cache[cacheIndex + i];
        __syncthreads();
        i /= 2;
    }

    if (cacheIndex == 0)
        c[blockIdx.x] = cache[0];
}


__syncthreads()是线程同步函数,调用这个函数确保在线程块中所有的线程都执行完__syncthreads()之前的代码,在执行后面的代码,当然,这会损失一定性能。

当执行__syncthreads()之后的代码,我们就能确定cache已经计算好了,下面只需要对cache求和就可以了,最简单的就是用一个for循环计算。但是,这相当只有一个线程在起作用,线程块其它线程都在做无用功,

使用规约运行是一个更好地选择,即每个线程将cache中的两个值相加起来,然后结果保存会cache中,规约的思想如下图所示。

规约算法图示
规约算法图示

按这种方法,每次将会使数据减少一半,只需执行log2(threadsPerBlock)个步骤后,就能得到cache中所有值的总和。

最后使用如下代码将结果保存在c中:

 if (cacheIndex == 0)
        c[blockIdx.x] = cache[0];

这是因为只有一个值需要写入,用一个线程来操作就行了,如果不加if,那么每个线程都将执行一次写内存操作,浪费大量的运行时间。

最后,只需要在CPU上把c中的值求和就得到了最终结果。下面给出完整代码:

#include <stdio.h>

const int N = 33 * 1024;
const int threadsPerBlock = 256;
const int blocksPerGrid = (N + threadsPerBlock -1) / threadsPerBlock;

__global__ void dot( float *a, float *b, float *c )
{
    __shared__ float cache[threadsPerBlock];
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    int cacheIndex = threadIdx.x;

    float   temp = 0;
    while (tid < N)
    {
        temp += a[tid] * b[tid];
        tid += blockDim.x * gridDim.x;
    }

    cache[cacheIndex] = temp;

    //同步
    __syncthreads();

    //规约求和
    int i = blockDim.x/2;
    while (i != 0)
     {
        if (cacheIndex < i)
        {
            cache[cacheIndex] += cache[cacheIndex + i];
        }

        __syncthreads();
        i /= 2;
    }

    if (cacheIndex == 0)
    {
        c[blockIdx.x] = cache[0];
    }
}

int  main(int argc, char const *argv[])
{
    float   *a, *b, *partial_c;
    float   *dev_a, *dev_b, *dev_partial_c;

    a = (float*)malloc( N*sizeof(float) );
    b = (float*)malloc( N*sizeof(float) );
    partial_c = (float*)malloc( blocksPerGrid*sizeof(float));

    cudaMalloc(&dev_a, N*sizeof(float));
    cudaMalloc(&dev_b, N*sizeof(float));
    cudaMalloc(&dev_partial_c, blocksPerGrid*sizeof(float));

    for (int i=0; i < N; ++i)
    {
        a[i] = i;
        b[i] = i * 2;
    }

    cudaMemcpy(dev_a, a, N*sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, b, N*sizeof(float), cudaMemcpyHostToDevice);

    dot<<<blocksPerGrid,threadsPerBlock>>>( dev_a, dev_b, dev_partial_c );

    cudaMemcpy(partial_c, dev_partial_c, blocksPerGrid*sizeof(float), cudaMemcpyDeviceToHost);

    int c = 0;
    for (int i=0; i < blocksPerGrid; ++i)
    {
        c += partial_c[i];
    }

    cudaFree(dev_a);
    cudaFree(dev_b);
    cudaFree(dev_partial_c);

    free(a);
    free(b);
    free(partial_c);

    return 0;
}

常量内存

常量内存,通过它的名字就可以猜到它是只读内存。常量内存其实只是全局内存的一种虚拟地址形式,并没有特殊保留的常量内存块。内存的大小为64KB。常量内存可以在编译时申明为常量内存,使用修饰符constant申明,也可以在运行时通过主机端定义为只读内存。常量只是从GPU内存的角度而言的,CPU在运行时可以通过调用cudaCopyToSymbol来改变常量内存中的内容。

全局内存

GPU的全局内存之所以是全局内存,主要是因为GPU与CPU都可以对它进行写操作,任何设备都可以通过PCI-E总线对其进行访问。在多GPU系统同,GPU之间可以不通过CPU直接将数据从一块GPU卡传输到另一块GPU卡上。在调用核函数之前,使用cudaMemcpy函数就是把CPU上的数据传输到GPU的全局内存上。

纹理内存

和常量内存一样,纹理内存也是一种只读内存,在特定的访问模式中,纹理内存能够提升程序的性能并减少内存流量。纹理内存最初是为图形处理程序而设计,不过同样也可以用于通用计算。由于纹理内存的使用非常特殊,有时使用纹理内存是费力不讨好的事情。因此,对于纹理内存,只有在应用程序真正需要的时候才对其进行了解。主要应该掌握全局内存、共享内存和寄存器的使用。

参考文献
•库克. CUDA并行程序设计. 机械工业出版社, 2014.
•桑德斯. GPU高性能编程CUDA实战. 机械工业出版社, 2011.
•CUDA C Programming Guide
•CUDA Toolkit Documentation
•R. Couturier, Ed., Designing Scientific Applications on GPUs, CRC Press, 2013.

本文地址 http://blog.5long.me/2015/2015-11-1-cuda-parallel-programming-4/

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CUDA共享内存是一种位于GPU上的高速存,可以用于在同一个线程内的线程之间共享数据。使用内存可以显著提内存访问的效,并且减少对全局内存访问次数。 以下使用CUDA共享内存的一般步: 1. 声明共内存:在GPU核函数中,可以使用`__shared__`关键字来声明共享内存共享内存的大小需要在编译时确定,并且是所有线程中的线程共享的。 ```cuda __shared__ float sharedData[SIZE]; ``` 2. 将数据从全局内存复制到共享内存:在GPU核函数中,使用线程中的线程来将数据从全局内存复制到共享内存中。可以使用线程索引和线程索引来确定数据的位置。 ```cuda int tid = threadIdx.x; int blockId = blockIdx.x; int index = blockId * blockDim.x + tid; sharedData[tid] = globalData[index]; ``` 3. 同步线程:在将数据复制到共享内存后,需要使用`__syncthreads()`函数来同步线程中的线程。这样可以确保所有线程都已经将数据复制到共享内存中。 ```cuda __syncthreads(); ``` 4. 使用共享内存:一旦所有线程都已经将数据复制到共享内存中,可以使用共享内存进行计算。由于共享内存位于GPU的高速缓存中,所以访问速度较快。 ```cuda sharedData[tid] += 1.0f; ``` 5. 将数据从共享内存复制回全局内存:在计算完成后,可以使用线程中的线程将数据从共享内存复制回全局内存。 ```cuda globalData[index] = sharedData[tid]; ``` 需要注意的是,共享内存的大小是有限的,不同的GPU架构及型号都有不同的限制。因此,在使用共享内存时,需要确保不超过设备的限制,并且合理地利用共享内存,以提高性能。此外,需要注意线程同步的位置和使用方法,以避免数据竞争和错误的结果。 以上是使用CUDA共享内存的基本步骤,具体的实现方式会根据具体问题而有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值