转自:AIfred

事实证明外排序的效率主要依赖于磁盘,归并阶段采用K路归并可以显著减少IO量,最小堆并行k路归并,效率倍增。

二路归并的思路会导致非常多冗余的磁盘访问,两组两组合并确定的是当前的相对位置并不能一次确定最终的位置。

K路归并,每一轮归并直接确定的是最终的位置,不用重复访问,减少IO。该排序算法需要对每个整数做2次磁盘读和2次磁盘写。

摘自维基百科:

外排序的一个例子是外归并排序(External merge sort),它读入一些能放在内存内的数据量,在内存中排序后输出为一个顺串(即是内部数据有序的临时文件),处理完所有的数据后再进行归并。比如,要对900MB 的数据进行排序,但机器上只有100 MB的可用内存时,外归并排序按如下方法操作:

  1. 读入100 MB的数据至内存中,用某种常规方式(如快速排序堆排序归并排序等方法)在内存中完成排序。
  2. 将排序完成的数据写入磁盘。
  3. 重复步骤1和2直到所有的数据都存入了不同的100 MB的块(临时文件)中。在这个例子中,有900 MB数据,单个临时文件大小为100 MB,所以会产生9个临时文件。
  4. 读入每个临时文件(顺串)的前10 MB( = 100 MB / (9块 + 1))的数据放入内存中的输入缓冲区,最后的10 MB作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得更好的效果。)
  5. 执行九路归并算法,将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写出至目标文件,清空缓冲区。一旦9个输入缓冲区中的一个变空,就从这个缓冲区关联的文件,读入下一个10M数据,除非这个文件已读完。这是“外归并排序”能在主存外完成排序的关键步骤 -- 因为“归并算法”(merge algorithm)对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。

算法思路:

1. 二分文件位置,选取每一个文件的枢轴,将每一个文件划分为thread个片段,使得每一个thread处理所有文件片段和相对均衡。

2. 然后用每一个线程各自处理属于他们的K个文件片段,规模为K的最小堆维护K路归并,构造一个大小为k的堆,先将k个节点的头元素插入到堆中,然后每次取出头结点,取出来的元素属于哪个子数组,再添加这个子数组的下一个元素进入堆中,来维护这个堆。这里的排序结果也是最后的排序结果,直接输出到文件。多线程并行处理。

用到知识点:

1. 数据并行拆分: (partition_and_sort)

  • 切分的大小符合内存大小限制。
  • 禁止拆分数据线程间的依赖。
  • 汇总时处理并发冲突,原子操作。

2. k路归并堆排序:(heapsort)

构造一个大小为k的最小二叉堆,先将k个节点的头元素插入到堆中,然后每次取出头结点,取出来的元素属于哪个子数组,再添加这个子数组的下一个元素进入堆中,来维护这个堆。这里的排序结果也是最后的排序结果,减少IO操作

3. 缓冲区(buffer)

如果将每个节点的最小值放入内存,例如2,1,3,4放入内存,但是把最小值1拿掉之后需要补充一个元素,将外部内存的2拿到内存里来,可是外部内存可能在硬盘或网络,此过程相比内存操作会慢很多,不断读取外部内存效率很低,所以采用缓冲区,每次读取k个节点前部分数据到内存缓冲区(几k或几M)。

 #include <stdio.h>
#include <cstring>
#include <string>
#include <atomic>
#include <queue>
#include <vector>
#include <Windows.h>
#include <ppl.h>
#include <functional>
#include <io.h>
#include <time.h>
#define MAX_THREADS 4
#define MAX_K 100
using namespace std;
using namespace concurrency;
const int dx = ;//并行快速排序的dx优化
const long long PARTITION_SIZE = ;
const long long BUFFER_SIZE = ;
const long long EACH_NUM = (PARTITION_SIZE / MAX_THREADS);
int parts, heapsize[MAX_THREADS];
long long data_size;
mutex m;
typedef pair<int, int> node; // (int,文件id)
// 每一个线程维护的最小堆,堆大小是文件数,K路归并
node heap[MAX_THREADS][MAX_K + ]; void parallel_qsort(int *begin, int *end) {//并行快速排序
if (begin >= end - ) return;
int *key = rand() % (end - begin) + begin;
swap(*key, *begin);
int *i = begin, *j = begin;
for (key = begin; j < end; j++) {
if (*j < *key) {
i++;
swap(*i, *j);
}
}
swap(*begin, *i);
if (i - begin > dx && end - i > dx) {//dx优化
parallel_for(, , [&](int x) {
if (x) parallel_qsort(begin, i);
else parallel_qsort(i + , end);
});
} else {
parallel_qsort(begin, i);
parallel_qsort(i + , end);
}
} // 添加新元素,向上找到合适的插入位置
inline void up(int idx, int x) {
int fa = x >> ; node tmp = heap[idx][x];
while (fa) {
if (tmp < heap[idx][fa])//cmp
heap[idx][x] = heap[idx][fa];
else break;
x = fa; fa = x >> ;
}
heap[idx][x] = tmp;
} // 向下找到合适的插入位置
inline void down(int idx, int x) {
int ch = x << ; node tmp = heap[idx][x];
while (ch <= heapsize[idx]) {
if (ch < heapsize[idx] && heap[idx][ch + ] < heap[idx][ch]) ch++;//cmp
if (heap[idx][ch] < tmp)//cmp
heap[idx][x] = heap[idx][ch];
else break;
x = ch; ch = x << ;
}
heap[idx][x] = tmp;
} inline void push(int idx, node val) { // 向最小堆插入元素
heap[idx][++heapsize[idx]] = val;
up(idx, heapsize[idx]);
}
inline node top(int idx) { return heap[idx][]; }
inline void pop(int idx) { // pop堆顶最小元素
heap[idx][] = heap[idx][heapsize[idx]--];
down(idx, );
} inline void ch_size(string file_name, fpos_t size) {
FILE *fout = fopen(file_name.c_str(), "wb");
_chsize_s(fileno(fout), size * sizeof(int));
fclose(fout);
}
inline int seek_dat(FILE* &f, fpos_t pos) {
int *get = new int;
pos *= sizeof(int);
fsetpos(f, &pos);
fread(get, sizeof(int), , f);
int tmp = *get; delete get;
return tmp;
} void partition_and_sort(string in_file) {
int *arr = new int[PARTITION_SIZE];
for (long long i = ; i < (data_size - ) / PARTITION_SIZE + ; i++) {
atomic_int each_get[MAX_THREADS + ] = {};
string tmp_file = "temp\\part" + to_string(i) + ".dat";
clock_t start = clock();
cout << "Reading part " << i << "...";
parallel_for(, MAX_THREADS, [&](long long x) {
FILE* fin = fopen(in_file.c_str(), "rb");
fpos_t pos = (PARTITION_SIZE * i + EACH_NUM * x) * sizeof(int);
if (fsetpos(fin, &pos) == )
each_get[x] = fread(arr + EACH_NUM * x, sizeof(int), EACH_NUM, fin);
each_get[MAX_THREADS] += each_get[x];
fclose(fin);
});
cout << "\rSorting part " << i << "...";
parallel_qsort(arr, arr + each_get[MAX_THREADS]); // 并行快速排序
cout << "\rWriting part " << i << "...";
ch_size(tmp_file, each_get[MAX_THREADS]);
parallel_for(, MAX_THREADS, [&](long long x) {
FILE* fout = fopen(tmp_file.c_str(), "rb+");
fpos_t pos = EACH_NUM * x * sizeof(int);
if (fsetpos(fout, &pos) == )
fwrite(arr + EACH_NUM * x, sizeof(int), each_get[x], fout);
fclose(fout);
});
clock_t end = clock();
cout << "\rPart " << i << " established. Time usage = " << end - start << "ms.\n";
}
delete[] arr;
} void merge_file() {
FILE* fin[MAX_K] = {};
fpos_t size[MAX_K] = {}, seek_pos[MAX_THREADS + ][MAX_K + ] = {};
for (int i = ; i < parts; i++) {
fin[i] = fopen(("temp\\part" + to_string(i) + ".dat").c_str(), "rb");
fseek(fin[i], , SEEK_END);
fgetpos(fin[i], &size[i]);
size[i] /= sizeof(int); // 有多少数
seek_pos[MAX_THREADS][parts] += (seek_pos[MAX_THREADS][i] = size[i]); // seek_pos[线程id][文件id] = 文件位置
}
cout << "\nInitializing merging operation...\n";
for (long long i = ; i < MAX_THREADS; i++) {
fpos_t l0 = , r0 = size[] - ;
while (r0 - l0 > ) { // 二分文件0的位置
seek_pos[i][parts] = seek_pos[i][] = (l0 + r0) / ;
int get0 = seek_dat(fin[], seek_pos[i][]);
for (int idx = ; idx < parts; idx++) {
fpos_t l = , r = size[idx];
while (r - l > ) { // 二分其他文件的位置,找到get0
seek_pos[i][idx] = (l + r) / ;
int get = seek_dat(fin[idx], seek_pos[i][idx]);
if (get0 <= get) r = seek_pos[i][idx];
else l = seek_pos[i][idx] + ;
}
seek_pos[i][parts] += (seek_pos[i][idx] = r);
}
// 二分文件0位置的目的是使得分治的较为均衡,所有文件相对片段长度之和接近于 data_size / MAX_THREADS
if (seek_pos[i][parts] * MAX_THREADS < data_size * i) l0 = seek_pos[i][] + ;
else r0 = seek_pos[i][] - ;
}
}
for (int i = ; i < parts; i++) fclose(fin[i]);
clock_t start = clock(); atomic_llong all_write = ;
parallel_for(, MAX_THREADS, [&](int x) { // 线程处理外循环
FILE *fin[MAX_K] = {}, *fout = fopen("ans.dat", "rb+");
fpos_t fpos = seek_pos[x][parts] * sizeof(int);
fsetpos(fout, &fpos);
int **buf = new int*[MAX_K + ];// 开文件数个buffer,K路归并
for (int i = ; i <= MAX_K; i++) buf[i] = new int[BUFFER_SIZE];
int pos[MAX_K + ] = {};//buffer pos
fpos_t all[MAX_K] = {};
for (int i = ; i < parts; i++) {
fin[i] = fopen(("temp\\part" + to_string(i) + ".dat").c_str(), "rb");
fpos = seek_pos[x][i] * sizeof(int);
fsetpos(fin[i], &fpos);
all[i] = seek_pos[x + ][i] - seek_pos[x][i]; // 记录每一个文件一个线程处理的长度
fread(buf[i], sizeof(int), BUFFER_SIZE, fin[i]);
}
for (int i = ; i < parts; i++) {
// 向最小堆中读入所有文件属于该线程处理的部分的第一个元素
push(x, node(buf[i][], i)); //(线程id,pair(buffer,文件id))
pos[i] = ; all[i]--;
}
while (heapsize[x]) {
// buf[parts]: k路归并排好序的缓冲区
if (pos[parts] == BUFFER_SIZE) {
fwrite(buf[parts], sizeof(int), BUFFER_SIZE, fout);
all_write += BUFFER_SIZE;
if (all_write % == ) {
m.lock();
cout << "\rStart merging... " << (all_write * ) / data_size
<< "% completed.";
m.unlock();
}
pos[parts] = ;
}
int bel = top(x).second;
buf[parts][pos[parts]++] = top(x).first;
if (all[bel]) {
heap[x][] = node(buf[bel][pos[bel]], bel); down(x, );// 该buffer的新元素替换heap的顶部最小元素
if ((++pos[bel]) == BUFFER_SIZE) {
fread(buf[bel], sizeof(int), BUFFER_SIZE, fin[bel]);
pos[bel] = ;
}
all[bel]--;
} else pop(x); // 该文件属于线程x的部分全部处理完了,就直接pop
}
fwrite(buf[parts], sizeof(int), pos[parts], fout); // 把余下排好序的buffer写入文件
cout << "\rStart merging... 100% completed.";
for (int i = ; i < parts; i++) fclose(fin[i]); fclose(fout);
for (int i = ; i < MAX_K; i++) delete[] buf[i]; delete[] buf;
});
clock_t end = clock();
cout << "\nMerging finished. Time usage = " << end - start << "ms.\n";
} int main() {
string in_file;
cout << "Enter data file name: ";
cin >> in_file;
FILE* fin = fopen(in_file.c_str(), "rb");
if (fin == NULL) {
cout << "Could not open that file.\n";
main();
}
clock_t start_time = clock();
fseek(fin, , SEEK_END);
fgetpos(fin, &data_size);
data_size /= sizeof(int);
parts = (data_size - ) / PARTITION_SIZE + ;
fclose(fin);
cout << "\nPartitioning " << data_size << " elements(int)...\n";
system("mkdir temp");
parallel_for(, , [&](int x) {
if (x) partition_and_sort(in_file);
else ch_size("ans.dat", data_size);
});
merge_file();
clock_t end_time = clock();
system("rd /s/q temp");
cout << "\nExternal sorting complete, result saved to \"ans.dat\".\n"
<< "Time usage = " << end_time - start_time << "ms.\n";
system("pause");
return ;
}

参考:

简单无堆无缓冲区单线程K路归并版本: https://www.cnblogs.com/this-543273659/archive/2011/07/30/2122083.html

wiki:外排序

多线程外排序解决大数据排序问题2(最小堆并行k路归并)的更多相关文章

  1. 使用最小堆来完成k路归并 6.5-8

    感谢:http://blog.csdn.net/mishifangxiangdefeng/article/details/7668486 声明:供自己学习之便而收集整理 题目:请给出一个时间为O(nl ...

  2. MySQL数据库如何解决大数据量存储问题

    利用MySQL数据库如何解决大数据量存储问题? 各位高手您们好,我最近接手公司里一个比较棘手的问题,关于如何利用MySQL存储大数据量的问题,主要是数据库中的两张历史数据表,一张模拟量历史数据和一张开 ...

  3. hadoop job解决大数据量关联时数据倾斜的一种办法

    转自:http://www.cnblogs.com/xuxm2007/archive/2011/09/01/2161929.html http://www.geminikwok.com/2011/04 ...

  4. 利用MySQL数据库如何解决大数据量存储问题?

    提问:如何设计或优化千万级别的大表?此外无其他信息,个人觉得这个话题有点范,就只好简单说下该如何做,对于一个存储设计,必须考虑业务特点,收集的信息如下:1.数据的容量:1-3年内会大概多少条数据,每条 ...

  5. MySQL数据库解决大数据量存储问题

    转载自:https://www.cnblogs.com/ryanzheng/p/8334915.html 提问:如何设计或优化千万级别的大表?此外无其他信息,个人觉得这个话题有点范,就只好简单说下该如 ...

  6. 解决大数据难题 阿里云MaxCompute获科技大奖

    摘要: 据介绍,MaxCompute(大规模分布式的数据计算平台)是国内最早自研的大数据计算平台之一,主要应用于大规模数据处理场景.目前,这项源自浙江.解决世界级难题的成果已拥有EB(百京)级别的数据 ...

  7. Redis集合解决大数据筛选

    Redis集合:集合是什么,就是一堆确定的数据放在一起,数学上集合有交集.并集的概念,这个就可以用来做大数据的筛选功能. 以商品为例,假如商品有颜色和分类.价格区间等属性. 给所有统一颜色的商品放一个 ...

  8. 大数据萌新的Python学习之路(一)

    笔记开始简介 从2018年9月份正式进入大学的时代,大数据和人工智能的崛起让我选择了计算机专业学习数据科学与大数据技术专业,接触的第一门语言就是C语言,后来因为同学推荐的原因进入了学校的人工智能研究协 ...

  9. php配置-解决大数据超多字段的POST方式提交无法完全接受的问题

    例如:在盘点表的数据提交中出现了POST大量数据超多字段的将近2000个字段,部分字段没有接受:修改方法为修改php.ini 将max_input_var调大,该值默认为1000 max_input_ ...

随机推荐

  1. keepalived的vip无法ping通【原创】

    今天收到redis的keepalived vip无法ping通的告警,查看服务器和服务时发现vip在服务器上,服务也正常.只能在本机ping通,跨网段无法ping通.切换keepalived vip至 ...

  2. MySQL没有备份怎么恢复被drop的表(利用undrop-for-innodb)

    介绍:     也许大家都难以理解,这么重要的数据为啥不备份(或者备份不可用)?而且还任性的drop table了.显然有备份是最好的,但是它们并不总是可用的.这种情况令人恐惧,但并非毫无希望.在许多 ...

  3. NLog类库使用探索——详解配置

    1 配置文件的位置(Configuration file locations) 通过在启动的时候对一些常用目录的扫描,NLog会尝试使用找到的配置信息进行自动的自我配置. 1.1 单独的*.exe客户 ...

  4. python 基础 01

    什么是计算机? cpu: 计算机的大脑; 读写速度 3GHZ 内存: (为了提高利用率) 缓冲硬盘和cpu 硬盘: 机械硬盘读写速度70mb/s 计算机里面读写的内容都是01代码 二进制(计算机只认二 ...

  5. Git操作----删除untracked files

    # 删除 untracked files git clean -f # 连 untracked 的目录也一起删掉 git clean -fd # 连 gitignore 的untrack 文件/目录也 ...

  6. MinGW-w64非官方编译器集成安装包(很清楚)

    官方链接[编辑] MinGW 官方网站. SourceForge.net上的MinGW官方的下载站点(Win32系统). MinGW-w64官方的下载站点[编辑] MinGW-w64是2005年由On ...

  7. LuoGu P2863 [USACO06JAN]牛的舞会The Cow Prom

    题目传送门 这个题还是个缩点的板子题...... 答案就是size大于1的强连通分量的个数 加一个size来统计就好了 #include <iostream> #include <c ...

  8. Confluence 6 通过 SSL 或 HTTPS 运行

    Atlassian 应用可以通过 HTTPS 进行访问,但是 Atlassian 并不提供有关访问的支持服务,同时 Atlassian 不能保证能够提供所有的支持. 如果你的 assistance 在 ...

  9. Confluence 6 升级你的许可证

    如果你修改了你的许可证(例如为你的许可证增加了更多的用户),或者从 Cloud 中整合到你本地,你需要更新你的许可证. 希望更新你的额许可证: 进入  > 基本配置(General Config ...

  10. 【Java】「深入理解Java虚拟机」学习笔记(4)- 类文件结构

    我为什么喜欢Java,另一个重要原因就是跨平台,WORA. 程序员是爽了,但肯定有人要为你遮风挡雨,解决WORA的基石就是字节码+虚拟机. ♣Tip 其实这里存在两种无关性,一是平台无关性.另一个是语 ...