我正在读取一个大文件(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
(运行时间实际上变得更糟)。还有什么值得尝试的?
首先,您的计算机似乎发生了一些不可思议的事情。当我在内存或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
仅供参考,有封装中提供
mmap
了ByteString
,既严格,(原因我不明白)懒惰。绝对是我的机器有些奇怪,而且我再也无法重现这些问题
mmap
了。我已经更换BS.readFile
用mmap
的ping功能bytestring-mmap
包(这似乎真正做到零拷贝),现在我越来越0.04 S的系统时间。无论如何,感谢您的详细答复!