我正在读取一个大文件(1-10 GB)并计算一些简单的统计数据,比如计算一个字符。在这种情况下流式传输是有意义的,所以我使用了 lazy ByteStrings。特别是,我的main样子
import qualified Data.ByteString.Lazy as BSL
main :: IO ()
main = do
  contents <- BSL.readFile "path"
  print $ computeStats contents
Run Code Online (Sandbox Code Playgroud)
computeStats在这种情况下,细节可能并不重要。
使用 运行它时+RTS -sstderr,我看到了这个:
MUT     time    0.938s  (  1.303s elapsed)
Run Code Online (Sandbox Code Playgroud)
请注意 CPU 时间和已用时间之间的差异。除此之外,运行 under/usr/bin/time显示类似的结果:
0.89user 0.45system 0:01.35elapsed
Run Code Online (Sandbox Code Playgroud)
我正在测试的文件在 中tmpfs,因此实际磁盘性能不应该是一个因素。
system在这种情况下如何减少时间?我尝试明确设置文件句柄的缓冲区大小(对运行时间没有统计上的显着影响)以及mmaping 文件并将其包装成一个ByteString(运行时间实际上变得更糟)。还有什么值得尝试的?
首先,您的机器似乎出现了一些奇怪的情况。当我在内存中或 tmpfs 文件系统(无论哪个)缓存的 1G 文件上运行此程序时,系统时间要小得多:
1.44user 0.14system 0:01.60elapsed 99%CPU (0avgtext+0avgdata 50256maxresident)
Run Code Online (Sandbox Code Playgroud)
如果您有任何其他负载或内存压力可能会导致这些额外的 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);
}
Run Code Online (Sandbox Code Playgroud)
在我的测试文件上运行编译gcc -O2多次:
real    0m2.035s
user    0m1.619s
sys     0m0.416s
Run Code Online (Sandbox Code Playgroud)
为了进行比较,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
Run Code Online (Sandbox Code Playgroud)
计算起来要慢一些,但系统时间几乎相同:
real    0m8.411s
user    0m7.966s
sys     0m0.444s
Run Code Online (Sandbox Code Playgroud)
像所有这样的更简单的测试cat testfile5G.dat >/dev/null都会给出一致的系统时间结果,因此可以安全地得出结论,调用的开销(read很可能是将数据从内核复制到用户地址空间的特定过程)占 410 毫秒左右的系统时间的相当大的一部分。
与您上面的经验相反,切换到mmap应该会减少这种开销。哈斯克尔程序:
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
Run Code Online (Sandbox Code Playgroud)
运行时用户时间大致相同,但系统时间大大减少:
real    0m7.972s
user    0m7.791s
sys     0m0.181s
Run Code Online (Sandbox Code Playgroud)
请注意,使用零复制方法将映射区域变为严格区域绝对至关重要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);
}
Run Code Online (Sandbox Code Playgroud)
具有类似的系统时间:
real    0m1.888s
user    0m1.708s
sys     0m0.180s
Run Code Online (Sandbox Code Playgroud)
        |   归档时间:  |  
           
  |  
        
|   查看次数:  |  
           110 次  |  
        
|   最近记录:  |