设计概述:
算法总体如上图。但实现起来有点略微调整,不细述。 C++如何设计一条流水线来进行归并排序?当我第一次看到流水线算法的时候,我认为 这是一个硬件算法,因为有时钟,有输入输出。很容易理解每个处理器应该是一个组合逻辑 电路,然后彼此通过触发器来连接,然后有一个系统时钟来进行同步。那如何用 C++来描述 这个算法呢。( 我认为 用 模拟 比较好 , 因为 这算法 肯定 是 放在 硬件上面 比较 实用 , 流水线 与 一个重要的 资源 ( D 触发器 ) 是 密切 相关的, 而且 对 编程 来说, 流水线 导致 进程 之间的 通信 开销 往往 比较大,还需要 同 步 ,( 流水线的 下一级 要用到 上一级 上一时刻 的 输出 ),软件 语言 描述 可以做一个 逻辑 级别 验证 ) 那么需要 解决 时钟,输入 输出 ,每个处理器 内部 资源 三 个问 题。
时钟
时钟是每个集成电路模块内部的指挥官,如何模拟时钟呢?我采用了一个循环。
将每个 future 看成是一个处理器,每次循环调用每个 future。然后再循环结尾将它们.get()。 ( 同步 )。 这里需要注意的是触发器如何模拟。
如图我用了二维数组,第一个下标代表是第几个处理器。第二个下标代表它在 CLK 时刻的 输出。
注意的是第 0 个处理器(缓冲区大小为 0),它的输出一开始就是可以知道的,所以我 用了 O(n)的时间初始化。不像课件上的初始化第 0 个处理器。 另外处理器 1 及以后的处理器我花了(O(nlogn) )的时间初始化为无效值,为什么还 要初始化它们呢( 因为 实际 硬件中 ,任何时刻都 是 有 输入 输出的,不能 因为 流水线 没流 到 那 一级, 那一级 处理器 就没有 输入 输出 )。 ( 所以 我的 整个 程序 时间复杂度 是 O ( n lo gn ), 其实 这一步 初始化 可以并行 去做 , 考虑 到设计 周期 及其 重要性 , 没有做 )
那还有一个问题触发器如何模拟呢? 当前处理器的输入,对于上一个处理器上一时刻的输出 这里有一个 起始 问题 ,就是 0 时刻 输入 。 -1 时刻是不存在的,是没有输出的。所以第 0 时刻, 只有处理器 1(缓冲区大小为 1)得到一个输出。 从第一个时刻开始,后面的处理器就可以得到前一个时刻处理器的输出。所以循环是这么写 的。
输入输出
在我的算法里面,输入输出是有两个通道的上和下。除此之外,输入输出还应该有一个 属性是否有效的,当数据还没有流到当前处理器或者已经流出当前处理器就应该有一个标志 标识输入输出是否有效。所以我设计了如下的数据结构。
那输入输出如何和 future 关联起来呢。一个 future 就是一个处理器。那么很自然的想到, 如下方 案 。
输入作为一个形参,输出作为一个返回值
处理器内部资源
这一部分是最难的。其实这一部分我至今觉得我的实现不够高效。 这里涉及的 一个 问题 如何让一个 每个 线 程 单独 拥有 静态变量 。 我刚开始想在 lambda 表达式里直接加 static,后 来发现多个线程共享这个全局变量。后来采用的策略是统一从主线程传引用,同时传处理器 下标。线程通过传来的下标,访问自己对应的资源。感觉这样通信特别不高效。 抛开通信效率不谈。我们考虑下如何每个处理器需要什么。 首先要有输入缓冲区,接受上个处理器输入的数据。
还要有输出队列,用于存放等待输出的数据。 输入 数据 和 输出 数据 是 有 时 延 的 ,比如 输 入 缓冲区 大小 如果 为 4 的 话 ,那么 输 入 第一个数据 和输出 第一个 数据 就有一个 4 的 时间差, 这一部分 我不知 道 高效的 实现 能否 省略 掉。
输入输出控制
这里涉及到三个数组。前面两个数组是用来控制第三个数组的。我根据已经接受到的数据个 数,来调整是否输出(必须上面那个缓冲区满了才输出),已经输出的数据个数来调整输出 的方向(上面输出满了,就切换到下面) 。 当然 输入 输 出 这两个 参数 是 具有 周期性 的。
算法理论分析
抛开我初始化无效输入输出(nlogn)。 下面讨论流水线过程的理论性能。 在 我 的 程 序 里 , 处理器个数一 共 需要
,因为在我的程序里处理器只有 归并功能,举例大小为 8 的数组,需要 3 个处理器器。第一个归并规模为 1 的,第二个归并 规模为 2 的,第三个归并规模为 4 的。 在我的程序里,0 时刻第一个数据输入处理器 0。 处理器 1(缓冲区大小为 1)输出第一个数据的距离输入处理器第一个数据时差为 1。 处理器 2(缓冲区大小为 2)输出第一个数据的距离输入处理器第一个数据时差为 2。 处理器 3(缓冲区大小为 4)输出第一个数据的距离输入处理器第一个数据时差为 4。 那么从处理器 1 输出第一个数据,到第二个处理器输入第一个数据还有个时间差为 1。 (触发器)。 (假设有 N 个处理器,中间就有 N-1 个处理器) 那么总用时为处理器 1 输入第一个数据,到最后一个处理器输出最后一个数据。共需要 周期数可由如下公式:
公式中 是所有处理器缓冲区的大小之和。
总的运算量
抛开前面的赋值操作,所花费时间大部分用于比较上了。 再抛开额外的比较开销(例如控制输出是向上还是向下及输出是否有效等),总的运算 量和归并排序是一样的。
流水线演示
为方便 演示 , 我所 给出的 结果是 用的 ( 我改成 用 ) 串行 的 方式, 即 处理器 1 先 工作, 再 处理 器 2 工作 …… 。 实际上 处理器 之际 彼此 是没有 竞争 的 , 在 异步方式 下 处理器 工作 次序 是 非顺 序 的, 只是 在 每个 C LK 开启 调用 处理器 函数, 然后 每个 C LK 结束 等待 最后 结束 的 那个 线程。 假设待归并排序序列为{4,3,2,1}:
可看到正如我们得 公式
-1+2+3-1+4=7 在第七个时刻最后一个处理器处理完毕。
假设待归并排序序列为{5,4,3,2,1}
可以看到多了一个处理器,并且正如我们的代入公式
得-1+3+7-1+5=13 时刻处理器处理完毕。
代码:
// Merge_sort.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <iostream>
#include <thread>
#include "windows.h"
#include <vector>
#include <string>
#include <random>
#include <future>
#include <iterator>
#include <queue>
using namespace std;
template <typename T>
class Merge_sort
{
private:
const int serial_pattern = 0;
const int pipeline_pattern = 1;
void serial_sort(std::vector<T> &p);
void pipeline_sort(std::vector<T> &p);
public:
Merge_sort(std::vector<T> &p, int pattern = serial_pattern);
~Merge_sort(){};
};
template <typename T>
Merge_sort<T>::Merge_sort(std::vector<T> &p, int pattern)
{
if (pattern == pipeline_pattern)
pipeline_sort(p);
else
serial_sort(p);
}
template <typename T>
void Merge_sort<T>::serial_sort(std::vector<T> &p)
{
std::sort(p.begin(), p.end());
}
template <typename T>
void Merge_sort<T>::pipeline_sort(std::vector<T> &p)
{
const bool flag_up = true, flag_down = false;
struct flow_data
{
T value;
bool is_empty;
bool flag;
};
struct buff
{
std::vector<flow_data> up_buff;
std::vector<flow_data> down_buff;
int buff_size;
};
auto meidium_process = [](int process_index, flow_data input, std::vector<int> &receive_number, std::vector<int> &send_number,
vector<bool> &flag_control, vector<buff> &buff_p, vector<std::queue<flow_data>> &output_buff) -> flow_data {
const bool flag_up = true, flag_down = false;
const flow_data flow_empty = {-1, true, true};
flow_data output;
/* std::cout << process_index << "号处理器"
<< "input:";
if (!input.is_empty)
{
std::cout << ',' << input.value << " ";
if (input.flag)
std::cout << "up";
else
std::cout << "down";
}
else
std::cout << "invalid";
std::cout << std::endl; */
if (input.is_empty)
{
if (!buff_p[process_index].up_buff.empty() && !buff_p[process_index].down_buff.empty())
{
if (buff_p[process_index].up_buff.begin()->value < (buff_p[process_index].down_buff.begin())->value)
{
output_buff[process_index].push(*buff_p[process_index].up_buff.begin());
buff_p[process_index].up_buff.erase(buff_p[process_index].up_buff.begin());
}
else
{
output_buff[process_index].push(*buff_p[process_index].down_buff.begin());
buff_p[process_index].down_buff.erase(buff_p[process_index].down_buff.begin());
}
}
else if (!buff_p[process_index].up_buff.empty() && buff_p[process_index].down_buff.empty())
{
output_buff[process_index].push(*buff_p[process_index].up_buff.begin());
buff_p[process_index].up_buff.erase(buff_p[process_index].up_buff.begin());
}
else if (buff_p[process_index].up_buff.empty() && !buff_p[process_index].down_buff.empty())
{
output_buff[process_index].push(*buff_p[process_index].down_buff.begin());
buff_p[process_index].down_buff.erase(buff_p[process_index].down_buff.begin());
}
}
else if (!input.is_empty)
{
if (input.flag == flag_up)
{
++receive_number[process_index];
buff_p[process_index].up_buff.push_back(input);
}
else
{
++receive_number[process_index];
buff_p[process_index].down_buff.push_back(input);
}
}
if (receive_number[process_index] > buff_p[process_index].buff_size || send_number[process_index] > 0)
{
if (!buff_p[process_index].up_buff.empty() && !buff_p[process_index].down_buff.empty())
{
if (buff_p[process_index].up_buff.begin()->value < (buff_p[process_index].down_buff.begin())->value)
{
output_buff[process_index].push(*buff_p[process_index].up_buff.begin());
buff_p[process_index].up_buff.erase(buff_p[process_index].up_buff.begin());
}
else
{
output_buff[process_index].push(*buff_p[process_index].down_buff.begin());
buff_p[process_index].down_buff.erase(buff_p[process_index].down_buff.begin());
}
}
else if (!buff_p[process_index].up_buff.empty() && buff_p[process_index].down_buff.empty())
{
output_buff[process_index].push(*buff_p[process_index].up_buff.begin());
buff_p[process_index].up_buff.erase(buff_p[process_index].up_buff.begin());
}
else if (buff_p[process_index].up_buff.empty() && !buff_p[process_index].down_buff.empty())
{
output_buff[process_index].push(*buff_p[process_index].down_buff.begin());
buff_p[process_index].down_buff.erase(buff_p[process_index].down_buff.begin());
}
}
if (receive_number[process_index] == 2 * buff_p[process_index].buff_size)
{
receive_number[process_index] = 0;
}
if (!output_buff[process_index].empty())
{
output = output_buff[process_index].front();
output_buff[process_index].pop();
output.flag = flag_control[process_index];
++send_number[process_index];
if (send_number[process_index] == 2 * buff_p[process_index].buff_size)
{
send_number[process_index] = 0;
flag_control[process_index] = !flag_control[process_index];
}
}
else
output = flow_empty;
/*std::cout << process_index << "号处理器"
<< "output:";
if (!output.is_empty)
{
std::cout << ',' << output.value;
if (output.flag)
std::cout << "up";
else
std::cout << "down";
}
else
{
std::cout << "invalid";
}
std::cout << std::endl;*/
return output;
};
int process_num = ceil(log2(p.size())); //应该向上取整
int meidium_clk = 1;
for (auto i = 1; i <= process_num; ++i)
{
meidium_clk *= 2;
}
meidium_clk -= 1;
int clk_sum = -1 + ceil(log2(p.size())) + meidium_clk - 1 + p.size();
std::vector<std::future<flow_data>> process_s(process_num + 1);
std::vector<std::vector<flow_data>> flow_trigger(process_num + 1);
std::vector<bool> flag_control(process_num + 1);
for (auto it = flag_control.begin();it!=flag_control.end();++it)
*it = flag_up;
std::vector<std::queue<flow_data>> output_buff(process_num + 1);
std::vector<int> receive_number(process_num + 1, 0);
std::vector<int> send_number(process_num + 1, 0);
for (int i = 0; i < process_num + 1; ++i)
flow_trigger[i].resize(clk_sum + 1);
flow_trigger[0][0].value = p[0];
flow_trigger[0][0].is_empty = false;
flow_trigger[0][0].flag = true;
for (auto i = 1; i <= clk_sum; ++i)
{
if (i < p.size())
{
flow_trigger[0][i].value = p[i];
flow_trigger[0][i].is_empty = false;
flow_trigger[0][i].flag = !flow_trigger[0][i - 1].flag;
}
else
{
flow_trigger[0][i].value = -1;
flow_trigger[0][i].is_empty = true;
flow_trigger[0][i].flag = false;
}
}
for (int i = 1; i < process_num + 1; ++i)
{
for (int j = 0; j < clk_sum; ++j)
{
flow_trigger[i][j].value = -1;
flow_trigger[i][j].is_empty = true;
flow_trigger[i][j].flag = false;
}
}
vector<buff> buff_p(process_num + 1);
buff_p[1].buff_size = 1;
for (auto it = buff_p.begin() + 2; it != buff_p.end(); ++it)
it->buff_size = (it - 1)->buff_size * 2;
/* std::cout << "时刻0" << std::endl;*/
std::future<flow_data> f1 = (std::async(std::launch::async, meidium_process, 1, flow_trigger[0][0], ref(receive_number),
ref(send_number), ref(flag_control), ref(buff_p), ref(output_buff)));
flow_trigger[1][0] = f1.get();
for (int clk = 1; clk <= clk_sum; ++clk)
{
//std::cout << "时刻" << clk << std::endl;
process_s[1] = (std::async(std::launch::async, meidium_process, 1, flow_trigger[0][clk], ref(receive_number), ref(send_number),
ref(flag_control), ref(buff_p), ref(output_buff)));
// flow_trigger[1][clk] = process_s[1].get(); //处理器1直接接受的是CLK时刻的输入,它前面已经没有触发器了
for (int i = 2; i <= process_num; ++i)
{
process_s[i] = (std::async(std::launch::async, meidium_process, i, flow_trigger[i - 1][clk - 1], ref(receive_number),
ref(send_number), ref(flag_control), ref(buff_p), ref(output_buff)));
//flow_trigger[i][clk] = process_s[i].get();
}
for (int i = 1; i <= process_num; ++i)
flow_trigger[i][clk] = process_s[i].get();
//非顺序版本,其实异步本应该并行,实测结果正确,但为方便演示注释掉了
}
for (int i = clk_sum - p.size() + 1; i <= clk_sum; ++i)
p[i - (clk_sum - p.size() + 1)] = flow_trigger[process_num][i].value;
/* for (auto const &t : p)
std::cout << t << endl; */
}
void random_vector(std::vector<double> &p)
{
std::random_device rd; // 将用于为随机数引擎获得种子
std::mt19937 gen(rd()); // 以播种标准 mersenne_twister_engine
std::uniform_real_distribution<double> dis;
for (int i = 0; i < p.size(); ++i)
p[i] = dis(gen);
}
int main()
{
int qqq = (-9 / 4);
int qqqq = (-9 % 4);
std::cout << qqq << qqqq;
const int a_size = 1;
std::cout << "所测数据(double)大小:"<<a_size<<std::endl;
int star, end;
std::vector<double> a(a_size);
std::vector<double> b(a_size);
random_vector(a);
for (int i = 0; i <= a_size; ++i)
{
b[i]=a[i];
}
star = clock();
sort(b.begin(), b.end());
end = clock();
std::cout << "标准库排序用时:" << end - star << std::endl;
star = clock();
Merge_sort<double> p(a, 1);
end = clock();
std::cout << "流水线并行算法用时:" << end - star << std::endl;
}
// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单
// 入门提示:
// 1. 使用解决方案资源管理器窗口添加/管理文件
// 2. 使用团队资源管理器窗口连接到源代码管理
// 3. 使用输出窗口查看生成输出和其他消息
// 4. 使用错误列表窗口查看错误
// 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
// 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件