温馨提示:本文翻译自stackoverflow.com,查看原文请点击:linux - Reducing kernel overhead when reading a huge file with lazy bytestrings
haskell io linux

linux - 减少读取带有惰性字节串的大文件时的内核开销

发布于 2020-04-06 00:35:24

我正在读取一个大文件(1-10 GB),并对其进行一些简单的统计,例如计算一个字符。在这种情况下,流是有意义的,因此我使用了lazy ByteString特别是我的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时间和经过时间之间的时差。除此之外,在以下条件下运行还会/usr/bin/time显示类似结果:

0.89user 0.45system 0:01.35elapsed

我正在测试的文件位于中tmpfs,因此实际磁盘性能不应该是一个因素。

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

查看更多

提问者
0xd34df00d
被浏览
127
K. A. Buhr 2020-02-01 06:46

首先,您的计算机似乎发生了一些不可思议的事情。当我在内存或tmpfs文件系统中缓存的1G文件上运行此程序时(无关紧要),系统时间会大大缩短:

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

如果您还有其他可能导致这300ms增长的其他负载或内存压力,我想您需要首先解决该问题,然后我在下面说的任何内容都将有所帮助,但是...

无论如何,对于我的测试,我使用了更大的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调用的开销(很可能是将数据从内核复制到用户地址空间的特定过程)占用了大约410ms的系统时间。

与上述经验相反,切换到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