检查基于磁盘的合并与辅助存储的性能

Checking the performance for disk-based merge sort with auxiliary storage

本文关键字:合并 性能 存储 于磁盘 磁盘 检查      更新时间:2023-10-16

我尝试与辅助存储实现基于磁盘的合并排序。实现如下。

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;
}

实现基于磁盘的合并排序后,我已经测试了一些小型案例以检查其是否正确运行。在很小的情况下,它看起来足够快,但是在使用20克超过20克的大数据集运行时(最终尺寸超过500克)。它需要2个小时,我将它确实在O(nlogn)中运行。当然,基于磁盘的算法和数据结构也存在一些其他时间。

我很好奇它确实在o(nlogn)中运行。

标准内存中合并排序log(n)通过数据传递,每次通过的较大列表将较大的列表融合在一起。在第一个通过中,您合并了包含一个项目的列表。接下来,它的列表每个有两个项目,然后是四个等。使用此方法,您可以使日志(n)通过数据,然后在每次通过时查看n个项目。因此o(n log n)复杂性。

该方法对于内存中非常有效,因为访问项目的成本不是很高。但是,对于基于磁盘的类型而言,它变得非常昂贵,因为访问每个项目的成本非常高。基本上,您正在阅读和编写整个文件日志(n)次。如果您有20 GB的100字节记录,则您会在文件上谈论25个或更多通行证。因此,您的排序时间将由读写文件25次读写时间主导。

一种外部类型是截然不同的动物。这个想法是尽可能减少磁盘I/O。您可以在两次传球中进行。在第一个通过中,您可以将尽可能多的数据读取到内存中,并使用QuickSort,Merge Sort或其他内存排序算法对其进行排序,并将该块写入磁盘。然后,您将文件的下一个块读为内存,对其进行排序并写入磁盘。

完成第一张通行证后,您会在磁盘上进行一些分类的块。如果您有一个20 GB的文件,并且计算机上有4 GB的内存,那么您将有5个块,每个块大约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时间,以至于它比理论上更快的算法。

Wikipedia上的外部排序文章给出了不错的解释,而Geeksforgeeks文章在C 中给出了一个工作示例。

算法是o(n logn),但性能不仅仅是记录的数量。

不断的寻求和文件访问非常慢。您应该在一个片段中阅读多个记录,因为阅读16个记录(或200)的时间与阅读的时间没有太大不同。

在您的主循环中,您已经拥有数据时正在阅读数据。仅在新记录中读取(对应于lr的哪个已更改)会有所帮助,尽管上面提到的多个记录读数会好得多。

您的最后一个循环,如果您使用大块(许多记录),而不是一次复制它们,则将数据从fd2复制到fd会更快。同样的适用于中间两个循环,在该循环中您复制一个侧面的残留物,并且在那里r环是多余的,因为您立即在最后一个循环中复制相同的数据。

有关在磁盘上对大文件进行排序的所有血腥详细信息,请参见Knuth's Art of Computer编程的第5章(在第3卷中)。(第二版中的第5.4节涉及外部排序。)