对大文件排序

设想你有一个20GB的文件,每行一个字符串,说明如何对这个文件进行排序。

内存肯定没有20GB大,所以不可能采用传统排序法。但是可以将文件分成许多块,每块xMB,针对每个快各自进行排序,存回文件系统。

然后将这些块逐一合并,最终得到全部排好序的文件。

外排序的一个例子是外归并排序(External merge sort),它读入一些能放在内存内的数据量,在内存中排序后输出为一个顺串(即是内部数据有序的临时文件),处理完所有的数据后再进行归并。[1][2]比如,要对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个文件bigdata,大小4663M,5亿个数,文件中的数据随机,如下一行一个整数:


1

2

3

4

5

6

7

8

9

10

11

12

13

6196302

3557681

6121580

2039345

2095006

1746773

7934312

2016371

7123302

8790171

2966901

...

7005375

现在要对这个文件进行排序,怎么搞?


内部排序

先尝试内排,选2种排序方式:


3路快排:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

private final  int  cutoff = 8;

public <T> void perform(Comparable<T>[] a) {

        perform(a,0,a.length - 1);

    }

    private <T> int median3(Comparable<T>[] a,int x,int y,int z) {

        if(lessThan(a[x],a[y])) {

            if(lessThan(a[y],a[z])) {

                return y;

            }

            else if(lessThan(a[x],a[z])) {

                return z;

            }else {

                return x;

            }

        }else {

            if(lessThan(a[z],a[y])){

                return y;

            }else if(lessThan(a[z],a[x])) {

                return z;

            }else {

                return x;

            }

        }

    }

    private <T> void perform(Comparable<T>[] a,int low,int high) {

        int n = high - low + 1;

        //当序列非常小,用插入排序

        if(n <= cutoff) {

            InsertionSort insertionSort = SortFactory.createInsertionSort();

            insertionSort.perform(a,low,high);

            //当序列中小时,使用median3

        }else if(n <= 100) {

            int m = median3(a,low,low + (n >>> 1),high);

            exchange(a,m,low);

            //当序列比较大时,使用ninther

        }else {

            int gap = n >>> 3;

            int m = low + (n >>> 1);

            int m1 = median3(a,low,low + gap,low + (gap << 1));

            int m2 = median3(a,m - gap,m,m + gap);

            int m3 = median3(a,high - (gap << 1),high - gap,high);

            int ninther = median3(a,m1,m2,m3);

            exchange(a,ninther,low);

        }

        if(high <= low)

            return;

        //lessThan

        int lt = low;

        //greaterThan

        int gt = high;

        //中心点

        Comparable<T> pivot =  a[low];

        int i = low + 1;

        /*

        * 不变式:

        *   a[low..lt-1] 小于pivot -> 前部(first)

        *   a[lt..i-1] 等于 pivot -> 中部(middle)

        *   a[gt+1..n-1] 大于 pivot -> 后部(final)

        *

        *   a[i..gt] 待考察区域

        */

        while (i <= gt) {

            if(lessThan(a[i],pivot)) {

                //i-> ,lt ->

                exchange(a,lt++,i++);

            }else if(lessThan(pivot,a[i])) {

                exchange(a,i,gt--);

            }else{

                i++;

            }

        }

        // a[low..lt-1] < v = a[lt..gt] < a[gt+1..high].

        perform(a,low,lt - 1);

        perform(a,gt + 1,high);

    }


归并排序:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

/**

 * 小于等于这个值的时候,交给插入排序

 */

private final  int  cutoff = 8;

/**

 * 对给定的元素序列进行排序

 *

 * @param a 给定元素序列

 */

@Override

public <T> void perform(Comparable<T>[] a) {

    Comparable<T>[] b = a.clone();

    perform(b, a, 0, a.length - 1);

}

private <T> void perform(Comparable<T>[] src,Comparable<T>[] dest,int low,int high) {

    if(low >= high)

        return;

    //小于等于cutoff的时候,交给插入排序

    if(high - low <= cutoff) {

        SortFactory.createInsertionSort().perform(dest,low,high);

        return;

    }

    int mid = low + ((high - low) >>> 1);

    perform(dest,src,low,mid);

    perform(dest,src,mid + 1,high);

    //考虑局部有序 src[mid] <= src[mid+1]

    if(lessThanOrEqual(src[mid],src[mid+1])) {

        System.arraycopy(src,low,dest,low,high - low + 1);

    }

    //src[low .. mid] + src[mid+1 .. high] -> dest[low .. high]

    merge(src,dest,low,mid,high);

}

private <T> void merge(Comparable<T>[] src,Comparable<T>[] dest,int low,int mid,int high) {

    for(int i = low,v = low,w = mid + 1; i <= high; i++) {

        if(w > high || v <= mid && lessThanOrEqual(src[v],src[w])) {

            dest[i] = src[v++];

        }else {

            dest[i] = src[w++];

        }

    }

}

数据太多,递归太深 ->栈溢出?加大Xss?
数据太多,数组太长 -> OOM?加大Xmx?

耐心不足,没跑出来.而且要将这么大的文件读入内存,在堆中维护这么大个数据量,还有内排中不断的拷贝,对栈和堆都是很大的压力,不具备通用性。


sort命令来跑


1

sort
-n bigdata -o bigdata.sorted

跑了多久呢?24分钟.

为什么这么慢?

粗略的看下我们的资源:

  1. 内存
    jvm-heap/stack,native-heap/stack,page-cache,block-buffer
  2. 外存
    swap + 磁盘

数据量很大,函数调用很多,系统调用很多,内核/用户缓冲区拷贝很多,脏页回写很多,io-wait很高,io很繁忙,堆栈数据不断交换至swap,线程切换很多,每个环节的锁也很多.

总之,内存吃紧,问磁盘要空间,脏数据持久化过多导致cache频繁失效,引发大量回写,回写线程高,导致cpu大量时间用于上下文切换,一切,都很糟糕,所以24分钟不细看了,无法忍受.


位图法


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

private
BitSet bits;

public
void  perform(

        String largeFileName,

        int
total,

        String destLargeFileName,

        Castor<Integer> castor,

        int
readerBufferSize,

        int
writerBufferSize,

        boolean
asc) throws
IOException {

    System.out.println("BitmapSort Started.");

    long
start = System.currentTimeMillis();

    bits = new
BitSet(total);

    InputPart<Integer> largeIn = PartFactory.createCharBufferedInputPart(largeFileName, readerBufferSize);

    OutputPart<Integer> largeOut = PartFactory.createCharBufferedOutputPart(destLargeFileName, writerBufferSize);

    largeOut.delete();

    Integer data;

    int
off = 0;

    try
{

        while
(true) {

            data = largeIn.read();

            if
(data == null)

                break;

            int
v = data;

            set(v);

            off++;

        }

        largeIn.close();

        int
size = bits.size();

        System.out.println(String.format("lines : %d ,bits : %d", off, size));

        if(asc) {

            for
(int
i = 0; i < size; i++) {

                if
(get(i)) {

                    largeOut.write(i);

                }

            }

        }else
{

            for
(int
i = size - 1; i >= 0; i--) {

                if
(get(i)) {

                    largeOut.write(i);

                }

            }

        }

        largeOut.close();

        long
stop = System.currentTimeMillis();

        long
elapsed = stop - start;

        System.out.println(String.format("BitmapSort Completed.elapsed : %dms",elapsed));

    }finally
{

        largeIn.close();

        largeOut.close();

    }

}

private
void  set(int
i) {

    bits.set(i);

}

private
boolean  get(int
v) {

    return
bits.get(v);

}

nice!跑了190秒,3分来钟.
以核心内存4663M/32大小的空间跑出这么个结果,而且大量时间在用于I/O,不错.

问题是,如果这个时候突然内存条坏了1、2根,或者只有极少的内存空间怎么搞?


外部排序

该外部排序上场了.
外部排序干嘛的?

  1. 内存极少的情况下,利用分治策略,利用外存保存中间结果,再用多路归并来排序;
  1. map-reduce的嫡系.


1.分

内存中维护一个极小的核心缓冲区memBuffer,将大文件bigdata按行读入,搜集到memBuffer满或者大文件读完时,对memBuffer中的数据调用内排进行排序,排序后将有序结果写入磁盘文件bigdata.xxx.part.sorted.
循环利用memBuffer直到大文件处理完毕,得到n个有序的磁盘文件:

2.合

现在有了n个有序的小文件,怎么合并成1个有序的大文件?
把所有小文件读入内存,然后内排?
(⊙o⊙)…
no!

利用如下原理进行归并排序:

我们举个简单的例子:

文件1:3,6,9
文件2:2,4,8
文件3:1,5,7

第一回合:
文件1的最小值:3 , 排在文件1的第1行
文件2的最小值:2,排在文件2的第1行
文件3的最小值:1,排在文件3的第1行
那么,这3个文件中的最小值是:min(1,2,3) = 1
也就是说,最终大文件的当前最小值,是文件1、2、3的当前最小值的最小值,绕么?
上面拿出了最小值1,写入大文件.

第二回合:
文件1的最小值:3 , 排在文件1的第1行
文件2的最小值:2,排在文件2的第1行
文件3的最小值:5,排在文件3的第2行
那么,这3个文件中的最小值是:min(5,2,3) = 2
将2写入大文件.

也就是说,最小值属于哪个文件,那么就从哪个文件当中取下一行数据.(因为小文件内部有序,下一行数据代表了它当前的最小值)

最终的时间,跑了771秒,13分钟左右.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

less bigdata.sorted.text

...

9999966

9999967

9999968

9999969

9999970

9999971

9999972

9999973

9999974

9999975

9999976

9999977

9999978

 

原文地址:https://www.cnblogs.com/panxuejun/p/8761321.html

时间: 2024-10-13 16:23:42

对大文件排序的相关文章

可以对大文件排序的排序算法

Z-Tree是一个可以对大数据排序的数据结构.Z-Tree排序的时间复杂度是O(n). Z-Tree可以取代Hash表实现关键字(Key)到值(Value)的映射. Z-Tree Demo展示了怎样用Z-Tree对若干GB的大文件排序. Z-Tree Demo同时展示了怎样用Z-Tree来实现大量关键字(Key)到值(Value)的映射并且根据关键字(Key)快速找到相应的值. Z-Tree Demo还展示了怎样用Z-Tree来从大量的字符串中查找最大匹配子字符串. 另外Z-Tree Demo包

大文件归并排序

//大文件排序 function countsLines($path){ $fd = fopen($path,"r"); $total=0; while(!feof($fd)){ $total++; fgets($fd); } return $total; } $filePath = "./file.dat"; function checkFiles($path,$rows=5000){ $totalFiles = countsLines($path); $tota

用java实现大文件分割、排序、合并

import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Collections; import java.util.Iterator; import java

PHP几个几十个G大文件数据统计并且排序处理

诸多大互联网公司的面试都会有这么个问题,有个4G的文件,如何用只有1G内存的机器去计算文件中出现次数最多的数字(假设1行是1个数组,例如QQ号 码).如果这个文件只有4B或者几十兆,那么最简单的办法就是直接读取这个文件后进行分析统计.但是这个是4G的文件,当然也可能是几十G甚至几百G的文 件,这就不是直接读取能解决了的. 同样对于如此大的文件,单纯用PHP做是肯定行不通的,我的思路是不管多大文件,首先要切割为多个应用可以承受的小文件,然后批量或者依次分析统计小文件后再把总的结果汇总后统计出符合要

【学习】大文件统计与排序(转载)

学习:大文件统计与排序 这篇主要记录一下学习陈硕同学的对下面这道题的算法思想与代码. 题目是这样的: 有10个文件,每个文件1G,每个文件的每行存放的都是用户的query(请自己随机产生),每个文件的query都可能重复.要求你按照query的频度排序. (当然,这里的重点是大文件,所以10个1G的文件,或者1个10G的文件,原理都是一样的) 陈硕的代码在这里: https://gist.github.com/4009225 这是一段非常漂亮的代码,解法与代码都非常值得一看. [解法] 基本步骤

Oracle bigfile 大文件表空间

Database 是由一个或多个被称为表空间(tablespace)的逻辑存储单位构成.表空间内的逻辑存储单位为段(segment),段又可以继续划分为数据扩展(extent).而数据扩展是由一组连续的数据块(datablock)构成. 大文件表空间 在Oracle中用户可以创建大文件表空间(bigfile tablespace).这样Oracle数据库使用的表空间(tablespace)可以由一个单一的大文件构成,而不是若干个小数据文件.这使Oracle可以发挥64位系统的能力,创建.管理超大

MYSQL 磁盘临时表和文件排序

因为Memory引擎不支持BOLB和TEXT类型,所以,如果查询使用了BLOB或TEXT列并且需要使用隐式临时表,将不得不使用MyISAM磁盘临时表,即使只有几行数据也是如此. 这会导致严重的性能开销..即使配置Mysql将临时表存储在内存块设备上(ram-disk),依然需要很多昂贵的系统调用. 最好的解决方案是尽量避免使用BLOB和TEXT类型.如果实在无法避免,有一个技巧是在所有用到BLOG字段的地方都使用SUBSTRING(culumn,length)将列值转换为字符串(在order b

Linux如何查找大文件或目录总结(转)

在Windows系统中,我们可以使用TreeSize工具查找一些大文件或文件夹,非常的方便高效,在Linux系统中,如何去搜索一些比较大的文件呢?下面我整理了一下在Linux系统中如何查找大文件或文件夹的方法. 1: 如何查找大文件? 其实很多时候,你需要了解当前系统下有哪些大文件,比如文件大小超过100M或1G(阀值视具体情况而定).那么如何把这些大文件搜索出来呢?例如我要搜索当前目录下,超过800M大小的文件 [[email protected] u03]# pwd /u03 [[email

系统清理——查找大文件

在系统中查找大文件的方法: 1. 统计当前文件夹的文件(或文件夹)大小.并依照从大到小的顺序排序 du -s /home/* | sort -nr * -h已易读的格式显示指定文件夹或文件的大小 * -s选项指定对于文件夹不具体显示每一个子文件夹或文件的大小 2. 查找大于100M的文件.并显示具体信息 find . -type f -size +100M -exec ls -lh {} \; find的处理动作能够是: -print     默觉得输出 -ls         显示查找到的文件