【问题标题】:Using cuda shared memory efficiently for storing characters有效地使用 cuda 共享内存来存储字符
【发布时间】:2013-12-12 19:59:51
【问题描述】:

假设我必须处理 8 位图像像素。我想分配共享内存来存储这些像素值,并在我的内核中使用。

现在的问题是共享内存库中的内存是按 32 位分配的。将存储一个字符(8 位像素值),并按 24 个零的序列填充。这将导致巨大的记忆损失。

那么在共享内存中存储像素值、避免内存浪费的最佳方式是什么?

【问题讨论】:

  • 每个银行块存储 4 个像素?
  • @Michael 你能给出一个示例代码来展示你的想法吗?你认为这不会导致银行冲突吗?
  • 是的,这将导致银行冲突,除非您将每个线程处理 4 个像素。
  • @Michael 所以你的意思是我将使用一个有 4 个像素成员的结构?你能给出一行代码来证明你的想法并将其作为答案吗?
  • “一个字符 ... 将被存储 24 个零的序列” 不知道你在这里得到什么。这是不正确的。如果您定义__shared__ char my_chars[32];,则将有一个由 32 个字符组成的序列,占用共享内存中的 8 个连续 32 位位置,没有填充。

标签: c cuda gpu gpgpu


【解决方案1】:

使用结构在 32 位块上存储 4 个像素。
每个线程处理整个块以避免银行冲突和非合并访问。

typedef struct
{
  unsigned char pixels[4];
} FourPixels;

__global__ void myKernel(FourPixels* gpixels)
{
  extern __shared__ FourPixels spixels[];

  int id = blockIdx.x * blockDim.x + threadIdx.x;

  //copy on shared memory
  spixels[id] = gpixels[id];

  //example : remove blue component
  spixels[id].pixels[0] &= 0xFC;
  spixels[id].pixels[1] &= 0xFC;
  spixels[id].pixels[2] &= 0xFC;
  spixels[id].pixels[3] &= 0xFC;

  //copy result on global memory
  gpixels[id] = spixels[id];
}

__host__ int main()
{
  FourPixels* mypixs;
  cudaMalloc(&mypixs, 4*sizeof(FourPixels));

  myKernel<<<1, 4, 4*sizeof(FourPixels)>>>(mypixs); // 16 pixels !
  cudaDeviceSynchronize();

  cudaFree(mypixs);
}

【讨论】:

  • 优秀。此外,可以从内核调用__device__ 函数以避免为每个字节复制代码。
  • @RogerDahl 你说的是上面代码中的“移除蓝色组件”吗?
  • @gpuguy:是的。对于像具体示例这样非常简单的事情,单独的函数可能没有意义,但如果重复的代码超过一两行,我会创建一个设备函数。
【解决方案2】:

可能会有一些误解,所以我想我也会添加一个答案,而不是更多的 cmets。作为序言,让我声明我不打算详细解释银行冲突是如何产生的。如果您想了解这一点,您可以参加网络研讨会,以及许多其他关于 SO 的问题。

  1. 从存储空间效率的角度来看,存储char(或unsigned char,我在本次讨论中将使用char)的数组并不缺乏效率,但这两者之间没有区别讨论)在共享内存中:

    __shared__ char my_chars[4096];
    

    上述声明中的所有字节都将连续打包,没有中间填充。无论我们如何访问这样的数组,这都是正确的。

  2. 从内存带宽利用效率的角度来看,访问每线程 32 位或每线程 64 位将始终提供最大的内存带宽利用率,因此每个线程 4 像素/字节/字符的分组将对此有所帮助。请注意,即使使用我的char 数组定义,我们也不需要特殊的struct 来完成此操作,但可以肯定的是,Michael 建议的 4 像素结构定义清楚地表明了如何执行此操作。请注意,在此参考资料中,我并不是说 Michael 代码的每个 方面都会带来更好的性能。显然,当访问单个像素时,我们不是在谈论 32 位与 8 位,所以我的 cmets 不适用于 Michael 的代码。但是,例如,在 Michael 的代码中,有这一行将数据从全局内存传输到共享内存:

    spixels[id] = gpixels[id];
    

    假设编译器将结构副本识别为 4 字节传输,这将具有更好的内存带宽利用率(无论是在全局端还是在共享端)。

  3. 从银行冲突的角度来看,银行冲突源于访问模式,而不是主要源于数据在内存中的存储方式。在 cc 2.0 和更新的设备上,之前的存储和访问模式定义:

    __shared__ char my_chars[4096];
    int idx=threadIdx.x+blockDim.x*blockIdx.x;
    char my_pixel = my_chars[idx]; 
    

    不要不要引起银行冲突。根据访问模式可能会出现银行冲突,例如:

    char my_pixel = my_chars[128*idx];
    

    将导致 32 路银行冲突的病态案例。但是,如果我们每个线程访问 32 位,则可以构建类似的病态访问模式; “4 像素结构”方法并不能防止这种糟糕的访问模式。

【讨论】:

  • @Robert 关于你的观点。 2、你谈到了cacheline。寄存器和共享内存之间是否有缓存?我怀疑在共享内存上下文中是否存在合并和非合并访问的问题。
  • 你是对的。我已经删除了对缓存的引用。但是,全局内存访问的合并概念与存储库与共享内存的冲突的概念有关。导致全局内存合并的访问模式也倾向于避免共享内存中的存储库冲突。然而,这些概念也有所不同。
  • 关于上面的 2(更新)wrt Michaels'code 我知道在任何时候线程 0 和线程 1(仅考虑两个线程应用程序)都不会访问连续的内存位置,因为第一个成员每个结构都不会相邻那么迈克尔的代码具有更好的带宽利用率的原因是什么(另请注意,每个线程正在访问4个值但串行)?不胜感激,如果您能在答案中给出解释,以供大家参考
  • 相应地更新了我的答案。我并没有说 Michael 的代码的每个方面都更好,即使是出于这个考虑。仅在我们谈论 32 位与 8 位访问的地方。
  • 编译器能否将 4 个连续的 8 位共享内存访问合并为一个 32 位访问?这似乎意味着编译器必须确保传入的字节指针是 32 位对齐的,它可以知道吗?
猜你喜欢
  • 2013-01-03
  • 2011-06-29
  • 2018-11-30
  • 1970-01-01
  • 2015-01-13
  • 1970-01-01
  • 2016-12-24
  • 2012-06-11
  • 2012-07-01
相关资源
最近更新 更多