2017-03-08 58 views
0

我尝试使用辅助存储实现基于磁盘的合并分类。实现如下所示。检查辅助存储的基于磁盘的合并分类的性能

FD - 为数据集文件描述符将被排序

FD2 - 辅助存储

#define LENGTH 100 
#define SEARCH_BEGIN 4 
int merge_sort_d(int fd, int fd2, int s, int e) { 
    int i, m; 
    int l, r; 
    char lv[LENGTH], rv[LENGTH]; 
    char buf[LENGTH]; 
    if (s >= e) return 1; 
    m = (s + e)/2; 
    merge_sort_d(fd, fd2, s, m); 
    merge_sort_d(fd, fd2, m+1, e); 
    l = s; 
    r = m+1; 
    memset(lv, 0, LENGTH); 
    memset(rv, 0, LENGTH); 
    lseek(fd2, 0, SEEK_SET); 
    while (l <= m && r <= e) { 
     lseek(fd, 1LL*SEARCH_BEGIN + 1LL*l*LENGTH, SEEK_SET); 
     read(fd, (void *)lv, LENGTH); 
     lseek(fd, 1LL*SEARCH_BEGIN + 1LL*r*LENGTH, SEEK_SET); 
     read(fd, (void *)rv, LENGTH); 
     if (strncmp(lv, rv, LENGTH) < 0) { 
      write(fd2, (void *)lv, LENGTH); 
      ++l; 
     } else { 
      write(fd2, (void *)rv, LENGTH); 
      ++r; 
     } 
    } 
    for (; l <= m; ++l) { 
     lseek(fd, 1LL*SEARCH_BEGIN + 1LL*l*LENGTH, SEEK_SET); 
     read(fd, (void *)lv, LENGTH); 
     write(fd2, (void *)lv, LENGTH); 
    } 
    for (; r <= e; ++r) { 
     lseek(fd, 1LL*SEARCH_BEGIN + 1LL*r*LENGTH, SEEK_SET); 
     read(fd, (void *)rv, LENGTH); 
     write(fd2, (void *)rv, LENGTH); 
    } 
    lseek(fd, 1LL*SEARCH_BEGIN + 1LL*s*LENGTH, SEEK_SET); 
    lseek(fd2, 0, SEEK_SET); 
    memset(buf, 0, LENGTH); 
    for (i=s; i<=e; ++i) { 
     read(fd2, (void *)buf, LENGTH); 
     write(fd, (void *)buf, LENGTH); 
    } 
    return 1; 
} 

实现基于磁盘的合并排序我已经测试了一些小的情况下,以检查其是否运行正常后文件描述符。它在小型案例中看起来足够快,但是在超过20G的大型数据集上运行时(最终大小超过500G)。它需要2个小时,我混淆它真的运行在O(nlogn)。当然,基于磁盘的算法和数据结构还有一些额外的时间。

我很好奇它是否真的在O(nlogn)中运行。

+1

我通常使用迭代自下而上的合并排序,而不是像在这里完成的自上而下递归排序。如果在块足够小时使用'std :: sort',则可以获得更高的效率,而不是调用'merge_sort'直至块大小为1. –

+0

感谢您的评论。当我尝试进行优化和重构时,我会考虑它们。 – chatterboy

回答

0

标准内存中合并排序不会log(n)传递数据,每次传递都会依次合并更大的列表。在第一遍中,合并包含每个项目的列表。接下来,它的列表分别包含两个项目,然后是四个等等。使用这种方法,您可以使log(n)遍历数据,并在每次传递期间查看n个项目。因此O(n log n)的复杂性。

该方法对于内存中的排序非常有效,因为访问项目的成本不是很高。但是,对于基于磁盘的排序,它变得非常昂贵,因为访问每个项目的成本非常高。基本上,你正在读写整个文件日志(n)次。如果您有20 GB的100字节记录,则说明该文件有25个或更多的通过。因此,您的排序时间将由读取和写入文件25次所花费的时间决定。

外部排序是一个非常不同的动物。这个想法是尽可能减少磁盘I/O。你通过两次。在第一遍中,您可以将尽可能多的数据读入内存,并使用Quicksort,合并排序或其他内存排序算法对其进行排序,然后将该块写入磁盘。然后,您将下一个文件块读入内存,对其进行分类,然后写入磁盘。

当你完成第一遍时,你在磁盘上有一些已排序的块。如果您有20 GB的文件,并且您的计算机上有4 GB的可用内存,那么您将拥有五个块,每块大小约为4 GB。 (请注意,实际上可能有五个块比4 GB略小,而第六个块很小)。调用块数k

请注意,第一遍完成后,您已经读取和写入每条记录一次。

在第二遍中,您合并了多个块。这是通过一堆k项目完成的。这个想法是,你用每个块的第一项初始化堆。您可以从这些k项目(位于堆顶部)中选择最小的项目,然后将其写入输出文件。然后,从包含刚删除的项目的块中取下一个项目,并将其添加到堆中。重复此过程,直到您清空所有块。

第一遍是O(n log n)。实际上,它是k *((n/k)log(n/k)),它计算出n log n。第二遍是O(n log k)。

这里的重要部分是在第二遍中,您再次读取和写入每个项目一次。你已经减少了你的磁盘I/O读写每个项目日志(n)次到读写每个项目两次。这会比你写的代码更快地运行更多

注意到两个算法确实被认为是O(n log n)也很重要。 I/O常数是杀手。第二种算法实际上做了更多的计算,但它节省了大量的磁盘I/O时间,使其比理论上更快的算法更快。

维基百科上的External sorting文章给出了一个体面的解释,而GeeksforGeeks article给出了一个在C++中的工作示例。

+0

在第二次(如果需要)以后,每个I/O应读取或写入包含许多记录的数据块。代码将不得不处理跨越块的记录。假设k路合并,则块大小将大约为可用内存大小/(k + 1),并且I/O大小小于处理跨越块的记录的最大记录大小。 Gnu排序完成所有这些,但由于有很多参数,所以它有很多代码。它在初始传递中使用多线程,并且每个块使用一个临时文件。 – rcgldr

0

该算法为O(N logN),但性能不仅仅是被排序的记录数量。

不断寻求和文件访问是非常缓慢的。您应该在一个块中读取多条记录,因为读取16条记录(或200条)的时间与读取记录的时间差别不大。

在您的主for循环中,当您已经拥有它时,您正在读取数据。只有在新记录中读取(对应于lr中的哪一个被更改)将会有很大帮助,尽管上述多次记录读数会更好。

如果您使用大块(许多记录)而不是一次只复制一个数据块,则最后一个循环将数据从fd2复制到fd会更快。这同样适用于中间两个循环,在这里您复制其中一个边的剩余部分,并且由于您立即将相同的数据复制回最后一个循环,因此r循环是多余的。

有关在磁盘上整理大文件的所有细节,请参阅Knuth的艺术计算机编程的第5章(第3卷)。 (第二版中的第5.4节涉及外部排序。)