mmap比getline慢吗?

8

我面临一个挑战,需要逐行读写(以G为单位)文件。阅读了许多论坛帖子和网站(包括一堆SO),mmap被建议作为读/写文件的最快选项。然而,当我用readline和mmap技术实现我的代码时,发现mmap比两者中更慢。这对读取和写入都是如此。我一直在测试大约600 MB的文件。

我的实现是逐行解析,然后标记该行。我只会提供文件输入。

这里是getline实现:

void two(char* path) {

    std::ios::sync_with_stdio(false);
    ifstream pFile(path);
    string mystring;

    if (pFile.is_open()) {
        while (getline(pFile,mystring)) {
            // c style tokenizing
        }
    }
    else perror("error opening file");
    pFile.close();
}

这里是 mmap

void four(char* path) {

    int fd;
    char *map;
    char *FILEPATH = path;
    unsigned long FILESIZE;

    // find file size
    FILE* fp = fopen(FILEPATH, "r");
    fseek(fp, 0, SEEK_END);
    FILESIZE = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    fclose(fp);

    fd = open(FILEPATH, O_RDONLY);

    map = (char *) mmap(0, FILESIZE, PROT_READ, MAP_SHARED, fd, 0);

    /* Read the file char-by-char from the mmap
     */
    char c;
    stringstream ss;

    for (long i = 0; i <= FILESIZE; ++i) {
        c = map[i];
        if (c != '\n') {
            ss << c;
        }
        else {
            // c style tokenizing
            ss.str("");
        }

    }

    if (munmap(map, FILESIZE) == -1) perror("Error un-mmapping the file");

    close(fd);

}

出于简洁的目的,我省略了大量错误检查。

我的mmap实现是否不正确,从而影响性能?也许对于我的应用程序来说,mmap并非理想选择?

感谢任何意见或帮助!


2
由于您正在使用 openmmap,因此当您可以在 open 后直接使用 fstat 时,使用 fopenfseekftell 没有任何意义。 - Zan Lynx
1
好观点 @Zan。 实际上,我运行了一些测试,发现我的版本对于小于约50MB的文件实际上更快(有趣的是)。然而,对于大于约50MB的文件,情况则相反。因此,对于我的应用程序,我应该真正使用fstat方法。 - Ian
4个回答

14

mmap的真正威力在于能够自由地在文件中查找,直接使用其内容作为指针,并避免从内核高速缓存内存复制数据到用户空间的开销。但是,您的代码示例没有充分利用它。

在您的循环中,您逐个字符扫描缓冲区,并将其附加到stringstream上。 stringstream不知道字符串的长度,因此在过程中必须重新分配多次。在这一点上,您已经破坏了使用mmap的任何性能提升-即使标准的getline实现也避免多次重分配(通过使用128字节的堆栈缓冲区,在GNU C++实现中)。

如果您想要充分利用mmap:

  • 切勿复制字符串。相反,在mmap缓冲区中复制指针。
  • 使用内置函数,例如strnchrmemchr来查找换行符;这些函数利用手动编写的汇编程序和其他优化来运行比大多数开放式搜索循环更快。

1
我已经按照您推荐的strchr和指针操作重新实现了我的mmap函数。现在,mmap赢得了比赛,但只有一个375 mb文件大约快了4秒。 - Ian
1
@Ian,是的,你的瓶颈很可能在其他地方。你正在使用已缓存的数据进行测试,对吧? - bdonlan
1
当然可以,只要您有足够的 RAM :) 但是循环运行的部分仍然适用;这样做应该会帮助平衡一下,可以这么说。尽管我猜中位数可能更受青睐... - bdonlan
好的,我已经对两种方法进行了优化,减少了内存分配等操作。在一个有10M行的375MB文件中,使用mmap相比于使用newline只能获得11%的速度提升。我的总体时间大约是0.9秒和0.8秒。最大的文件大小将会是现在的24倍左右。这种微小的速度提升并不值得使用mmap所消耗的大量内存。 - Ian
1
@Ian,mmap并不比常规读取使用更多的内存,只是以不同的方式计算。请参见https://dev59.com/unI-5IYBdhLWcg3wMFW0。 - bdonlan
显示剩余3条评论

9

告诉你使用mmap的人其实不太了解现代计算机。

mmap的性能优势是完全错误的。用Linus Torvalds的话来说:

没错,内存“慢”,但是该死的,mmap()也是。

mmap的问题在于,每次您首次访问映射区域中的页面时,它都会陷入内核并将页面实际映射到您的地址空间中,从而对TLB造成破坏。

尝试使用read读取一个大文件,每次8K,并再次使用mmap进行测试。(重复使用相同的8K缓冲区。)您几乎可以肯定地发现read实际上更快。

你的问题从来不是从内核中获取数据,而是在获取数据后如何处理数据。尽量减少逐个字符执行的工作;只需扫描以查找换行符,然后对该块执行单个操作。就我个人而言,我会返回到使用适合于L1缓存(约8K)的缓冲区的read实现,并重复使用它。
或者至少,我会尝试一个简单的read与mmap基准测试,看看哪个在你的平台上实际上更快。
[更新]
我找到了Torvalds先生的另外两组评论:

http://lkml.iu.edu/hypermail/linux/kernel/0004.0/0728.html http://lkml.iu.edu/hypermail/linux/kernel/0004.0/0775.html

摘要:

此外,您仍然需要考虑实际的CPU TLB缺失成本等问题。如果您只是重新读取相同区域而不是过度使用内存管理来避免复制,则通常可以避免这种情况。

在许多情况下,memcpy()(即此处的“read()”)始终会更快,仅因为它避免了所有额外的复杂性。而mmap()在其他情况下将更快。

根据我的经验,在顺序读取和处理大型文件时,使用(并重复使用)具有read/write的适度大小的缓冲区明显比使用mmap更快。


测试read()是我接下来要做的事情。 然而,我不确定有多少行实际上大于8k,因此无法确定能够实现多少优化。 感谢您的提示;我一定会尝试的。 - Ian

0
你正在使用 stringstream 来存储识别到的行,这与 getline 实现不兼容,因为 stringstream 本身会增加开销。正如其他人建议的那样,你可以将字符串的开头存储为 char*,也许还可以存储行的长度(或指向行尾的指针)。读取的主体代码应该像这样:
char* str_start = map;
char* str_end;
for (long i = 0; i <= FILESIZE; ++i) {
        if (map[i] == '\n') {
            str_end = map + i;
            {
                // C style tokenizing of the string str_start to str_end
                // If you want, you can build a std::string like:
                // std::string line(str_start,str_end);
                // but note that this implies a memory copy.
            }
            str_start = map + i + 1;
        }
    }

请注意,这种方法更加高效,因为您不需要在每个字符中处理任何内容(在您的版本中,您正在将字符添加到 stringstream 中)。

0
您可以使用 memchr 来查找行尾。它比逐个添加字符到 stringstream 中要快得多。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接