【高性能计算】Sort排序的CUDA计算优化加速-冒泡\并归\双调

Sort排序的CUDA计算优化加速-冒泡\并归\双调

CUDA sort

  • 基于reduce_sum的思想,利用cuda对排序进行优化是一个常见的进阶编程问题

冒泡排序

  • Bubble Sort冒泡排序,空间复杂度O(1),时间复杂度O(n^2)
  • 1996年,J Kornerup 提出了odd-even sort算法
    在这里插入图片描述
  • odd-even sort的基本方法:
    • 奇数步中, array中奇数项array[i]与右边的item(array[i + 1])比较;
    • 偶数步中, array中奇数项array[i]与左边的item(array[i - 1]) 比较;
  • 这样,同一个step中的各个相邻比较就可以并行化了。
  • 在odd-even sort的算法下, 原本O(n^2)的总比较次数不变,但是由于并行,时间复杂度降到O(n)
#include<stdio.h>
#include<stdlib.h>
#include<cuda_runtime.h>
#include<iostream>
#include"cuda.h"

#define N 1024

// 冒泡排序 CPU实现
void cpuBubbleSort(int* nums) {
    for (int i = 0; i < N-1; i++) {
        // 每次冒泡将最大值推到末尾
        for (int j = 0; j < N-i-1; j++) {
            if (nums[j] > nums[j+1]) {
                std::swap(nums[j], nums[j+1]);
            }
        }
    }
}

// 冒泡排序 odd-even sort算法
// 原本O(n^2)的总比较次数不变 但是由于并行 时间复杂度降到O(n)
__global__ void gpuBubbleSort(int* nums) {
    int i = 0;
    int tmp = 0;

    int tid = blockIdx.x * blockDim.x + threadIdx.x;  // 此处仅一个block
    // 交换相邻元素-奇数步骤
    // array中奇数项array[i]与右边的item(array[i + 1])比较
    if((2*tid + 1) < N)
    {
        if(nums[2*tid]>nums[2*tid + 1])
        {
            tmp = nums[2*tid];
            nums[2*tid] = nums[2*tid + 1];
            nums[2*tid + 1] = tmp;
        }
    }
	__syncthreads();
	
	// 交换相邻元素-偶数步骤
    // array中奇数项array[i]与左边的item(array[i - 1]) 比较
	if((2*tid +2) < N)
	{
		if(nums[2*tid+1]>nums[2*tid + 2])
		{
			tmp = nums[2*tid + 1];
			nums[2*tid + 1] = nums[2*tid + 2];
			nums[2*tid + 2] = tmp;
		}
	}
	__syncthreads();
}

并归排序

  • CUDA sort使用GPU加速并归排序的计算
  • 归并操作是将两个有序序列合并成一个有序序列的过程, nlog(n) 的复杂度,这通常涉及以下步骤:
    • 申请一个足够大的临时数组,用于存放合并后的序列。
    • 设置两个指针,分别指向两个待合并序列的起始位置。
    • 比较两个指针所指向的元素,将较小的元素复制到临时数组中,并移动相应的指针。
    • 重复上述步骤,直到某个序列的元素全部被复制到临时数组中。
    • 将另一个序列中剩余的元素(如果有)直接复制到临时数组的末尾。
  • 并归排序是如何实现:

// 归并排序 CPU实现 nlog(n)复杂度
// 合并两个有序子数组
void merge(int *nums, int left, int mid, int right) {
    int temp[N] = {0};
    int i = left, j = mid+1, k = 0;
    while (i <= mid && j <= right) {
        temp[k++] = (nums[i] <= nums[j]) ? nums[i++] : nums[j++];
    }
    // 拷贝较长的剩余元素
    while (i <= mid) temp[k++] = nums[i++];
    while (j <= right) temp[k++] = nums[j++];
    for (int p = 0; p < k; p++) {
        nums[left + p] = temp[p];
    }
}

void cpuMergeSort(int *nums, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;

    // 对左右子数组进行排序
    cpuMergeSort(nums, left, mid);
    cpuMergeSort(nums, mid+1, right);

    // 合并已排序的子数组
    merge(nums, left, mid, right);
}

// GPU实现合并两个有序块
__global__ void gpuMergeSort(int* nums, int n, int blockSize) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int start = idx * 2 * blockSize;
    int mid = start + blockSize;
    int end = min(start + 2 * blockSize, N);

    if (mid >= end) return;

    int i = start, j = mid, k = 0;
    int* temp = new int[end - start];
    
    while (i < mid && j < end) {
        temp[k++] = (nums[i] <= nums[j]) ? nums[i++] : nums[j++];
    }
    while (i < mid) temp[k++] = nums[i++];
    while (j < end) temp[k++] = nums[j++];
    for (int p = 0; p < k; p++) {
        nums[start + p] = temp[p];
    }
    delete[] temp;
}

双调排序

  • 为什么要使用双调排序而不是并归排序?
    • 双调排序 Bitonic Sort,整体复杂度为 n*log^2(n)
    • 归并排序的缺点: 当归并排序到后期时,待归并的段主要有少数的“大段”构成,而这几个大段的归并比较难运用到多核多线程的优势。
    • 双调序列是一个先单调递增后单调递减(或者先单调递减后单调递增)的序列。
      • 即存在一个索引i(0≤i≤n-1),使得序列A[0…n-1]满足:
      • A[0] < A[1] < A[2] … < A[i-1] < A[i] > A[i+1] > A[i+2] > … > A[n-1]
      • 或者反过来,先递减后递增。
  • 双调排序的基本原理
    • 构建双调序列:
      • 对于任意给定的序列,首先通过一系列的比较和交换操作,将其转换为一个双调序列。
      • 这个过程通常是通过将序列分成较小的子序列,对每个子序列进行排序(升序或降序),然后按照特定的方式合并这些子序列来实现的。
    • 双调归并:
      • 一旦构建了双调序列,就可以使用双调归并过程来对其进行排序。
      • 双调归并是基于Batcher定理:
        • 该定理指出,将任意一个长为2n的双调序列A分为等长的两半X和Y,将X中的元素与Y中的元素一一按原序比较(即a[i]与a[i+n](i<n)比较),将较大者放入MAX序列,较小者放入MIN序列。则得到的MAX和MIN序列仍然是双调序列,并且MAX序列中的任意一个元素不小于MIN序列中的任意一个元素。
        • 通过递归地应用这个过程,最终可以得到一个有序序列。
  • 一个双调序列如何排序?
    • 假设我们有一个双调序列,则我们根据Batcher定理,将该序列划分成2个双调序列,然后继续对每个双调序列递归划分,得到更短的双调序列,直到得到的子序列长度为1为止。这时的输出序列按单调递增顺序排列。
    • 见下图:升序排序,具体方法是,把一个序列(1…n)对半分,假设n=2^k,然后1和n/2+1比较,小的放上,接下来2和n/2+2比较,小的放上,以此类推;然后看成两个(n/2)长度的序列,因为他们都是双调序列,所以可以重复上面的过程;总共重复k轮,即最后一轮已经是长度是2的序列比较了,就可得到最终的排序结果。
      在这里插入图片描述
  • 任意序列如何变成一个双调序列?
  • Bitonic Sort(双调排序)
    • 对于两个元素x,y,如果x<=y,则x,y都位于双调序列的递增部分,而递减部分没有元素,如果x>=y,则x,y都位于双调序列的递减部分,而递增部分没有元素,于是x和y构成一个双调序列。因此,任何无序的序列都是由若干个只有2个元素的双调序列连接而成。
    • 于是,对于一个无序序列,我们按照递增和递减顺序合并相邻的双调序列,按照双调序列的定义,通过连接递增和递减序列得到的序列是双调的。最终,我们可以将若干个只有2个元素的双调序列合并成1个有n个元素的双调序列。

以16个元素的array为例:
相邻两个元素合并形成8个单调性相反的单调序列
两两序列合并,形成4个双调序列,分别按相反单调性排序
4个长度为4的相反单调性单调序列,相邻两个合并,生成两个长度为8的双调序列,分别排序
2个长度为8的相反单调性单调序列,相邻两个合并,生成1个长度为16的双调序列,排序

在这里插入图片描述

  • 以下的例子中,先比较相邻的2个元素,按照 递增->递减->递增->递减 排序
  • 再比较4个元素,按照 递增->递减 排序,4个元素内部进行对比排序
    在这里插入图片描述
  • 非2的幂次长度序列排序?
    • 这样的双调排序算法只能应付长度为2的幂的数组。那如何转化为能针对任意长度的数组呢?
    • 一个直观的方法就是使用padding。即使用一个定义的最大或者最小者来填充数组,让数组的大小填充到2的幂长度,再进行排序。最后过滤掉那些最大(最小)值即可。
    • 这种方式会使用到额外的空间,而且有时候padding的空间比较大(如数组长度为1025个元素,则需要填充到2048个,浪费了大量空间)。但是这种方法比较容易转化为针对GPU的并行算法。所以一般来说,并行计算中常使用双调排序来对一些较小的数组进行排序
// CPU实现双调排序 dir调整升序降序 表示sorting direction
// 需要限制输入数组为2次幂 可以padding数组以实现填充长度
void bitonicMerge(int* nums, int low, int cnt, bool dir)
{
    if (cnt > 1) {
        int mid = cnt / 2;
        for (int i = low; i < low + mid; i++) {
            if (dir == (nums[i] > nums[i + mid])) {
                std::swap(nums[i], nums[i + mid]);
            }
        }
        bitonicMerge(nums, low, mid, dir);
        bitonicMerge(nums, low + mid, mid, dir);
    }
}
void cpuBitonicSort(int* nums, int low, int cnt, bool dir) {
    if (cnt > 1) {
        int mid = cnt / 2;  // mid表示每次处理的长度
        cpuBitonicSort(nums, low, mid, true);  // 前半升序
        cpuBitonicSort(nums, low + mid, mid, false);  // 后半降序
        bitonicMerge(nums, low, cnt, dir);
    }
}

//  GPU实现双调排序核函数
__global__ void gpuBitonicSort(int* nums, int stride, int inner_stride) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    int flipper = inner_stride/2;
    int idx_other = idx ^ flipper;  // 按位异或 计算对称索引
    if (idx < idx_other) {   // 操纵左侧的半部分
        if ((idx & stride) == 0) {  // 升序比较
            if (nums[idx] > nums[idx_other]) {
                int temp = nums[idx];
                nums[idx] = nums[idx_other];
                nums[idx_other] = temp;
            }
        } 
        else {  // 降序比较
            if (nums[idx] < nums[idx_other]) {
                int temp = nums[idx];
                nums[idx] = nums[idx_other];
                nums[idx_other] = temp;
            }
        }
    }
}

Sort算子实现完整测试代码:

#include<stdio.h>
#include<stdlib.h>
#include<cuda_runtime.h>
#include<iostream>
#include"cuda.h"

#define N 1024

// 冒泡排序 CPU实现
void cpuBubbleSort(int* nums) {
    for (int i = 0; i < N-1; i++) {
        // 每次冒泡将最大值推到末尾
        for (int j = 0; j < N-i-1; j++) {
            if (nums[j] > nums[j+1]) {
                std::swap(nums[j], nums[j+1]);
            }
        }
    }
}

// 冒泡排序 odd-even sort算法
// 原本O(n^2)的总比较次数不变 但是由于并行 时间复杂度降到O(n)
__global__ void gpuBubbleSort(int* nums) {
    int i = 0;
    int tmp = 0;

    int tid = blockIdx.x * blockDim.x + threadIdx.x;  // 此处仅一个block
    // 交换相邻元素-奇数步骤
    // array中奇数项array[i]与右边的item(array[i + 1])比较
    if((2*tid + 1) < N)
    {
        if(nums[2*tid]>nums[2*tid + 1])
        {
            tmp = nums[2*tid];
            nums[2*tid] = nums[2*tid + 1];
            nums[2*tid + 1] = tmp;
        }
    }
	
	__syncthreads();
	
	// 交换相邻元素-偶数步骤
    // array中奇数项array[i]与左边的item(array[i - 1]) 比较
	if((2*tid +2) < N)
	{
		if(nums[2*tid+1]>nums[2*tid + 2])
		{
			tmp = nums[2*tid + 1];
			nums[2*tid + 1] = nums[2*tid + 2];
			nums[2*tid + 2] = tmp;
		}
	}
	__syncthreads();
}


// 归并排序 CPU实现 nlog(n)复杂度
// 合并两个有序子数组
void merge(int *nums, int left, int mid, int right) {
    int temp[N] = {0};
    int i = left, j = mid+1, k = 0;
    while (i <= mid && j <= right) {
        temp[k++] = (nums[i] <= nums[j]) ? nums[i++] : nums[j++];
    }
    // 拷贝较长的剩余元素
    while (i <= mid) temp[k++] = nums[i++];
    while (j <= right) temp[k++] = nums[j++];
    for (int p = 0; p < k; p++) {
        nums[left + p] = temp[p];
    }
}

void cpuMergeSort(int *nums, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;

    // 对左右子数组进行排序
    cpuMergeSort(nums, left, mid);
    cpuMergeSort(nums, mid+1, right);

    // 合并已排序的子数组
    merge(nums, left, mid, right);
}

// GPU实现合并两个有序块
__global__ void gpuMergeSort(int* nums, int n, int blockSize) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int start = idx * 2 * blockSize;
    int mid = start + blockSize;
    int end = min(start + 2 * blockSize, N);

    if (mid >= end) return;

    int i = start, j = mid, k = 0;
    int* temp = new int[end - start];
    
    while (i < mid && j < end) {
        temp[k++] = (nums[i] <= nums[j]) ? nums[i++] : nums[j++];
    }
    while (i < mid) temp[k++] = nums[i++];
    while (j < end) temp[k++] = nums[j++];
    for (int p = 0; p < k; p++) {
        nums[start + p] = temp[p];
    }
    delete[] temp;
}


// CPU实现双调排序 dir调整升序降序 表示sorting direction
// 需要限制输入数组为2次幂 可以padding数组以实现填充长度
void bitonicMerge(int* nums, int low, int cnt, bool dir)
{
    if (cnt > 1) {
        int mid = cnt / 2;
        for (int i = low; i < low + mid; i++) {
            if (dir == (nums[i] > nums[i + mid])) {
                std::swap(nums[i], nums[i + mid]);
            }
        }
        bitonicMerge(nums, low, mid, dir);
        bitonicMerge(nums, low + mid, mid, dir);
    }
}
void cpuBitonicSort(int* nums, int low, int cnt, bool dir) {
    if (cnt > 1) {
        int mid = cnt / 2;  // mid表示每次处理的长度
        cpuBitonicSort(nums, low, mid, true);  // 前半升序
        cpuBitonicSort(nums, low + mid, mid, false);  // 后半降序
        bitonicMerge(nums, low, cnt, dir);
    }
}

//  GPU实现双调排序核函数
__global__ void gpuBitonicSort(int* nums, int stride, int inner_stride) {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    int flipper = inner_stride/2;
    int idx_other = idx ^ flipper;  // 按位异或 计算对称索引
    if (idx < idx_other) {   // 操纵左侧的半部分
        if ((idx & stride) == 0) {  // 升序比较
            if (nums[idx] > nums[idx_other]) {
                int temp = nums[idx];
                nums[idx] = nums[idx_other];
                nums[idx_other] = temp;
            }
        } 
        else {  // 降序比较
            if (nums[idx] < nums[idx_other]) {
                int temp = nums[idx];
                nums[idx] = nums[idx_other];
                nums[idx_other] = temp;
            }
        }
    }
}


int main() {
    int i=0;

    // CPU内存
    std::cout << " nums: " << std::endl;
    int nums[N] = {0};
    for(i=0; i<N; i++){
        nums[i] = rand()%N;
        std::cout << nums[i] << " ";  // 输出顺序可能不正确(竞争条件)
    }

    // CPU实现
    // cpuBubbleSort(nums);
    // std::cout << " cpu bubble sort nums: " << std::endl;
    // for(i=0; i<N; i++){
    //     std::cout << nums[i] << " ";  // 输出顺序可能不正确(竞争条件)
    // }
    // cpuMergeSort(nums, 0, N-1);
    // std::cout << " cpu merge sort nums: " << std::endl;
    // for(i=0; i<N; i++){
    //     std::cout << nums[i] << " ";  // 输出顺序可能不正确(竞争条件)
    // }
    // cpuBitonicSort(nums, 0, N, true);
    // std::cout << " cpu bitonic sort nums: " << std::endl;
    // for(i=0; i<N; i++){
    //     std::cout << nums[i] << " ";  // 输出顺序可能不正确(竞争条件)
    // }


    // GPU内存
    int *dev_nums = NULL;
	cudaMalloc((void**)&dev_nums, N*sizeof(int));
	cudaMemcpy(dev_nums, nums, N*sizeof(int), cudaMemcpyHostToDevice);

    // GPU实现
    // CUDA版冒泡排序由于并行竞争问题无法正确运行 对数组的写需要进行原子操作 效率极低
    // gpuBubbleSort<<<1,N/2>>>(dev_nums);

    // 并归 需要分块逐步合并
    // 当归并排序到后期 待归并的段主要有少数的“大段”构成 较难运用到多核多线程的优势
    // for (int blockSize = 1; blockSize < N; blockSize *= 2) {
    //     int threads = 32;
    //     int blocks = (N + (2*blockSize * threads) - 1) / (2*blockSize * threads);
    //     gpuMergeSort<<<blocks, threads>>>(dev_nums, N, blockSize);
    // }
    
    // 双调
    for (int stride = 2; stride <= N; stride *= 2) {  // 序列长度翻倍
        for (int inner_stride = stride; inner_stride >= 2; inner_stride /= 2) {  // 步长逐步减半
            dim3 grid_dim((N / 256 == 0)? 1 : N / 256);
            dim3 block_dim((N / 256 == 0)? N : 256);
            gpuBitonicSort<<<grid_dim, block_dim>>>(dev_nums, stride, inner_stride);
        }
    }

    cudaMemcpy(nums, dev_nums, N*sizeof(int), cudaMemcpyDeviceToHost);
    cudaFree(dev_nums);

    std::cout << " gpu sort nums: " << std::endl;
    for(i=0; i<N; i++){
        std::cout << nums[i] << " ";  // 输出顺序可能不正确(竞争条件)
    }
    system("pause");

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值