目录
说明:本任务是C语言课程与Intel合作的任务。通过自己撰写并行归并排序算法,理解数据分割与合并以及线程之间的协作对于运行效率的作用。
转载请说明来源。
1 题目描述
1.1 描述
使用基于oneAPI的C++/SYCL实现⼀个高效的并行归并排序。需要考虑数据的分割和合并以及线程之间的协作。
1.2 分析&示例
归并排序是⼀种分治算法,其基本原理是将待排序的数组分成两部分,分别对这两部分进行排序,然后将已排序的子数组合并为⼀个有序数组。可考虑利用了异构并行计算的特点,将排序和合并操作分配给多个线程同时执行,以提高排序效率。具体实现过程如下:
-
将待排序的数组分割成多个较小的子数组,并将这些⼦数组分配给不同的线程块进行处理。
-
每个线程块内部的线程协作完成子数组的局部排序。
-
通过多次迭代,不断合并相邻的有序⼦数组,直到整个数组有序。
在实际实现中,归并排序可使用共享内存来加速排序过程。具体来说,可以利用共享内存来存储临时数据,减少对全局内存的访问次数,从而提高排序的效率。另外,在合并操作中,需要考虑同步机制来保证多个线程之间的数据⼀致性。
需要注意的是,在实际应用中,要考虑到数组大小、线程块大小、数据访问模式等因素,来设计合适的算法和参数设置,以充分利用目标计算硬件GPU的并行计算能力,提高排序的效率和性能。
2 代码实现
2.1 设备选择
在这里,我们可以选择不同的设备对后续排序算法进行计算。
import ipywidgets as widgets
device = widgets.RadioButtons(
options=['GPU Gen9', 'GPU Iris XE Max', 'CPU Xeon 6128', 'CPU Xeon 8153'],
value='CPU Xeon 6128',
description='Device:',
disabled=False
)
display(device)
2.2 代码实现
2.2.1 归并算法描述
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略。分治法将问题分解为若干个规模较小的子问题,然后递归地对子问题进行解决,最后将子问题的解决结果合并得到原问题的解。
归并排序的基本思想是:将待排序的序列不断地拆分为子序列,直到每个子序列只有一个元素为止。然后,将这些有序的子序列两两合并,得到更大的有序子序列。重复此过程,直到得到整个序列。
归并排序的具体实现步骤如下:
- 将待排序的序列拆分为两个子序列,直到每个子序列只有一个元素为止。
- 将两个有序的子序列合并为一个有序的子序列。
- 重复步骤2,直到得到整个序列。
2.2.2.1 基础归并算法实现
-
归并排序原理:
- 归并排序是一种分治算法,它将数组分为两半,对每一半递归地应用归并排序,然后将排序好的两部分合并成一个完整的排序数组。
-
合并函数 (
merge
):- 输入参数:排序数组
arr
,左边界left
,中点middle
,右边界right
。 - 功能:将两个已排序的子数组(
arr[left..middle]
和arr[middle+1..right]
)合并成一个有序数组。 - 实现:创建两个临时数组
L
和R
,分别复制左半部分和右半部分的数据,然后按顺序将它们合并回原数组arr
。
- 输入参数:排序数组
-
归并排序函数 (
mergeSort
):- 输入参数:SYCL 队列
q
,排序数组arr
,左边界left
,右边界right
。 - 功能:递归地对数组进行分割,并对每个子数组进行排序和合并。
- 实现:递归调用自身以对数组的左半部分和右半部分进行排序,然后调用
merge
函数将它们合并成一个有序的数组。
- 输入参数:SYCL 队列
-
主函数 (
main
):- 功能:读取数据,初始化SYCL设备和队列,执行排序,打印排序后的数组。
- 实现:从文件
problem-2.txt
读取浮点数数据到arr
,选择SYCL设备(GPU或CPU),创建队列,调用mergeSort
进行排序,最后输出排序结果。
不考虑多线程同步机制,撰写基础的归并排序算法的代码如下:
%%writefile lab/my_sort.cpp
#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <sstream>
using namespace sycl;
// Merge function to merge two sorted arrays
void merge(std::vector<float>& arr, size_t left, size_t middle, size_t right) {
size_t i, j, k;
size_t n1 = middle - left + 1;
size_t n2 = right - middle;
// Create temporary arrays
std::vector<float> L(n1), R(n2);
// Copy data to temporary arrays L[] and R[]
for (i = 0; i < n1; i++)
L[i] = arr[left + i];
for (j = 0; j < n2; j++)
R[j] = arr[middle + 1 + j];
// Merge the temporary arrays back into arr[left..right]
i = 0; // Initial index of first subarray
j = 0; // Initial index of second subarray
k = left; // Initial index of merged subarray
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// Copy the remaining elements of L[], if there are any
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// Copy the remaining elements of R[], if there are any
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
// Recursive function for parallel merge sort
void mergeSort(queue& q, std::vector<float>& arr, size_t left, size_t right) {
if (left < right) {
size_t middle = left + (right - left) / 2;
// Recursively sort first and second halves
mergeSort(q, arr, left, middle);
mergeSort(q, arr, middle + 1, right);
// Merge the sorted halves
merge(arr, left, middle, right);
}
}
int main() {
std::vector<float> arr;
std::ifstream file("problem-2.txt");
std::string line;
if (file.is_open()) {
getline(file, line);
file.close();
} else {
std::cerr << "Unable to open file";
return 1;
}
std::istringstream iss(line);
float number;
while (iss >> number) {
arr.push_back(number);
}
// Choose the device
device selected_device;
try {
// Try to select GPU device
selected_device = gpu_selector{
}.select_device();
std::cout << "Using GPU." << std::endl;
} catch (const sycl::exception& e) {
// If GPU selection fails, fall back to CPU
std::cerr << "GPU not available. Using CPU instead." << std::endl;
selected_device = cpu_selector{
}.select_device();
std::cout << "Using CPU." << std::endl;
}
// Create a SYCL queue
queue q(selected_device);
// Print the unsorted array
std::cout << "Unsorted array:" << std::endl;
for (size_t i = 0; i < arr.size(); ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// Call parallel merge sort
mergeSort(q, arr, 0, arr.size() - 1);
// Print the sorted array
std::cout << "Sorted array:" << std::endl;
for (size_t i = 0; i < arr.size(); ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0;
}
2.2.2.2 并行归并算法实现
虽然上述代码实现了基础归并排序算法,但无法通过并行有效利用资源,接下来,我对代码进行优化,下面的这段代码实现了一个并行归并排序算法,使用了C++/SYCL来利用异构并行计算的特点,特别是在GPU上进行并行处理。
- 分割数组并分配给线程块
在这个代码中,并没有直接体现将待排序数组分割成多个小的子数组并分配给不同的线程块。实际上,这是因为并行性是在更细粒度上实现的。在mergeSort_parallel
函数中,通过递归地调用自身,实际上在分割数组。每次递归调用处理数组的一半,直到每个子数组足够小,可以被单独一个线程或线程块处理。这种递归分割是归并排序的典型特征。
- 线程块内部线程的协作
这个代码中的关键并行部分是merge_parallel
函数。在这个函数里,使用了SYCL的parallel_for
来创建并行的执行单元。每个执行单元负责将两个小的有序数组(来自左半部分和右半部分)合并到一起。
L_buf
和R_buf
分别表示左右两个子数组,它们被加载到了SYCL的buffer中,这些buffer在设备上可用。parallel_for
通过range<1>(right - left + 1)
创建了足够多的执行单元,每个单元负责合并操作的一部分。- 这里的并行性在于同时对多个元素进行合并操作,而非传统的逐个元素处理。
- 合并有序子数组
归并排序的关键步骤是合并有序子数组。在merge_parallel
函数中,通过parallel_for
实现了这一步骤的并行化。每个执行单元独立地从两个子数组中取出元素,比较它们,然后按顺序放入最终的数组中。这个过程中,不同的执行单元可能会处理相邻的数据段,但由于归并排序的特性,它们不会相互干扰。
- 共享内存的使用和线程间同步
这段代码中,并没有直接使用类似CUDA中的共享内存(shared memory)。SYCL的buffer
和accessor
抽象可能会在底层使用类似的机制来优化内存访问,但这是透明的。在这种情况下,主要的性能考虑是确保内存访问尽可能高效,而这通常是由SYCL运行时和硬件驱动自动处理的。
同步方面,由于每次合并操作都是独立的,线程之间的直接同步需求被最小化。在merge_parallel
函数的结束部分,q.wait()
确保了所有并行操作完成后,程序才会继续执行,这是保证数据一致性的关键。
接下来分段展示最终的代码:
- 引入库
%%writefile lab/my_sort.cpp
#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <sstream>
#include <algorithm>
using namespace sycl;
- Parallel merge function
void merge_parallel(queue& q, std::vector<float>& arr, size_t left, size_t middle, size_t right) {
size_t n1 = middle - left + 1;
size_t n2 = right - middle;
// Allocate memory for temporary arrays on the device
buffer<float, 1> L_buf(arr.data() + left, range<1>(n1));
buffer<float, 1> R_buf(arr.data() + middle + 1, range<1>(n2));
buffer<float, 1> arr_buf(arr.data(), range<1>(arr.size()));
// Perform parallel merge
q.submit([&](handler& h) {
auto L = L_buf.get_access<access::mode::read>(h);
auto R = R_buf.get_access<access::mode::read>(h);
auto A = arr_buf.get_access<access::mode::write>(h);
h.parallel_for(range<1>(right - left + 1), [=](id<1> idx) {
size_t index = left + idx[0];
size_t i = idx[0] < n1 ? idx[0] : n1;
size_t j = idx[0] < n1 ? 0 : idx[0] - n1;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
A[index++] = L[i++];
} else {
A[index++] = R[j++];
}
}
while (i < n1) {
A[index++] = L[i++];
}
while (j < n2) {
A[index++] = R[j++];
}
});
});
q.wait();
}
- Parallel merge sort function
void mergeSort_parallel(queue& q, std::vector<float>& arr, size_t left, size_t right) {
if (left < right) {
size_t middle = left + (right - left) / 2;
// Recursively sort halves in parallel
mergeSort_parallel(q, arr, left, middle);
mergeSort_parallel(q, arr, middle + 1, right);
// Merge the sorted halves
merge_parallel(q, arr, left, middle, right);
}
}
- 主函数
int main() {
std::vector<float> arr;
std::ifstream file("problem-2.txt");
std::string line;
if (file.is_open()) {
getline(file, line);
file.close();
} else {
std::cerr << "Unable to open file" << std::endl;
return 1;
}
std::istringstream iss(line);
float number;
while (iss >> number) {
arr.push_back(number);
}
// Choose the device
device selected_device;
try {
selected_device = gpu_selector{
}.select_device();
std::cout << "Using GPU." << std::endl;
} catch (const sycl::exception& e) {
std::cerr << "GPU not available. Using CPU instead." << std::endl;
selected_device = cpu_selector{
}.select_device();
std::cout << "Using CPU." << std::endl;
}
// Create a SYCL queue
queue q(selected_device);
// Print the unsorted array
std::cout << "Unsorted array:" << std::endl;
for (const auto& e : arr) {
std::cout << e << " ";
}
std::cout << std::endl;
// Call parallel merge sort
mergeSort_parallel(q, arr, 0, arr.size() - 1);
// Print the sorted array
std::cout << "Sorted array:" << std::endl;
for (const auto& e : arr) {
std::cout << e << " ";
}
std::cout << std::endl;
return 0;
}
3 运行结果
编写脚本运行上述代码:
#!/bin/bash
source /opt/intel/inteloneapi/setvars.sh > /dev/null 2>&1
# Command Line Arguments
src="lab/"
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/intel/oneapi/compiler/latest/linux/lib
echo ====================
echo my_merge_sort
dpcpp ${src}my_sort.cpp -o ${src}my_sort -w -O3 -lsycl
./${src}my_sort
获得结果如下所示(包括了未排序的矩阵和排序后的矩阵):
整个任务完整的运行时间:
观察到,这段并行归并排序的代码在结果上能够有效实现数组的排序,而在运行效率上,因为利用了异构并行计算的特点,将排序和合并操作分配给多个线程同时执行,以提高排序效率。
4 总结与心得
本次任务是C语言课程与Intel合作的任务。在上一个并行矩阵计算任务中参考了Intel的示例进行学习,而并行归并排序任务则是自己撰写代码实现。一开始我只能基于架构实现基础的归并排序算法,之后通过Intel的课程分享学习了一些并行代码计算的代码编写,开始对基础代码进行修改。最终实现了通过数据的分割和合并以及线程之间的协作进行较有效的并行归并排序。不同于以往的代码编写,在本次合作课程中首次尝试异构并行计算,感受到这对于加快运算速度的魅力,收获颇多。