使用CUDA可以有效地计算元素更改的排序数组的位置

时间:2013-04-11 01:45:45

标签: c++ cuda parallel-processing gpu-programming

假设我们有这个排序数组

     0 1 1 1 1 2 2 2 2 2 3 10 10 10

我想有效地找到元素变化的位置。例如,在我们的数组中,位置如下:

    0 1 5 10 11

我知道有一些库(Thrust)可以实现这一目标,但是我想为教育目的创建自己的高效实现。

您可以在此处找到完整的代码:http://pastebin.com/Wu34F4M2

它还包括验证。

内核是以下功能:

__global__ void findPositions(int *device_data, 
         int totalAmountOfValuesPerThread, int* pos_ptr, int N){

   int res1 = 9999999;
   int res2 = 9999999;
   int index = totalAmountOfValuesPerThread*(threadIdx.x + 
                  blockIdx.x*blockDim.x);
   int start = index; //from this index each thread will begin searching
   if(start < N){ //if the index is out of bounds do nothing
      if(start!=0){ //if start is not in the beginning, check the previous value
        if(device_data[start-1] != device_data[start]){
        res1 = start;
        }
      }
      else res1 = start; //since it's the 
          //beginning we update the first output buffer for the thread
      pos_ptr[index] = res1;

      start++; //move to the next place and see if the 
      //second output buffer needs updating or not

      if(start < N && device_data[start] != device_data[start-1]){
         res2 = start;
      }

      if((index+1) < N)
        pos_ptr[index+ 1] = res2;
      }
}

我创建了很多线程,以便每个线程都必须使用数组的两个值。

  1. device_data包含存储在数组中的所有数字
  2. 在这种情况下,
  3. totalAmountOfValuesPerThread是每个线程必须使用的值的总量
  4. pos_ptrdevice_data具有相同的长度,每个线程将缓冲区的结果写入此device_vector
  5. Ndevice_data数组
  6. 中的总数量

    在名为res1res2的输出缓冲区中,每个线程都会保存一个之前未找到的位置,或者保留原样。

    示例:

      0   <---- thread 1
      1
      1   <---- thread 2
      1
      2   <---- thread 3
      2
      3   <---- thread 4
    

    假设大数字9999999为inf,每个线程的输出缓冲区将为:

      thread1 => {res1=0, res2=1}
      thread2 => {res1=inf, res2=inf}
      thread3 => {res1=4, res2=inf}
      thread4 => {res1=6, res2=inf}
    

    每个帖子都会更新pos_ptr device_vector,因此此向量将包含以下内容:

      pos_ptr =>{0, 1, inf, inf, 4, inf, 6, inf}
    

    完成内核后,我使用库Thrust对矢量进行排序,并将结果保存在名为host_pos的主机矢量中。因此host_pos向量将具有以下内容:

      host_pos => {0, 1, 4, 6, inf, inf, inf, inf}
    

    这种实施很糟糕,因为

    1. 内核中创建了很多分支,因此会发生低效的包裹处理
    2. 我假设每个线程仅使用2个值,这是非常低效的,因为创建了太多线程
    3. 我创建并传输一个与输入一样大的device_vector,并且也驻留在全局内存中。每个线程访问此向量以便写入结果,这是非常低效的。
    4. 以下是在每个块中包含1 000 000个线程时输入大小为512的测试用例。

           CPU time: 0.000875688 seconds
           GPU time: 1.35816 seconds
      

      另一个输入大小为10 000 000

      的测试用例
           CPU time: 0.0979209
           GPU time: 1.41298 seconds
      

      请注意,CPU版本几乎慢了100倍,而GPU几乎相同。

      不幸的是我的GPU没有足够的内存,所以让我们试试50 000 000

           CPU time: 0.459832 seconds
           GPU time: 1.59248 seconds
      

      据我了解,对于巨大的输入,我的GPU实现可能会变得更快,但我相信更高效的方法可能会使实现更快,即使是较小的输入。

      为了让我的算法运行得更快,您会建议使用什么设计?不幸的是,我想不出更好的事情。

      提前谢谢

1 个答案:

答案 0 :(得分:4)

我真的不明白为什么你认为这很糟糕的原因。线程太多了?太多线程的定义是什么?每个输入元素一个线程是CUDA程序中非常常见的线程策略。

因为您似乎愿意考虑在大部分工作中使用推力(例如,您在标记数据后愿意调用推力::排序)并考虑到BenC的观察(您正在消费大量的时间试图优化总运行时间的3%)也许你可以通过更好地利用推力来产生更大的影响。

建议:

  1. 让您的内核尽可能简单。让每个线程看起来 在一个元素,并决定基于比较制作标记 前一个元素。我不确定是否会取得任何重大进展 通过让每个线程处理2个元素。或者,让内核创建更少数量的块,但让它们循环遍历整个device_data数组,标记边界。这可能会对您的内核产生明显的改善。但同样,优化3%并不一定是您想要花费大量精力的地方。
  2. 你的内核将受到内存带宽限制。因此,我不会担心像分支这样的事情,而是专注于有效使用内存,即最大限度地减少对全局内存的读写,并寻找确保读写结合的机会。独立于程序的其余部分测试内核,并使用可视化分析器告诉您是否在内存操作方面做得很好。
  3. 考虑使用共享内存。通过让每个线程将其各自的元素加载到共享内存中,您可以轻松地合并所有全局读取(并确保您只读取每个全局元素一次,或几乎每个元素读取一次)然后在共享内存中操作,即具有每个线程将它的元素与共享内存中的前一个元素进行比较。
  4. 创建pos_ptr数组后,请注意除此之外 inf已经排序。所以也许有一个更聪明的人 选项比“thrust :: sort”然后修剪数组,到 产生结果。看看像这样的推力函数 remove_ifcopy_if。我没有对它进行基准测试,但我的猜测 他们的价格会明显低于排序,其次是 修剪数组(删除inf值)。