基于oneAPI的SYCL归并排序加速实现
任务概述
使用基于oneAPI的C++/SYCL实现一个归并排序操作。输⼊为乱序数字,输出顺序数字。
技术方案
(1)oneAPI、SYCL介绍
oneAPI 是一个由英特尔主导的开放、全面的软件栈,旨在简化多核和异构计算的编程。oneAPI 提供了一个统一的编程模型,可以用于不同类型的处理器,包括 CPU、GPU、FPGA 和其他加速器。通过 oneAPI,开发人员可以更轻松地利用各种处理器的性能优势,同时减少针对特定处理器类型的编程复杂性。
SYCL(pronounced “sickle”)是一种基于 C++ 的编程模型,用于实现异构计算。SYCL 的目标是提供一种简单且高性能的编程模型,以实现 CPU、GPU 和其他加速器之间的无缝协同。SYCL 基于标准的 C++,并引入了许多用于表示并行性和内存关系的新特性。它使开发人员能够使用标准的 C++ 代码编写并行程序,而无需深入了解特定硬件体系结构。
总的来说,oneAPI 为异构计算提供了统一的编程模型和工具集,而 SYCL 则是其中的一部分,专注于提供基于 C++ 的高性能并行编程模型。这些工具和模型使开发人员能够更轻松地利用各种处理器的潜力,并加快异构计算应用程序的开发和部署速度。
(2)归并排序
归并排序是一种经典的分治算法,它通过递归地将数组分成两半、对每一半进行排序,然后合并两个有序的子数组来实现整体排序。
(3)基于oneAPI的SYCL归并排序
实现mergeSort函数时,可以使用了buffer类来存储数据,并使用queue类来提交任务到设备执行
代码实现
传统归并排序
%%writefile lab/merge_classic.cpp
//==============================================================
// Copyright © Intel Corporation
//
// SPDX-License-Identifier: MIT
// =============================================================
#include <iostream>
#include <vector>
#include <chrono>
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);
for (int i = 0; i < n1; ++i) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; ++j) {
R[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
// 将剩余的元素拷贝回原数组
while (i < n1) {
arr[k++] = L[i++];
}
while (j < n2) {
arr[k++] = R[j++];
}
}
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 i = 0; i < size; ++i) {
// 生成 -10000 到 10000 之间的随机数
arr[i] = (rand() % 20001) - 10000;
}
return arr;
}
int main() {
int array_size = 100000;
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的归并排序
%%writefile lab/merge.cpp
#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <chrono>
using namespace sycl;
std::vector<int> generateRandomArray(int size) {
std::vector<int> arr(size);
for (int i = 0; i < size; ++i) {
// Generate random numbers between 37 and 2064
arr[i] = (rand() % 2028) + 37;
}
return arr;
}
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 any
while (i < n1) {
arr_acc[k] = L[i];
i++;
k++;
}
// Copy the remaining elements of R[], if 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);
int n1 = mid - left + 1;
int n2 = right - mid;
buffer<int, 1> left_buf(&arr.get_pointer()[left], range<1>(n1));
buffer<int, 1> right_buf(&arr.get_pointer()[mid + 1], range<1>(n2));
buffer<int, 1> temp_buf(range<1>(n1 + n2));
{
auto L = left_buf.get_access<sycl::access::mode::read>();
auto R = right_buf.get_access<sycl::access::mode::read>();
auto temp = temp_buf.get_access<sycl::access::mode::write>();
parallel_for(range<1>(n1), [=](id<1> idx) {
temp[idx] = L[idx];
});
parallel_for(range<1>(n2), [=](id<1> idx) {
temp[n1 + idx] = R[idx];
});
}
{
auto temp = temp_buf.get_access<sycl::access::mode::read_write>();
parallel_sort(temp, temp + n1 + n2);
auto arr_acc = arr.get_access<sycl::access::mode::write>();
parallel_for(range<1>(n1 + n2), [=](id<1> idx) {
arr_acc[left + idx] = temp[idx];
});
}
}
}
int main() {
int array_size = 100000;
std::vector<int> a = generateRandomArray(array_size);
auto start = std::chrono::high_resolution_clock::now(); // Get start time
queue q;
buffer<int, 1> arr_buf(a.data(), range<1>(a.size()));
q.submit([&](handler &h) {
mergeSort(q, arr_buf, 0, a.size() - 1);
});
q.wait_and_throw();
auto arr_acc = arr_buf.get_host_access();
auto end = std::chrono::high_resolution_clock::now(); // Get end time
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << 运行时间: " << duration.count() << "毫秒" << std::endl;
return 0;
}
实验对比
(1)运行截图
实验通过随机生成不同长度的乱序vector (n),加速前后归并排序结果一致。下图展示n=100000时,基于oneAPI的SYCL归并排序的结果:
(2)效率对比
输入乱序数组长度(n) | 传统归并排序 | 基于oneAPI的SYCL归并排序 |
---|---|---|
100000 | 65ms | 1871ms |
10000000 | 6705ms | 13111ms |
100000000 | 71945ms | 117427ms |
运行平台:Intel® DevCloud
注:
1、由于使用Devcloud平台,计算资源为动态分配,表中每组数据均为重复5次取平均数
2、由于平台差异,传统归并排序得到的计算时间或与其他平台(如本机)可能存在较大的差距,但不影响同平台下的加速效果比较。
实验总结
根据实验结果,我们可以看到,在输入规模相对较小的情况下(例如100000),传统归并排序比基于 oneAPI 的 SYCL 归并排序要快得多。这是因为 SYCL 归并排序需要额外的开销来处理并行计算。
然而,随着输入规模较大的情况下,SYCL 归并排序并没有表现出更好的性能优势。
可能存在SYCL加速的实现问题,需要进一步修改和验证。