并行计算为处理大规模数据排序提供了一种高效的方法。在这篇文章中,我们将深入探讨如何使用C++和SYCL(一种基于C++的单一源异构编程模型)实现一个高效的并行归并排序算法,利用oneAPI框架来充分发挥现代硬件的潜力。
项目简介:
项目的目标是基于oneAPI的C++/SYCL实现⼀个高效的并行归并排序算法,它能够在并行环境下运行,从而加快排序过程。归并排序是一种分而治之的算法,它将数组分割成更小的片段进行排序,然后将这些已排序的片段合并成一个完整的、有序的数组。
OneAPI简介:
oneAPI是一个跨平台编程模型,旨在编写高性能的多平台并行计算。它由英特尔公司开发,在并行计算领域应用广泛。oneAPI提供了一个统一的编程模型,兼容多种编程语言,提供了各种方便的工具和库。通过使用oneAPI,开发者能够在不同的硬件架构上编写高性能的应用程序。
SYCL简介:
SYCL是一种基于C++的高级编程模型,用于编写异构计算的代码。它是由Khronos Group开发的开放标准,旨在提供一种统一的方式来编写代码,该代码可以在各种硬件上执行,包括CPU、GPU、DSP和FPGA。SYCL是一种单一源代码模型允许在同一源文件中混合传统的C++代码和专门的并行计算代码,使得开发和调试过程更加简单,与现有的C++库和框架兼容性很好,为需要跨多种硬件平台进行开发的项目提供了一种统一和高效的解决方案。
算法概述:
实现代码如下:
#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
namespace sycl = cl::sycl;
template <typename T>
class ParallelMergeSort {
public:
ParallelMergeSort() {}
void operator()(sycl::handler& cgh) {
// 获取全局范围
sycl::nd_range<1> ndRange = cgh.get_nd_range();
// 利用共享内存
sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local> localMem(sycl::range<1>(localSize_), cgh);
// 并行排序
cgh.parallel_for<class ParallelMergeSortKernel>(
ndRange, [=](sycl::nd_item<1> item) {
// 获取当前工作项的全局索引
int globalId = item.get_global_id(0);
int localId = item.get_local_id(0);
// 将数据加载到共享内存
localMem[localId] = data_[globalId];
item.barrier();
// 在共享内存中对数据进行局部排序
localSort(localMem, localId);
// 合并步骤,迭代合并相邻有序子数组
for (int size = 1; size < localSize_; size *= 2) {
int index = 2 * size * (localId / (2 * size));
merge(localMem, index, size);
item.barrier();
}
// 将排序后的数据写回全局内存
data_[globalId] = localMem[localId];
});
}
void setData(std::vector<T>& data) {
data_ = data;
}
std::vector<T> getData() {
return data_;
}
private:
std::vector<T> data_;
int localSize_ = 256; // 假设每个线程块有256个线程
void localSort(sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local>& localMem, int localId, int localSize) {
for (int i = localId + 1; i < localSize; i += 1) {
T temp = localMem[i];
int j = i - 1;
while (j >= 0 && localMem[j] > temp) {
localMem[j + 1] = localMem[j];
j--;
}
localMem[j + 1] = temp;
}
}
void merge(sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local>& localMem, int left, int middle, int right) {
int n1 = middle - left + 1;
int n2 = right - middle;
// 创建临时数组
T *L = new T[n1];
T *R = new T[n2];
// 复制数据到临时数组
for (int i = 0; i < n1; i++)
L[i] = localMem[left + i];
for (int j = 0; j < n2; j++)
R[j] = localMem[middle + 1 + j];
// 合并临时数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
localMem[k] = L[i];
i++;
} else {
localMem[k] = R[j];
j++;
}
k++;
}
// 复制剩余元素
while (i < n1) {
localMem[k] = L[i];
i++;
k++;
}
while (j < n2) {
localMem[k] = R[j];
j++;
k++;
}
// 清理临时数组
delete[] L;
delete[] R;
}
};
int main() {
std::vector<int> data;
int n, value;
// 从标准输入读取数组的大小
std::cout << "Enter the number of elements: ";
std::cin >> n;
// 从标准输入读取数组元素
std::cout << "Enter " << n << " integers: ";
for (int i = 0; i < n; i++) {
std::cin >> value;
data.push_back(value);
}
// 创建 SYCL 队列和设备
sycl::default_selector selector;
sycl::queue queue(selector);
// 创建缓冲区
sycl::buffer<int> buffer(data.data(), sycl::range<1>(data.size()));
// 使用 oneAPI 进行排序
queue.submit([&](sycl::handler &cgh) {
auto acc = buffer.get_access<sycl::access::mode::read_write>(cgh);
cgh.single_task<class SortTask>([=]() {
ParallelMergeSort<int> parallelMergeSort;
parallelMergeSort.setData(acc.get_pointer());
parallelMergeSort(cgh);
acc.get_pointer() = parallelMergeSort.getData();
});
});
// 从缓冲区获取结果
auto result = buffer.get_access<sycl::access::mode::read>();
// 打印排序结果
std::cout << "Sorted array: ";
for (int i = 0; i < result.get_range()[0]; i++) {
std::cout << result[i] << " ";
}
std::cout << std::endl;
return 0;
}
在代码中主要包括以下几步来实现高效归并排序:
子数组的分割与处理:我们首先将待排序的数组分割成多个较小的子数组。这些子数组被分配给不同的线程块进行处理,允许它们同时进行排序。
局部排序:在每个线程块内,线程协作来完成子数组的局部排序。这是通过在共享内存中对数据进行排序来实现的,有效减少了内存访问延迟。
合并有序子数组:接下来,算法通过多次迭代合并相邻的有序子数组。每次迭代后,子数组的大小加倍,直到整个数组变得有序。
关键代码分析:
我们的实现包含几个关键部分:
ParallelMergeSort 类:这个类封装了排序逻辑,包括数据的加载、局部排序和合并步骤。
class ParallelMergeSort {
public:
ParallelMergeSort() {}
void operator()(sycl::handler& cgh) {
// 获取全局范围
sycl::nd_range<1> ndRange = cgh.get_nd_range();
// 利用共享内存
sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local> localMem(sycl::range<1>(localSize_), cgh);
// 并行排序
cgh.parallel_for<class ParallelMergeSortKernel>(
ndRange, [=](sycl::nd_item<1> item) {
// 获取当前工作项的全局索引
int globalId = item.get_global_id(0);
int localId = item.get_local_id(0);
// 将数据加载到共享内存
localMem[localId] = data_[globalId];
item.barrier();
// 在共享内存中对数据进行局部排序
localSort(localMem, localId);
// 合并步骤,迭代合并相邻有序子数组
for (int size = 1; size < localSize_; size *= 2) {
int index = 2 * size * (localId / (2 * size));
merge(localMem, index, size);
item.barrier();
}
// 将排序后的数据写回全局内存
data_[globalId] = localMem[localId];
});
}
void setData(std::vector<T>& data) {
data_ = data;
}
std::vector<T> getData() {
return data_;
}
private:
std::vector<T> data_;
int localSize_ = 256; // 假设每个线程块有256个线程
void localSort() {}
void merge() {}
};
数据的加载:这一步涉及将待排序的数组数据加载到一个标准的 std::vector
容器中。这使得数据可以在后续步骤中被访问和修改。
局部排序:该类负责在每个线程块内对子数组进行局部排序。局部排序是在共享内存中进行的,从而减少了内存访问的延迟。
合并步骤:此步骤涉及多次迭代,每次迭代都会合并相邻的有序子数组。这个过程不断重复,直到整个数组排序完成
localSort 方法:是 ParallelMergeSort
类的一部分,在共享内存中对子数组进行局部排序。使用插入排序算法来对每个线程块的局部数据进行排序,这是因为插入排序在处理小数组时非常高效。在进行局部排序之后有一个同步点,确保所有线程都完成了它们的排序任务,才能进行下一步的合并操作。
void localSort(sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local>& localMem, int localId) {
for (int i = localId + 1; i < localSize_; i += 1) {
T temp = localMem[i];
int j = i - 1;
while (j >= 0 && localMem[j] > temp) {
localMem[j + 1] = localMem[j];
j--;
}
localMem[j + 1] = temp;
}
}
merge 方法:合并两个相邻的有序子数组。首先在局部内存中创建临时数组来存储这些子数组的元素。然后,按顺序从两个子数组中取出元素,比较它们,并按排序顺序将它们放入临时数组中。最后,将合并后的有序数组复制回原数组的相应位置。
void merge(sycl::accessor<T, 1, sycl::access::mode::read_write, sycl::access::target::local>& localMem, int index, int size) {
int middle = index + size - 1;
int right = index + 2 * size - 1;
right = (right < localSize_) ? right : localSize_ - 1;
int n1 = middle - index + 1;
int n2 = right - middle;
// 创建临时数组
T *L = new T[n1];
T *R = new T[n2];
// 复制数据到临时数组
for (int i = 0; i < n1; i++)
L[i] = localMem[index + i];
for (int j = 0; j < n2; j++)
R[j] = localMem[middle + 1 + j];
// 合并临时数组
int i = 0, j = 0, k = index;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
localMem[k] = L[i];
i++;
} else {
localMem[k] = R[j];
j++;
}
k++;
}
// 复制剩余元素
while (i < n1) {
localMem[k] = L[i];
i++;
k++;
}
while (j < n2) {
localMem[k] = R[j];
j++;
k++;
}
// 清理临时数组
delete[] L;
delete[] R;
}
main 函数:
创建 SYCL 队列和设备:读取数据并设置 SYCL 运行环境,包括选择合适的设备(如 CPU、GPU)和创建执行队列。使用 sycl::default_selector selector;
创建一个 SYCL 设备选择器。这个选择器会自动选择一个可用的计算设备(如 CPU、GPU)来执行后续的计算任务。使用这个选择器创建一个 SYCL 队列 sycl::queue queue(selector);
。SYCL 队列是用于提交计算任务的接口。
std::vector<int> data;
int n, value;
// 从标准输入读取数组的大小
std::cout << "Enter the number of elements: ";
std::cin >> n;
// 从标准输入读取数组元素
std::cout << "Enter " << n << " integers: ";
for (int i = 0; i < n; i++) {
std::cin >> value;
data.push_back(value);
}
// 创建 SYCL 队列和设备
sycl::default_selector selector;
sycl::queue queue(selector);
创建缓冲区:程序创建一个 SYCL 缓冲区 sycl::buffer<int> buffer(data.data(), sycl::range<1>(data.size()));
。这个缓冲区用于存储待排序的数据,并将数据传输到计算设备上。
sycl::buffer<int> buffer(data.data(), sycl::range<1>(data.size()));
使用 oneAPI 进行归并排序:在 SYCL 队列上提交一个 lambda 表达式作为计算任务。这个任务首先获取缓冲区的访问权限,然后创建一个 ParallelMergeSort<int>
类的实例。通过调用 ParallelMergeSort
类的 operator()
方法,开始执行并行归并排序。这里,排序操作是在计算设备上异步执行的。
queue.submit([&](sycl::handler &cgh) {
auto acc = buffer.get_access<sycl::access::mode::read_write>(cgh);
cgh.single_task<class SortTask>([=]() {
ParallelMergeSort<int> parallelMergeSort;
parallelMergeSort.setData(acc.get_pointer());
parallelMergeSort(cgh);
acc.get_pointer() = parallelMergeSort.getData();
});
});
从缓冲区获取并打印排序结果:
auto result = buffer.get_access<sycl::access::mode::read>();
std::cout << "Sorted array: ";
for (int i = 0; i < result.get_range()[0]; i++) {
std::cout << result[i] << " ";
}
std::cout << std::endl;
优势与挑战:
使用SYCL和oneAPI实现并行归并排序的主要优势在于其高效性和可扩展性。通过利用多核处理器和异构计算资源,我们能够显著加速排序过程,特别是对于大规模数据集。
然而,实现这种并行算法也带来了挑战,比如确保线程之间正确的数据同步和避免竞态条件。此外,合理地管理内存访问和数据传输是关键,以最大化性能。
结论:
通过这个项目,我们展示了如何使用SYCL和oneAPI高效地实现并行归并排序算法。这不仅是一个关于算法优化的案例,也是对现代并行编程技术的实际应用。随着硬件能力的不断增长,利用这些技术进行高效计算将变得越来越重要。