学习自Parallel Programming for FPGAs(the HLS Book)
插入排序
大伙应该都很了解了,就是将一个新的元素插入到一个有序数组中,并继续保持有序。每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
现在来考虑它的优化问题,基本代码为
#include "insertion_sort.h"
void insertion_sort(DTYPE A[SIZE]) {
L1:
for(int i = 1; i < SIZE; i++) {
DTYPE item = A[i];
int j = i;
DTYPE t = A[j-1];
L2:
while(j > 0 && A[j−1] > item ) {
#pragma HLS pipeline II=1
A[j] = A[j−1];
j−−;
}
A[j] = item;
}
}
最简单的优化策略是使用pipeline指令,使其内部循环L2支持流水功能。
由于数据没有相关性,设置期望流水线启动间隔为1,即(#pragma HLS pipeline II=1)
并行化插入排序
目标: 每个时钟周期插入一个新元素
#include "insertion_sort_parallel.h"
#include "assert.h"
void insertion_sort_parallel(DTYPE A[SIZE], DTYPE B[SIZE]) {
#pragma HLS array_partition variable=B complete
L1:
for(int i = 0; i < SIZE; i++) {
#pragma HLS pipeline II=1
DTYPE item = A[i];
L2:
for(int j = SIZE−1; j >= 0; j−−) {
DTYPE t;
if(j > i) {
t = B[j];
} else if(j > 0 && B[j−1] > item) {
t = B[j−1];
} else {
t = item;
if (j > 0)
item = B[j−1];
}
B[j] = t;
}
}
}
本质上生成了多组内部循环体,这个内部循环体的结构主要是由几个多路复用器、一个决定谁最小的比较器和1个存储数组元素的寄存器等组成。当然,每个阶段还可能包括缓冲寄存器,以确保生成的电路逻辑在一个有效的时钟频率。若把内部循环体L2看为排序单元,那么插入排序函数即是由一个一维数组的排序单元和一些在输入输出接口层的额外逻辑组成,在这种情况下, 外部迭代体仅需要SIZE个时钟周期就可以处理完。这个排序单元的主要特性是每个排序单元只与相邻的排序单元通信,而不是所有的单元。像这样的设计被称为脉动阵列,是一种常见的并行算法优化的技术。在很多情况下,只要在不同的循环之间的通信是有限的,当展开内部循环的优化,包括排序和脉动阵列的实现,都可以称为隐式脉动阵列。
这块其实我没看懂啊,太草了
显示脉动阵列插入排序
每个单元都是相同的,每个单元的输入端口in用来接收当前寄存器中的值,较小的值发送到输出端口out, 而较大的值存放在本地寄存器local中, 其实每个单元的功能也就是out = min(in, local)。第i号单元的输出结果传递给线性阵列的下一个(即第i + 1号)单元的输入,当新的输入到来时,会与存储在阵列中的本数组进行比较,直到找到正确的位置。如果新的输入大于阵列中的所有值,排序后的值将向右移动一个单元阵列;如果新的输入小于阵列中的所有值,此值会在阵列中传输,最终会被存放在最右边的阵列单元中。当所有的数据都处理完时,最小的元素存放在第N − 1个阵列单元,直接从输出读出即可。
其中一个单元的代码为
void cell0(hls::stream<DTYPE> & in, hls::stream<DTYPE> & out)
{
static DTYPE local = 0;
DTYPE in copy = in.read();
if(in copy > local) {
out.write(local);
local = in.copy;
}
else
{
out.write(in copy);
}
}
输入和输出变量都声明为HLS的数据流类型。DTYPE是一个类型参数,允许对不同类型进行操作。本地变量存放阵列中的某一个元素,添加了static关键字是为了在多个函数调用中保存它的值。这里需要注意的是使用相同的函数,复制单元功能N次;每个单元必须有一个单独的单元静态变量,一个静态变量不能跨N个函数共享。
归并排序
归并排序算法的基本思想是将两个有序序列合并成一个较大的有序序列,可以在O(N)时间内完成。
核心思想是分治,学过的朋友应该都很熟悉了。
将两个排序数组组合成一个较大的排序数组的过程通常称为two-finger算法,two-finger操作初始化时指向每个数组第一个元素(数值in1中的元素3和数组in2中的元素1),在算法执行的过程中,两个指针会指向数组中的不同元素。
基本操作
处理数组排序大约需要NlogN次比较,以及额外的临时存储空间。
#include "merge_sort.h"
#include "assert.h"
// subarray1 is in[i1..i2-1], subarray2 is in[i2..i3-1], result is in out[i1..i3-1]
void merge(DTYPE in[SIZE], int i1, int i2, int i3, DTYPE out[SIZE]) {
int f1 = i1, f2 = i2;
// Foreach element that needs to be sorted...
for(int index = i1; index < i3; index++) {
// Select the smallest available element.
if((f1 < i2 && in[f1] <= in[f2]) || f2 == i3) {
out[index] = in[f1];
f1++;
} else {
assert(f2 < i3);
out[index] = in[f2];
f2++;
}
}
}
void merge_sort(DTYPE A[SIZE]) {
DTYPE temp[SIZE];
// Each time through the loop, we try to merge sorted subarrays of width elements
// into a sorted subarray of 2*width elements.
stage:
for (int width = 1; width < SIZE; width = 2 * width) {
merge_arrays:
for (int i1 = 0; i1 < SIZE; i1 = i1 + 2 * width) {
// Try to merge two sorted subarrays:
// A[i1..i1+width-1] and A[i1+width..i1+2*width-1] to temp[i1..2*width-1]
int i2 = i1 + width;
int i3 = i1 + 2*width;
if(i2 >= SIZE) i2 = SIZE;
if(i3 >= SIZE) i3 = SIZE;
merge(A, i1, i2, i3, temp);
}
// Copy temp[] back to A[] for next iteration
copy:
for(int i = 0; i < SIZE; i++) {
A[i] = temp[i];
}
}
}
代码里首先考虑数组中的每个元素作为一个排序子数组,外部循环的每次迭代都将排序子数组合并成较大的排序子数组。第一次迭代,对最大SIZE为2的子数组进行排序;第二次迭代,对最大SIZE为4的子数组进行排序;然后是8,以此类推。