基于oneAPI的SYCL异构计算在排序中的应用

引言

  • 在并行计算和算法优化领域,oneAPI 和 SYCL 的结合也同样展现出了巨大的潜力,特别是在数据排序这样的基础任务中。合并排序(MergeSort)作为一种效率较高的排序算法,在处理大量数据时的性能表现尤为关键。通过应用 oneAPI 和 SYCL,我们能够在多种计算平台上实现合并排序的高效并行化处理,从而大幅提升性能。
  • 在具体的实现案例中,我们考虑了对一个较大的数据集应用合并排序算法。首先,我们使用传统的串行算法编写了合并排序的基本逻辑。然后,通过使用 SYCL,我们对排序算法中的关键部分进行并行化处理。SYCL 提供的并行计算模型不仅简化了编程工作,还使得算法能够充分利用现代计算平台的多核心和多线程特性。
  • 这种并行化处理显著加快了排序过程,尤其是在处理大型数据集时。我们的实验显示,与传统串行方法相比,使用 oneAPI 和 SYCL 实现的并行合并排序在多种计算设备上都实现了显著的性能提升。
  • 在未来的工作中,我们计划深入探索 oneAPI 和 SYCL 在数据排序和其他算法优化方面的应用潜力。我们将考虑将这些技术应用于更复杂的排序算法和其他数据处理任务,如大规模数据分析、机器学习等领域。同时,我们也期待探索这些技术在异构计算环境中的应用,以进一步提高算法的性能和效率。通过这种方式,oneAPI 和 SYCL 将继续在现代计算领域扮演关键角色,为处理复杂的数据处理任务提供强大的支持。

排序概览

  • 排序概览: 排序是计算机科学中的一项基础任务,涉及将数据元素按照特定顺序(通常是升序或降序)排列。不同的排序算法有着各自的特点和应用场景。

  • 主要排序算法

  1. 冒泡排序
    • 原理:重复地遍历待排序的列表,比较相邻元素,如果顺序错误就交换它们。
    • 时间复杂度:平均和最差情况为 O ( n 2 ) O(n^2) O(n2)
  2. 选择排序
    • 原理:从待排序的数据中寻找最小(或最大)元素,放到序列的起始位置。
    • 时间复杂度:平均和最差情况为 O ( n 2 ) O(n^2) O(n2)
  3. 插入排序
    • 原理:构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
    • 时间复杂度:平均为 O ( n 2 ) O(n^2) O(n2),最佳情况为 O ( n ) O(n) O(n)
  4. 快速排序
    • 原理:选择一个基准值,将数组分为两个子数组,分别对子数组进行快速排序。
    • 时间复杂度:平均为 O ( n l o g n ) O(nlogn) O(nlogn),最差情况为 O ( n 2 ) O(n^2) O(n2)
  5. 堆排序
    • 原理:利用堆这种数据结构所设计的一种排序算法,将最大元素移至堆顶,然后将其从堆中删除,再重新调整堆。
    • 时间复杂度:平均和最差情况为 O ( n l o g n ) O(nlogn) O(nlogn)
  • 归并排序(Merge Sort)在这些排序算法中以其独特的特性和优势脱颖而出:

  • 基本原理:归并排序是基于分治法的算法,它将数组分为两半,对每一半递归地应用归并排序,然后将结果合并。

  • 时间复杂度:对于所有情况,其时间复杂度均为 O ( n l o g n ) O(nlogn) O(nlogn),这使得归并排序在大数据量处理中非常高效。

  • 空间复杂度:归并排序需要 O ( n ) O(n) O(n)的额外空间,因为它需要一个与原数组相同大小的辅助数组来合并排序后的数据。

  • 稳定性:归并排序是一种稳定的排序方法,即值相同的元素在排序前后的相对位置不变。

  • 适用性:特别适用于大型数据集的排序,因为其性能不受数据初始顺序的影响。

  • 并行化能力:归并排序特别适合于并行计算。不同部分的归并操作可以同时在多个处理器上进行,从而显著加速排序过程。

  • 应用场景:广泛应用于大型数据库排序、文件系统排序以及作为各种高级算法的基础组件。

​ 总结来说,归并排序以其优秀的性能、稳定性和并行化能力,在众多排序算法中占有重要地位,尤其适用于大规模数据处理和复杂计算环境。

代码与运行结果

经典代码

//==============================================================
// Copyright © Intel Corporation
//
// SPDX-License-Identifier: MIT
// =============================================================
#include <iostream>
#include <vector>
#include <chrono>
#include <cstdlib> // 引入标准库以支持随机数生成

// 将数组arr的[left, mid]和[mid+1, right]范围内的元素合并
void merge(std::vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    std::vector<int> L(n1), R(n2);

    // 拷贝数据到临时数组L和R
    std::copy(arr.begin() + left, arr.begin() + mid + 1, L.begin());
    std::copy(arr.begin() + mid + 1, arr.begin() + right + 1, R.begin());

    int i = 0, j = 0, k = left;
    // 合并两个临时数组回到原数组
    while (i < n1 && j < n2) {
        arr[k++] = (L[i] <= R[j]) ? L[i++] : R[j++];
    }

    // 将剩余的元素拷贝回原数组
    std::copy(L.begin() + i, L.end(), arr.begin() + k);
    std::copy(R.begin() + j, R.end(), arr.begin() + k);
}

// 对数组arr的[left, right]范围进行归并排序
void mergeSort(std::vector<int>& arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        merge(arr, left, mid, right);
    }
}

// 生成一个随机整数数组
std::vector<int> generateRandomArray(int size) {
    std::vector<int> arr(size);
    for (int& val : arr) {
        val = (rand() % 20001) - 10000;
    }
    return arr;
}

int main() {
    int array_size = 1000000000;
    std::vector<int> data = generateRandomArray(array_size);
    
    auto start = std::chrono::high_resolution_clock::now();
    mergeSort(data, 0, data.size() - 1);
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "运行时间: " << duration.count() << " 毫秒" << std::endl;

    return 0;
}

基于oneAPI的SYCL归并排序

#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <chrono>

using namespace sycl;

void merge(queue &q, buffer<int, 1> &arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // Create temporary arrays
    std::vector<int> L(n1), R(n2);

    // Use get_host_access without template arguments
    auto arr_acc = arr.get_host_access();

    // Copy data to temp arrays L[] and R[]
    for (int i = 0; i < n1; i++)
        L[i] = arr_acc[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr_acc[mid + 1 + j];

    // Merge the temp arrays back into arr[left..right]
    int i = 0;
    int j = 0;
    int k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr_acc[k] = L[i];
            i++;
        } else {
            arr_acc[k] = R[j];
            j++;
        }
        k++;
    }

    // Copy the remaining elements of L[], if there are any
    while (i < n1) {
        arr_acc[k] = L[i];
        i++;
        k++;
    }

    // Copy the remaining elements of R[], if there are any
    while (j < n2) {
        arr_acc[k] = R[j];
        j++;
        k++;
    }
}

void mergeSort(queue &q, buffer<int, 1> &arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(q, arr, left, mid);
        mergeSort(q, arr, mid + 1, right);
        merge(q, arr, left, mid, right);
    }
}

std::vector<int> generateRandomArray(int size) {
    std::vector<int> arr(size);
    for (int i = 0; i < size; ++i) {
        // 生成 -10000 到 10000 之间的随机数
        arr[i] = (rand() % 20001) - 10000;
    }
    return arr;
}


int main() {
    int array_size = 1000000000;
    std::vector<int> data = generateRandomArray(array_size);

    queue q;
    
    auto start = std::chrono::high_resolution_clock::now(); // 获取开始时间
    buffer<int, 1> arr_buf(data.data(), range<1>(data.size()));

    q.submit([&](handler &h) {
        mergeSort(q, arr_buf, 0, data.size() - 1);
    });

    q.wait_and_throw();
    
    auto end = std::chrono::high_resolution_clock::now(); // 获取结束时间
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "运行时间: " << duration.count() << " 毫秒" << std::endl;


    return 0;
}

实验结果

1000数组长度

  • classical
    image-20231202105232223

  • oneAPI

image-20231202105455969

100000数组长度

  • classical

image-20231202105532973

  • oneAPI

image-20231202105719068

10000000数组长度

  • classical
    image-20231202105756603

  • oneAPI

image-20231202110003227

100000000数组长度

  • classical

image-20231202110141711

  • oneAPI

image-20231202110506809

实验结论

排序数组长度(个数)Classical归并排序耗时(ms)oneAPI归并排序耗时(ms)
100012
10000069116
10000000688412538
10000000073388126545

这个表格提供了两种归并排序方法(传统的归并排序和oneAPI归并排序)在处理不同长度数组时的耗时(以毫秒为单位)。从这些数据中,我们可以得出几个洞见:

  1. 规模敏感性:无论是传统归并排序还是oneAPI归并排序,随着数组长度的增加,所需的排序时间显著增加。这表明归并排序的性能与输入数据的大小紧密相关。
  2. 性能比较:在小规模数据(例如1000个元素)上,两种排序方法的性能差异不大(1ms对比2ms)。然而,随着数据规模的增大,这种差异变得更加明显。在最大的数据集(1亿个元素)上,oneAPI归并排序的耗时几乎是传统归并排序的两倍。
  3. 时间增长趋势:对于较小的数据集(1000至100,000个元素),传统归并排序的时间增长较为平缓。然而,在处理非常大的数据集(如1亿个元素)时,耗时急剧增加,这可能表明在处理大规模数据时,传统归并排序的效率会受到较大影响。
  4. oneAPI归并排序的性能开销:与传统归并排序相比,oneAPI归并排序在所有测试的数据集上都表现出更高的耗时。这可能表明oneAPI归并排序在处理大规模数据时可能不如传统方法高效,或者它可能需要更多的优化来提高其性能。
  5. 对未来优化的指示:考虑到oneAPI归并排序在大规模数据处理上的性能损失,可能需要针对大数据集进行特定的优化或考虑使用不同的并行/优化策略。
  6. 算法选择的重要性:这些数据强调了根据数据规模和性能需求选择合适排序算法的重要性。对于小型数据集,两种方法都是可行的,但在处理大规模数据集时,算法选择可能会对性能产生显著影响。
  7. 实际应用场景的考量:选择排序算法时,还需要考虑实际应用场景,如内存使用、并行化能力和实现复杂度等因素。尽管oneAPI归并排序在性能上不如传统归并排序,但它可能在其他方面(如更好的并行处理能力)提供优势。

结论

​ 归并排序在处理大规模数据时具有较好的性能,这表明它是一种高效的排序算法。在小规模数据上,传统归并排序和oneAPI归并排序的性能差异不大,但在大规模数据上,oneAPI归并排序的性能劣于传统归并排序。针对大规模数据处理的优化是必要的,尤其是在处理具有数百万甚至数十亿个元素的数组时。

​ 这种结果造成的原因可能是由于我们对于oneAPI的使用不当造成的,可以再进一步进行研究。oneAPI是一个开放的统一的API框架,旨在加速应用程序的开发,提高应用程序的性能和可扩展性。然而,由于oneAPI的使用不当,可能会导致应用程序出现各种问题,包括性能问题、安全问题、兼容性问题等。因此,我们需要仔细研究oneAPI的使用方法,了解其特性和限制,正确地使用它来开发应用程序。
总之,对于oneAPI的使用不当可能会对应用程序造成不良影响。因此,我们需要认真研究其使用方法,积累实践经验,以确保正确地使用它来开发高性能、高可扩展性的应用程序。

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值