欢迎您访问第A百科网

CUDA加速——共享内存介绍及其应用

895次浏览     发布时间:2024-03-19 12:48:51     编辑: 萌萌哒程序猴


CUDA的存储器可以大致分为两类:

  • 板载显存(On-board memory)
  • 片上内存(On-chip memory)

其中板载显存主要包括全局内存(global memory)、本地内存(local memory)、常量内存(constant memory)、纹理内存(texture memory)等,片上内存主要包括寄存器(register)和共享内存(shared memory)。不同类型的内存有各自不同的特点,不过片上内存通常比板载显存要快,而寄存器又是所有存储种类中最快的。本文我们着重介绍共享内存的基础知识以及应用例子。



01

查看自己显卡上的共享内存信息

CUDA提供了cudaGetDeviceCount和cudaGetDeviceProperties这两个函数,分别用于获取CUDA设备数、获取CUDA设备属性,通过调用这两个函数,可以方便获取共享内存信息和其它CUDA设备信息:

//显示CUDA设备信息
void show_GPU_info(void)
{
    int deviceCount;
    //获取CUDA设备总数
    cudaGetDeviceCount(&deviceCount);
    //分别获取每个CUDA设备的信息
    for(int i=0;i<deviceCount;i++)
    {
        //定义存储信息的结构体
        cudaDeviceProp devProp;
        //将第i个CUDA设备的信息写入结构体中
        cudaGetDeviceProperties(&devProp, i);
        std::cout << "使用GPU device " << i << ": " << devProp.name << std::endl;
        std::cout << "设备全局内存总量:" << devProp.totalGlobalMem / 1024 / 1024 << "MB" << std::endl;
        std::cout << "SM的数量:" << devProp.multiProcessorCount << std::endl;
        std::cout << "每个线程块的共享内存大小:" << devProp.sharedMemPerBlock / 1024.0 << " KB" << std::endl;
        std::cout << "每个线程块的最大线程数:" << devProp.maxThreadsPerBlock << std::endl;
        std::cout << "设备上一个线程块(Block)中可用的32位寄存器数量: " << devProp.regsPerBlock << std::endl;
        std::cout << "每个EM的最大线程数:" << devProp.maxThreadsPerMultiProcessor << std::endl;
        std::cout << "每个EM的最大线程束数:" << devProp.maxThreadsPerMultiProcessor / 32 << std::endl;
        std::cout << "设备上多处理器的数量:" << devProp.multiProcessorCount << std::endl;
        std::cout << "======================================================" << std::endl;     
        
    }
}


运行以上函数,得到共享内存信息以及其它设备信息,如下图所示,本人使用的显卡上,针对于每一个线程块,其可以使用的最大共享内存为48 KB。



02

共享内存的特性

共享内存的主要特点在于“共享”,也即同一个线程块中的所有线程都可以对这一块存储进行读写操作,所以“共享”是针对同一个线程块中所有线程而言的。一旦共享内存被定义并指定大小,系统将给所有线程块都分配相同大小的共享内存,比如定义一个大小为8 bytes的unsigned char型共享内存,那么所有线程块都会被分配一个8 bytes的unsigned char型共享内存。如下图所示:

共享内存在CUDA核函数中定义,通常有两种方式:静态方式、动态方式。

  • 静态方式定义。这种方式定义的特点是定义的同时指定大小
__global__ shared_memory_kernel(uchar *inputs, int row, int col)
{
     int x = threadIdx.x + blockDim.x * blockIdx.x;  //col
     int y = threadIdx.y + blockDim.y * blockIdx.y;  //row


     if (x < col && y < row)
     {
          __shared__ uchar s[8];  //定义的同时指定大小为8 bytes,因此每个线程块都被分配8 bytes的共享内存
          .
          .
          .
     }
}


  • 动态方式定义。此方式特点为定义的时候不指定大小,在调用核函数的时候将共享内存大小以输入参数的形式传入
__global__ shared_memory_kernel(uchar *inputs, int row, int col)
{
     int x = threadIdx.x + blockDim.x * blockIdx.x;  //col
     int y = threadIdx.y + blockDim.y * blockIdx.y;  //row


     if (x < col && y < row)
     {
          extern __shared__ uchar s[];  //定义的时候不指定大小
          .
          .
          .
     }
}


void shared_memory_test(void)
{
     .
     .
     .
     //传入的第1个参数block_num为线程块总数
     //第2个参数thread_num为每个线程块包含的线程数
     //第3个参数8为共享内存大小,所以动态共享内存大小通过第3个参数传入
     shared_memory_kernel<<<block_num, thread_num, 8>>>(inputs, row, col);
     .
     .
     .
}

需要注意:动态定义共享内存时,调用核函数传入的数值必须以字节byte为单位,所以如果定义的共享内存不是byte类型,数值必须乘以类型占用的字节数。比如要动态定义长度为8的float类型共享内存,那么传入核函数的数值为8*sizeof(float)。

shared_memory_kernel<<<block_num, thread_num, 8 * sizeof(float)>>>(inputs, row, col);



03

共享内存的应用例子

前文我们讲的数组元素规约求和算法,使用CUDA全局内存来存储数据:

CUDA加速——基于规约思想的数组元素求和

我们知道全局内存属于板载显存,而共享内存属于片上内存,因此共享内存的读写速度比全局内存快得多。在前文代码的核函数中有个for循环需要多次读写全局内存,全局内存本身就很慢,而且如果不是连续访问会更慢,因此本文我们尝试使用共享内存来代替全局内存实现前文讲的规约求和算法。

由于前文的规约算法是在不同线程块分别进行的,而共享内存又具有线程块内共享的特性,故共享内存正好适合此应用场景。

前文的规约结构

本文使用共享内存的规约结构

下面我们比较使用共享内存的核函数与前文使用全局内存的核函数:

//使用全局内存
__global__ void cal_sum_ker0(float *Para, float *blocksum_cuda)
{
    int tid = blockIdx.x * blockDim.x + threadIdx.x;


  if(tid < N)
  {
    for (int index = 1; index < blockDim.x; index = (index*2))
    {
      if (threadIdx.x % (index*2) == 0) 
      { 
        Para[tid] += Para[tid + index]; 
      } 


      __syncthreads();  //同步,以防止归约过程中某个线程运行速度过快导致计算错误
    } 


    if(threadIdx.x == 0)   //整个数组相加完成后,将共享内存数组0号元素的值赋给全局内存数组0号元素 
      blocksum_cuda[blockIdx.x] = Para[tid]; 
  }
}




//使用共享内存
//blockIdx.x为线程块的ID号
//blockDim.x每个线程块中包含的线程总个数
//threadIdx.x为每个线程块中的线程ID号
__global__ void cal_sum_ker(float *Para, float *blocksum_cuda)
{
  int tid = blockIdx.x * blockDim.x + threadIdx.x;


  if(tid < N)
  {
    //动态方式定义float型共享内存
    extern __shared__ float s_Para[];     
    
    //线程块中的每个线程负责把其对应的数据从全局内存加载到共享内存
    s_Para[threadIdx.x] = Para[tid];  
    __syncthreads();    //块内线程同步,等待线程块内所有线程加载数据完毕            


    for (int index = 1; index < blockDim.x; index = (index*2))
    {
      if (threadIdx.x % (index*2) == 0) 
      { 
        //在for循环中使用共享内存实现规约,避免频繁读写全局内存
        s_Para[threadIdx.x] += s_Para[threadIdx.x + index]; 
      } 


      __syncthreads();  //块内线程同步,以防止归约过程中某个线程运行速度过快导致计算错误
    } 


    if(threadIdx.x == 0)   //将共享内存数组0号元素的值赋给全局内存数组
      blocksum_cuda[blockIdx.x] = s_Para[threadIdx.x]; 
  }
}




接着在测试代码中分别调用上方两个核函数。调用时指定共享内存的长度为每个线程块包含的线程数:

void cal_sum_test() 
{ 
  Timer_Us2 timer;
  //定义CPU端数组
  float *test_d = (float *)malloc(N * sizeof(float));
  for (long long i = 0; i < N; i++)
  { 
    test_d[i] = 0.5;  
  } 


  dim3 sumblock(512);//设置每个线程块有512个线程
  dim3 sumgrid(((N%sumblock.x) ? (N/sumblock.x + 1) : (N/sumblock.x)));


  float *test_d_cuda;
  float *blocksum_cuda;
  float *blocksum_host = (float *)malloc(sizeof(float) * sumgrid.x);
  cudaMalloc((void **)&test_d_cuda, sizeof(float) * N);
  cudaMalloc((void **)&blocksum_cuda, sizeof(float) * sumgrid.x);


  timer.start_timer();
  //将数据从CPU端拷贝到GPU端
  cudaMemcpy(test_d_cuda, test_d, sizeof(float) * N, cudaMemcpyHostToDevice);
  //调用使用全局内存规约的核函数
  cal_sum_ker0 << < sumgrid, sumblock>> > (test_d_cuda, blocksum_cuda); 
  //将所有线程块的规约结果从GPU端拷贝到CPU端
  cudaMemcpy(blocksum_host, blocksum_cuda, sizeof(float) * sumgrid.x, cudaMemcpyDeviceToHost);
  
  //在CPU端对所有线程块的规约求和结果做串行求和
  double sum = 0.0;
  for(int i = 0; i < sumgrid.x; i++)
  {
    sum += blocksum_host[i];
  }


  timer.stop_timer("GPU time (global memory):");


  cout << " GPU result (global memory) = " << sum << endl;   //显示GPU端结果


  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


  timer.start_timer();


  cudaMemcpy(test_d_cuda, test_d, sizeof(float) * N, cudaMemcpyHostToDevice);
  //调用使用共享内存规约的核函数,sumblock.x为每个线程块包含的线程数,sumblock.x * sizeof(float)就是传入的共享内存字节数
  cal_sum_ker << < sumgrid, sumblock, sumblock.x * sizeof(float) >> > (test_d_cuda, blocksum_cuda); 


  cudaMemcpy(blocksum_host, blocksum_cuda, sizeof(float) * sumgrid.x, cudaMemcpyDeviceToHost);
  
  sum = 0.0;
  for(int i = 0; i < sumgrid.x; i++)
  {
    sum += blocksum_host[i];
  }


  timer.stop_timer("GPU time (shared memory):");


  cout << " GPU result (shared memory) = " << sum << endl;   //显示GPU端结果
  
  cudaFree(test_d_cuda);
  cudaFree(blocksum_cuda);
  free(blocksum_host);
  free(test_d);
} 


运行结果如下,可以看到使用共享内存之后,耗时减少了,这是因为共享内存的读写效率比全局内存高。

相关文章

银行卡状态异常是什么意思(银行卡长期没用转账取钱时,反馈状态异常是为什么?)

前几天有个小伙伴着急地问我,有个客户给他转了几万块钱,指定要转到某个银行卡,但这张卡很长时间没用了,到账后,想把钱转出来,银行反馈此卡状态异常,不可用,该怎么办?回答这个问题前,咱们先来看看,有哪些原因会导致银行反馈此卡状态‬异常‬持卡人连续三次或‬三次以上‬输错银行卡密码,银行卡会被冻结;司法冻结

2024-11-15 10:45

win7怎么进入bios设置教程(如何正确进入BIOS设置)

BIOS,即Basic Input/Output System,是计算机启动过程中不可或缺的重要程序。它负责初始化硬件设备、设置系统参数以及为操作系统的加载提供必要的支持。正确进入BIOS设置是计算机维护和管理的重要步骤,也是解决硬件问题、优化系统性能和实现个性化设置的关键环节。本文将详细介绍进入B

2024-11-15 10:31

自然之道主要内容(中国哲学基础概念:自然之道、理从欲见)

自然之道 明清之际朱之瑜提出的命题。他反对理学家“道在器先”的玄虚之“道”,提出“道无空名”,“道”是明明白白、平平常常的“现前道理”(《答安东守约书》)。认为“道”既是自然界运动变化的普遍规律,也是人事社会发展的必由之路。“自然之道”主要体现在治理社会的“习行实践”中,“圣贤要道,止在彝伦日用,彼

2024-11-15 10:16

糍粑的吃法大全(蛋黄糍粑的两种做法,可煎可烤)

不同于红糖糍粑,蛋黄糍粑(也叫鸡蛋糍粑、蛋煎糍粑主打咸口,更适合作为早餐、夜宵和正餐家宴的主食。端午期间吃咸鸭蛋,既有祛毒消灾的美好祝愿,也有补铁和钙的实际功效。要是家里没有咸蛋黄,用普通的鸡蛋液来做,味道也不是不错哒。那今天就先拿鸡蛋做个例子,一起来看看“烤”和“煎”的两种做法吧。蛋黄糍粑(烤箱版

2024-11-15 10:01

万户侯是什么意思古代(古代的“万户侯”放到今天是什么地位)

古时,人们讲究“学而优则仕”,对于做官总是有种莫名的执着,这种思想的影响甚至延续至今。而作为汉代侯爵最高一级的“万户侯”,“有能得齐王头者,封万户侯”到“不受万户侯,长揖千乘君”再到“粪土当年万户侯”,更是从古至今无数文臣武将的毕生追求。那么,可能就要有许多人问了,“万户侯”究竟是什么呢?“万户侯”

2024-11-15 09:45

养蜥蜴的危害是什么呢(巨蜥有毒性有多强悍?)

«——【·前言·】——»地球上存在着众多生物多样性的生命形式,其中爬行动物世界也有着其独特的角落。在这个令人着迷的领域中,巨蜥作为一种令人着迷的生物引发了广泛的兴趣和好奇心。巨蜥的广泛分布、令人叹为观止的食性以及其引人入胜的有毒性,使其成为了爬行动物学研究领域中备受瞩目的焦点。巨蜥不仅以其独特的外貌

2024-11-15 09:31

古代通信方式有哪几种(古代老百姓如何传递信息)

中国是世界上最早建立组织传递信息的国家之一,邮驿历史虽长达3000多年,但留存的遗址、文物并不多。驿站是中国古代供传递官府文书和军事情报的人或来往官员途中食宿、换马的场所。驿站自先秦时便已有之,秦汉时已经完善。“一骑红尘妃子笑,无人知是荔枝来”说的就是杨贵妃喜欢吃南方的荔枝,通过驿站的马匹快马加鞭一

2024-11-15 09:16

秋梨膏什么梗(“蒸梨十碗,不如梨膏一勺”)

“一场秋雨一场凉”,秋分过后,秋高气爽逐渐被冷风冷雨替代,早晚温差带来的燥气愈加明显,“秋燥”带来的身体不适感也让人愈发疲乏,适当进补润燥的食物,可有效对抗气候变化带来的侵扰。梨,清甜多汁,被誉为“百果之宗”,号称“天然矿泉水”。《木草纲目》称,梨“生者清六腑之热,熟者滋五脏之阴”,民间有“秋吃梨,

2024-11-15 09:01

怎么磨钻头的技巧(4步,让废旧的钻头重振雄“锋”)

第一步:刃口摆平轮面靠很多朋友磨钻头几下就磨废了,就是因为第一步没有做好,麻花钻的角一般为118°,磨钻头时,先将钻头的主切削刃与砂轮面放置在同一个平面,然后慢慢靠上去。第二步:钻轴斜放出锋角斜放的角度,这里说的是钻头轴心线与砂轮面之间的角度,一般取60°左右即可。斜放的时候一定要注意,同时还要考虑

2024-11-15 08:46

第五人格祭司加点和技能(祭司改定位了?

祭司作为第五人格这款游戏的一位早期角色,因为独特的技能机制、灵活多变的搭配玩法、别有风味的造型表现深受玩家追捧。早期通道技能仅供求生者穿越的特性一度使得祭司成为最强辅助,在圣心医院、月亮河公园等地图中具有得天独厚的优势。而在之后的版本更迭中,尽管监管者可以穿越并破坏通道,但超长通道的存在使得祭司拥有

2024-11-15 08:16