首页 > 解决方案 > 在读取带有惰性字节串的大文件时减少内核开销

问题描述

我正在读取一个大文件(1-10 GB)并计算一些简单的统计数据,比如计算一个字符。在这种情况下,流式传输是有意义的,所以我使用 lazy ByteStrings。特别是,我的main样子

import qualified Data.ByteString.Lazy as BSL

main :: IO ()
main = do
  contents <- BSL.readFile "path"
  print $ computeStats contents

在这种情况下,细节computeStats可能并不重要。

当运行这个时+RTS -sstderr,我看到这个:

MUT     time    0.938s  (  1.303s elapsed)

注意 CPU 时间和运行时间之间的差异。除此之外,运行 under/usr/bin/time显示类似的结果:

0.89user 0.45system 0:01.35elapsed

我正在测试的文件是 in tmpfs,所以实际的磁盘性能不应该是一个因素。

在这种情况下如何减少system时间?我尝试明确设置文件句柄的缓冲区大小(对运行时间没有统计上的显着影响)以及mmaping 文件并将其包装到 a 中ByteString(运行时间实际上变得更糟)。还有什么值得尝试的?

标签: linuxhaskellio

解决方案


首先,您的机器似乎出了点问题。当我在缓存在内存或 tmpfs 文件系统中的 1G 文件上运行此程序时(不管哪个),系统时间要小得多:

1.44user 0.14system 0:01.60elapsed 99%CPU (0avgtext+0avgdata 50256maxresident)

如果您有任何其他类型的负载或内存压力可能导致这些额外的 300 毫秒,我认为您需要先解决这个问题,然后我在下面说的任何内容都会有所帮助,但是......

无论如何,对于我的测试,我使用了更大的 5G 测试文件来使系统时间更容易量化。作为基线,C 程序:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFLEN (1024*1024)
char buffer[BUFLEN];

int
main()
{
        int nulls = 0;
        int fd = open("/dev/shm/testfile5G.dat", O_RDONLY);
        while (read(fd, buffer, BUFLEN) > 0) {
                for (int i = 0; i < BUFLEN; ++i) {
                        if (!buffer[i]) ++nulls;
                }
        }
        printf("%d\n", nulls);
}

编译时gcc -O2在我的测试文件上运行时间:

real    0m2.035s
user    0m1.619s
sys     0m0.416s

作为比较,使用以下代码编译的 Haskell 程序ghc -O2

import Data.Word
import qualified Data.ByteString.Lazy as BSL

main :: IO ()
main = do
  contents <- BSL.readFile "/scratch/buhr/testfile5G.dat"
  print $ BSL.foldl' go 0 contents
    where go :: Int -> Word8 -> Int
          go n 0 = n + 1
          go n _ = n

计数要慢一些,但系统时间几乎相同:

real    0m8.411s
user    0m7.966s
sys     0m0.444s

像所有简单的测试cat testfile5G.dat >/dev/null都给出一致的系统时间结果,因此可以安全地得出结论,read调用的开销,很可能是从内核复制数据到用户地址空间的特定过程,占 410 毫秒左右系统时间的相当一部分。

与您上面的经验相反,切换到mmap应该可以减少这种开销。Haskell 程序:

import System.Posix.IO
import Foreign.Ptr
import Foreign.ForeignPtr
import MMAP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS

-- exact length of file
len :: Integral a => a
len = 5368709120

main :: IO ()
main = do
  fd <- openFd "/scratch/buhr/testfile5G.dat" ReadOnly Nothing defaultFileFlags
  ptr <- newForeignPtr_ =<< castPtr <$>
    mmap nullPtr len protRead (mkMmapFlags mapPrivate mempty) fd 0
  let contents = BS.fromForeignPtr ptr 0 len
  print $ BS.foldl' (+) 0 contents

以大约相同的用户时间运行,但大大减少了系统时间:

real    0m7.972s
user    0m7.791s
sys     0m0.181s

请注意,在ByteString此处使用将映射区域变为严格区域的零复制方法是绝对关键的。

在这一点上,我认为我们可能归结为管理进程页表的开销,以及使用 mmap 的 C 版本:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

size_t len = 5368709120;

int
main()
{
        int nulls = 0;
        int fd = open("/scratch/buhr/testfile5G.dat", O_RDONLY);
        char *p = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
        for (int i = 0; i < len; ++i) {
                if (!p[i]) ++nulls;
        }
        printf("%d\n", nulls);
}

具有相似的系统时间:

real    0m1.888s
user    0m1.708s
sys     0m0.180s

推荐阅读