CUDA编程练习(十二) 双调归并排序

1 想法

难啃的东西

双调归并的核心思想是两段连续数组经过一个操作,合并为一个,性质不变。说起来简单,实际步骤会比较困难,因为涉及许多边界条件确定,复杂的升序和降序控制。

2 原理解释

2.1 双调归并排序

包含两个部分:如何双调归并,如何排序。

图1中,同字母索引的相互比较交换。

图2中,BM使得该区域变成尖尖对应的排序方式(升序/降序),逐渐变到最后。

Question:排序深度是否是log的平方?

3 代码

多block版本很复杂

3.1 单block版本

唯一需要注意的,是为什么2*threadIdx.x-(threadIdx.x & (stride-1))能将线程映射到对应的位置。你可以自己写一个对应关系看看。

// 交换器
__device__ void comparator(ELEM_TYPE &A, ELEM_TYPE &B, uint dir) {
    ELEM_TYPE t;
    // 交换各自独立
    if ((A>B) == dir) {  // dir=1就是升序
        t=A; A=B; B=t;
    }
}
// 单block双调排序
__global__ void bitonicSort_singleBlock(ELEM_TYPE* d_src, ELEM_TYPE* d_tar, uint n, uint dir) {
    __shared__ ELEM_TYPE temp[SHARED_SIZE];

    int tx = threadIdx.x;
    temp[tx] = d_src[tx];
    temp[tx+BLOCK_DIM] = d_src[tx+BLOCK_DIM];


    // 双调排序至全局双调
    for (uint size=2; size<n; size<<=1) {  // size是,当前要构造的双调规模
        // 确定当前排序次序
        uint curdir = dir ^ ((threadIdx.x & (size/2)) != 0);
        // 双调归并
        for (uint stride=size/2; stride>0; stride>>=1) {
            __syncthreads();
            uint pos = 2*threadIdx.x-(threadIdx.x & (stride-1));
            comparator(temp[pos], temp[pos+stride], curdir);
        }
    }
    // 全局双调后,把它变为单调,最终使用一次归并
    for (uint stride=n/2; stride>0; stride>>=1) {
        __syncthreads();
        uint pos = 2*threadIdx.x-(threadIdx.x & (stride-1));
        comparator(temp[pos], temp[pos+stride], dir);
    }
    __syncthreads();

    // 移动回去
    d_tar[tx] = temp[tx];
    d_tar[tx+BLOCK_DIM] = temp[tx+BLOCK_DIM];
}

3.2 多block版本(包括归并)

这个对应关系过于复杂,尤其是如何将线程对应到它应该执行的大小判断。这里只做代码分析:首先&(n-1)的意思是,向n取余数。comparatorI取值范围就是0~n/2-1。再和size/2取&,意思就是你以size/2为单位跳跃,你是第奇数个还是偶数个。

为什么分两个函数?因为stride可以小于一个共享内存大小是,不必进行多block之间同步了。

__global__ void bitonicSort_multiBlock_intra(ELEM_TYPE* d_src, ELEM_TYPE* d_tar, uint n) {
    __shared__ ELEM_TYPE temp[SHARED_SIZE];

    uint tidx = blockIdx.x*blockDim.x*2 + threadIdx.x;
    uint tx = threadIdx.x;
    uint dir = blockIdx.x & 1;

    temp[tx] = d_src[tidx];
    temp[tx+BLOCK_DIM] = d_src[tidx+BLOCK_DIM];
    // 双调排序至共享内存双调
    for (uint size=2; size<SHARED_SIZE; size<<=1) {  // size是,当前要构造的双调规模
        // 确定当前排序次序
        uint curdir = dir ^ ((tx & (size/2)) != 0);
        // 双调归并
        for (uint stride=size/2; stride>0; stride>>=1) {
            __syncthreads();
            uint pos = 2*tx-(tx & (stride-1));
            comparator(temp[pos], temp[pos+stride], curdir);
        }
    }

    // 全局双调后,把它变为单调,最终使用一次归并
    for (uint stride=BLOCK_DIM; stride>0; stride>>=1) {
        __syncthreads();
        uint pos = 2*tx-(tx & (stride-1));
        comparator(temp[pos], temp[pos+stride], dir);
    }
    __syncthreads();

    // 移动回去
    d_tar[tidx] = temp[tx];
    d_tar[tidx+BLOCK_DIM] = temp[tx+BLOCK_DIM];
}

// 块间合并
__global__ void bitonicMergeGlobal(ELEM_TYPE* d_src, ELEM_TYPE* d_tar, uint n, uint size, uint stride, uint dir) {
    uint tidx = blockIdx.x*blockDim.x+threadIdx.x;
    uint curdir = dir ^ ( (tidx & (n/2-1) & (size/2)) != 0);
    uint pos = 2*tidx-(tidx&(stride-1));
    ELEM_TYPE A = d_src[pos], B = d_tar[pos+stride];
    comparator(A, B, curdir);
    d_tar[pos] = A;
    d_tar[pos+stride] = B;
}

__global__ void bitonicMergeShared(ELEM_TYPE* d_src, ELEM_TYPE* d_tar, uint n, uint size, uint dir) {
    __shared__ ELEM_TYPE ss[SHARED_SIZE];
    uint tx = threadIdx.x;
    d_src += blockIdx.x*SHARED_SIZE + threadIdx.x;
    d_tar += blockIdx.x*SHARED_SIZE + threadIdx.x;

    ss[tx] = d_src[0];
    ss[tx+SHARED_SIZE/2] = d_src[SHARED_SIZE/2];
    uint comparatorI = (blockIdx.x*blockDim.x+tx) & (n/2-1);
    uint curdir = dir ^ ((comparatorI&(size/2)) != 0);

    for(uint stride=SHARED_SIZE/2; stride>0; stride>>=1) {
        __syncthreads();

        uint pos = 2*tx-(tx & (stride-1));
        comparator(ss[pos], ss[pos+stride], curdir);
    }

    d_tar[0] = ss[tx];
    d_tar[SHARED_SIZE/2] = ss[tx+SHARED_SIZE/2];
}

4 总结

最后两个函数的编写能理解,但是让我来写,我会在对应关系上折磨1w年。有没有什么好方法来推导出这种复杂映射关系?

但是,本篇我以通晓原理为核心要务。因为光理解+看懂就很费劲。

我承认我摸到了我的上限。(好困。。。)

虽然下次我认为我的上限不只是如此。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值