linux - 在读取带有惰性字节串的大文件时减少内核开销
问题描述
我正在读取一个大文件(1-10 GB)并计算一些简单的统计数据,比如计算一个字符。在这种情况下,流式传输是有意义的,所以我使用 lazy ByteString
s。特别是,我的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
时间?我尝试明确设置文件句柄的缓冲区大小(对运行时间没有统计上的显着影响)以及mmap
ing 文件并将其包装到 a 中ByteString
(运行时间实际上变得更糟)。还有什么值得尝试的?
解决方案
首先,您的机器似乎出了点问题。当我在缓存在内存或 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