大数据与算法——累加求和、求最大值、排序加速
大数据与算法作业介绍及加速方法解析
1.作业介绍
1.1 作业要求
利用多种加速方式,实现对64*2000000个数据的累加求和、求最大值、排序三种算法的加速,采用两台计算机实现分布式计算,并通过Socket实现通信,尽可能提高加速比,加速手段可包括:多线程、多进程、SSE指令集、Openmp加速、Cuda显卡加速,以及拓展部分:实现非windows系统的算法加速。
其中关键的要求在于
1.数据量:64*2000000(可根据电脑性能自行减少,满足加速关系即可);
2.加速算法:累加求和、求最大值、排序三种算法;
3.通信方式:Socket(由于是双机加速,即为C/S模型;且由于UDP通信无法保证可靠性,故选择TCP通信,保证数据按时准确的传输)、RPC(使用此方法时会下降分数)
4.加速方式:加速方法不限,老师提供了以下几种:
多线程、多进程、SSE指令集、Openmp、Cuda加速;
5.扩展:使用非windows系统实现加速(我们选择了Linux下的Ubuntu系统)
1.2 概念解析
1.2.1 算法
累加求和
针对于累加则没有太多的算法上的研究,对于拥有强大计算能力的计算机来说,用循环累加即可,这里给出不加速版本的累加代码如下:
int mysum(const float data[], const int len, float &result)
//data是原始数据,len为长度。结果通过函数返回
{
double localresult = 0.0f;
for (size_t i = 0; i < len; i++)
{
localresult += log(sqrt(rawFloatData[i] / 4.0)); //有意浪费时间
}
result = localresult;
return 0;
}
这里使用localresult作为中间变量,是由于float变量在进行大数加法时,会由于误差无法将一个较小的数加进去,故选择用double类型作为中间量,最后再转回float
求最大值
由于数据是随机生成的,并无顺序可言,故求最大值也不存在什么加速算法,不加速版本如下:
int mymax(const float data[], const int len, float &result)
//data是原始数据,len为长度。结果通过函数返回
{
double localresult = 0.0f;
for (size_t i = 0; i < len; i++)
{
if (localresult <= log(sqrt(data[i] / 4.0)))
localresult = log(sqrt(data[i] / 4.0)); //有意浪费时间
}
result = localresult;
return 0;
}
排序
对于无序数组的排序算法则有很多,我们熟知的就有快速排序、选择排序、冒泡排序等,这里还给出了包括堆排序、归并排序、折半插入排序的C++代码
快速排序(递归实现)
void MyquickSort(int left, int right, float data[])
{
if (left >= right)
return;
int i, j;
float base, temp;
i = left, j = right;
base = data[left] ; //取最左边的数为基准数
while (i < j)
{
while (data[j] >= base && i < j)
j--;
while (data[i] <= base && i < j)
i++;
if (i < j)
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
//基准数归位
data[left] = data[i];
data[i] = base;
MyquickSort(left, i - 1, data);//递归左边
MyquickSort(i + 1, right, data);//递归右边
}
冒泡排序
void BubbleSort(vector<int> &arr)
{
for (int i = 0; i < arr.size() - 1; i++)
{
bool flag=true;
for (int j = 0; j < arr.size() - i - 1; j++)
{
//经过第1趟可以找出最大的数
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag=false;
}
}
if(flag) return;
}
}
折半插入排序
int low,high,middle;
void BinaryInsertSort(int a[], int len)
{
for(int i=0;i<len;i++)
{
int temp=a[i];
low=0;high=i-1;
while(low<=high)
{
middle=(low+high)/2;
if(temp<a[middle])
high=middle-1;
else
low=middle+1;
}
for(int k=i;k>low;k--)
a[k]=a[k-1];
a[low]=temp;
}
}
归并排序
void MyMerge(float data[], int start, int end)
{
if (start >= end) return;
int mid = (end + start) >> 1;
//分成两部分
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
//然后合并
MyMerge(data, start1, end1);
MyMerge(data, start2, end2);
int k = start;
//两个序列一一比较,哪的序列的元素小就放进reg序列里面,然后位置+1再与另一个序列
//原来位置的元素比较,如此反复,可以把两个有序的序列合并成一个有序的序列
while (start1 <= end1 && start2 <= end2)
reg[k++] = data[start1] < data[start2] ? data[start1++] : data[start2++];
//然后这里是分情况,如果arr2序列的已经全部都放进reg序列了然后跳出了循环
//那就表示arr序列还有更大的元素(一个或多个)没有放进reg序列,所以这一步就是接着放
while (start1 <= end1)
reg[k++] = data[start1++];
//这一步和上面一样
while (start2 <= end2)
reg[k++] = data[start2++];
//把已经有序的reg序列放回arr序列中
for (k = start; k <= end; k++)
data[k] = reg[k];
}
堆排序
void Heapify(float data[], int first, int end)
{
int father = first;
int son = father * 2 + 1;
while (son < end) {
if (son + 1 < end && data[son] < data[son + 1]) ++son;
//如果父节点大于子节点则表示调整完毕
if (data[father] > data[son]) break;
else {
//不然就交换父节点和子节点的元素
float temp = data[father];
data[father] = data[son];
data[son] = temp;
//父和子节点变成下一个要比较的位置
father = son;
son = 2 * father + 1;
}
}
}//调整成堆
void MyHeapSort(float data[], int len) {
int i;
//初始化堆,从最后一个父节点开始
for (i = len / 2 - 1; i >= 0; --i) {
Heapify(data, i, len);
}
//从堆中的取出最大的元素再调整堆
for (i = len - 1; i > 0; --i) {
float temp = data[i];
data[i] = data[0];
data[0] = temp;
Heapify(data, 0, i);//调整成堆
}
}
其余排序方法如桶排序、希尔排序等等,由于不稳定及适用于不同环境,这里不做描述;而由于数据量过大,冒泡排序、折半插入排序等基础排序算法在有限时间内难以做出结果,故实际中只对快速排序、堆排序、归并排序做了分析
1.2.2 加速手段(具体实现代码可见下篇)
多线程(多进程)
实现多线程是采用一种并发执行机制 。
并发执行机制原理:简单地说就是把一个处理器划分为若干个短的时间片,每个时间片依次轮流地执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果 。
多线程就是把操作系统中的这种并发执行机制原理运用在一个程序中,把一个程序划分为若干个子任务,多个子任务并发执行,每一个任务就是一个线程。
针对于此次作业,由于windows系统一个进程的最大线程数量为64个,故只能创建64个进程实现并行加速
SSE指令集
SSE(Streaming SIMD Extensions,单指令多数据流扩展)指令集是Intel在Pentium III处理器中率先推出,SSE指令集包括了70条指令,其中包含提高3D图形运算效率的50条SIMD(单指令多数据技术)浮点运算指令、12条MMX 整数运算增强指令、8条优化内存中连续数据块传输指令。理论上这些指令对流行的图像处理、浮点运算、3D运算、视频处理、音频处理等诸多多媒体应用起到全面强化的作用。
由于SSE指令集难以对排序算法加速,作业中最终只实现了求和和求最值的加速
OpenMP加速
OpenMP是由OpenMP Architecture Review Board牵头提出的,并已被广泛接受,用于共享内存并行系统的多处理器程序设计的一套指导性编译处理方案(Compiler Directive) 。OpenMP支持的编程语言包括C、C++和Fortran;而支持OpenMp的编译器包括Sun Compiler,GNU Compiler和Intel Compiler等。OpenMp提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。当选择忽略这些pragma,或者编译器不支持OpenMp时,程序又可退化为通常的程序(一般为串行),代码仍然可以正常运作,只是不能利用多线程来加速程序执行。
OpenMP加速使用起来较为简单,但需要在特定环境中
Cuda加速
CUDA(Compute Unified Device Architecture),是显卡厂商NVIDIA推出的运算平台。 CUDA™是一种由NVIDIA推出的通用并行计算架构,该架构使GPU能够解决复杂的计算问题。 它包含了CUDA指令集架构(ISA)以及GPU内部的并行计算引擎。 开发人员可以使用C语言来为CUDA™架构编写程序,C语言是应用最广泛的一种高级编程语言。所编写出的程序可以在支持CUDA™的处理器上以超高性能运行。CUDA3.0已经开始支持C++和FORTRAN。
此方面代码是由小组另一位成员完成,了解不够深入,故不做多介绍
1.2.3 扩展
ubuntu介绍
Ubuntu Linux是由南非人马克·沙特尔沃思(Mark Shuttleworth)创办的基于Debian Linux的操作系统,于2004年10月公布Ubuntu的第一个版本(Ubuntu 4.10“Warty Warthog”)。Ubuntu适用于笔记本电脑、桌面电脑和服务器,特别是为桌面用户提供尽善尽美的使用体验。Ubuntu几乎包含了所有常用的应用软件:文字处理、电子邮件、软件开发工具和Web服务等。用户下载、使用、分享Ubuntu系统,以及获得技术支持与服务,无需支付任何许可费用。
Ubuntu提供了一个健壮、功能丰富的计算环境,既适合家庭使用又适用于商业环境。Ubuntu社区承诺每6个月发布一个新版本,以提供最新最强大的软件。
2.初始化代码
2.1初始化数据
//测试数据量和计算内容为:
#define MAX_THREADS 64
#define SUBDATANUM 2000000
#define DATANUM (SUBDATANUM * MAX_THREADS) /*这个数值是总数据量*/
__declspec(align(16)) float rawFloatData[DATANUM];
//数据初始化
void initial()
{
for (size_t i = 0; i < DATANUM; i++)
{
rawFloatData[i] = float((rand() + rand() + rand() + rand()));//rand返回short数据类型,增加随机性
}
}
__declspec(align(16)) 是16字节对齐,与后SSE指令集中实现并行操作统一,如果非SSE指令集加速,可直接去掉
2.2建立通信连接
server端
printf("TCP服务器启动中...\n");
//
//连接所需变量
SOCKET sClient;
WSADATA wsd;
SOCKET sServer;
SOCKADDR_IN servAddr;
//建立连接准备
WSAStartup(MAKEWORD(2, 2), &wsd); //初始化Socket环境
sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建server端的Socket(TCP)
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(9990);
addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//设置服务器监听地址为任意本地地址,设置端口号为9990
bind(sServer, (const struct sockaddr*)&addrServ, sizeof(SOCKADDR_IN));
//绑定SocketServer到本地地址
listen(sServer, 3); //设置等待序列最大为3的监听socket函数
printf("TCP服务器启动成功!\n");
sockaddr_in addrClient;
int addrClientlen = sizeof(addrClient);
/
//开始连接
sClient = accept(sServer, (sockaddr FAR*) & addrClient, &addrClientlen);
//生成子socket为监听到的第一个访问者的socket
ZeroMemory(buf, BUF_SIZE);
const char buf1[] = "您是辅机!";
strcpy_s(buf, buf1);
retVal = send(sClient, buf, BUF_SIZE, 0);
printf("服务器连接到客户机!\n");
//
client端
//socket连接初始化
///
WSADATA wsd;
SOCKET sHost;
SOCKADDR_IN servAddr;
int retVal; //声明程序所需的变量
WSAStartup(MAKEWORD(2, 2), &wsd); //初始化Socket环境
sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建client端的Socket
printf("请输入server端的ip地址以连接!\n");
char ip[20]; //存放点分十进制IP地址
cin >> ip; //键盘输入server端的ip地址
struct in_addr s; // IPv4地址结构体
inet_pton(AF_INET, ip, (void*)&s);
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = s.s_addr;
servAddr.sin_port = htons(9990);
int sServerAddlen = sizeof(servAddr);
//设置服务器socket地址
retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));
if (retVal == SOCKET_ERROR)
{
printf("connect failed!\n");
closesocket(sHost);
WSACleanup();
}
ZeroMemory(buf, BUF_SIZE);
retVal = recv(sHost, buf, sizeof(buf), 0);
cout << buf << endl;