Haskell performance实现unix';s";猫;使用Data.ByteString编程

Haskell performance实现unix';s";猫;使用Data.ByteString编程,performance,unix,haskell,pipeline,cat,Performance,Unix,Haskell,Pipeline,Cat,我有以下Haskell代码,实现了一个简单版本的“cat”unix命令行实用程序。在400MB文件上使用“时间”测试性能,速度大约慢3倍。(我用来测试它的确切脚本在代码下面) 我的问题是: 这是一个有效的性能测试吗 如何使该程序运行得更快 通常,我如何识别Haskell程序中的性能瓶颈 关于问题2和3:我使用了GHC-prof,然后使用+RTS-p运行,但是我发现这里的输出有点缺乏信息 来源(Main.hs) 我的结果: timing Haskell real 0m0.980s user

我有以下Haskell代码,实现了一个简单版本的“cat”unix命令行实用程序。在400MB文件上使用“时间”测试性能,速度大约慢3倍。(我用来测试它的确切脚本在代码下面)

我的问题是:

  • 这是一个有效的性能测试吗
  • 如何使该程序运行得更快
  • 通常,我如何识别Haskell程序中的性能瓶颈
  • 关于问题2和3:我使用了GHC-prof,然后使用+RTS-p运行,但是我发现这里的输出有点缺乏信息

    来源(Main.hs)

    我的结果:

    timing Haskell
    
    real    0m0.980s
    user    0m0.296s
    sys     0m0.684s
    
    
    timing 'cat'
    
    real    0m0.304s
    user    0m0.001s
    sys     0m0.302s
    
    使用-prof编译并使用+RTS-p运行时的分析报告如下:

      Sat Dec 13 21:26 2014 Time and Allocation Profiling Report  (Final)
    
         Main +RTS -p -RTS huge
    
      total time  =        0.92 secs   (922 ticks @ 1000 us, 1 processor)
      total alloc = 7,258,596,176 bytes  (excludes profiling overheads)
    
    COST CENTRE MODULE  %time %alloc
    
    MAIN        MAIN    100.0  100.0
    
    
                                                           individual     inherited
    COST CENTRE MODULE                   no.     entries  %time %alloc   %time %alloc
    
    MAIN        MAIN                      46           0  100.0  100.0   100.0  100.0
     CAF        GHC.Conc.Signal           84           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.FD                 82           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Handle.FD          81           0    0.0    0.0     0.0    0.0
     CAF        System.Posix.Internals    76           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding           70           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding.Iconv     69           0    0.0    0.0     0.0    0.0
    

    这只是试图解决第二个问题的部分答案:

    timing 'cat'
    
    real    0m0.075s
    user    0m0.000s
    sys     0m0.074s
    timing strict bytestring with GHC -O2
    
    real    0m0.254s
    user    0m0.126s
    sys     0m0.127s
    timing strict bytestring with GHC -O2 -fllvm
    
    real    0m0.267s
    user    0m0.132s
    sys     0m0.134s
    timing lazy bytestring with GHC -O2
    
    real    0m0.091s
    user    0m0.023s
    sys     0m0.067s
    timing lazy bytestring with GHC -O2 -fllvm
    
    real    0m0.091s
    user    0m0.021s
    sys     0m0.069s
    
    我使用
    GHC.IO.Buffer
    API尝试了类似的方法:

    module Main where
    
    import System.IO
    import System.Environment
    import GHC.IO.Buffer
    import Data.ByteString as BS
    
    import Control.Monad
    
    -- Copied from cat source code
    bufsize = 1024*128
    
    go handle bufPtr = do
      read <- hGetBuf handle bufPtr bufsize
      when (read > 0) $ do
        hPutBuf stdout bufPtr read
        go handle bufPtr
    
    main = do
      file    <- fmap Prelude.head getArgs
      handle  <- openFile file ReadMode
      buf     <- newByteBuffer bufsize WriteBuffer
    
      withBuffer buf $ go handle
    
    我认为使用buffer API,我们可以在使用原始代码中的
    hGetSome
    时显式避免分配所有的buffer ByTestRing,但我只是在这里猜测,不知道这两个编译代码中到底发生了什么

    更新:在我的笔记本电脑上添加原始代码的性能:

    time ./Cat2 huge > /dev/null
    ./Cat2 huge > /dev/null  0.12s user 0.10s system 99% cpu 0.219 total
    
    更新2:添加一些基本分析结果:

    原始代码:

    Cat2 +RTS -p -RTS huge
    
        total time  =        0.21 secs   (211 ticks @ 1000 us, 1 processor)
        total alloc = 6,954,068,112 bytes  (excludes profiling overheads)
    
    COST CENTRE MODULE  %time %alloc
    
    MAIN        MAIN    100.0  100.0
    
    
                                                           individual     inherited
    COST CENTRE MODULE                   no.     entries  %time %alloc   %time %alloc
    
    MAIN        MAIN                      46           0  100.0  100.0   100.0  100.0
     CAF        GHC.IO.Handle.FD          86           0    0.0    0.0     0.0    0.0
     CAF        GHC.Conc.Signal           82           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding           80           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.FD                 79           0    0.0    0.0     0.0    0.0
     CAF        System.Posix.Internals    75           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding.Iconv     72           0    0.0    0.0     0.0    0.0
    
    缓冲区API代码:

    Cat +RTS -p -RTS huge
    
        total time  =        0.06 secs   (61 ticks @ 1000 us, 1 processor)
        total alloc =   3,487,712 bytes  (excludes profiling overheads)
    
    COST CENTRE MODULE  %time %alloc
    
    MAIN        MAIN    100.0   98.9
    
    
                                                          individual     inherited
    COST CENTRE MODULE                  no.     entries  %time %alloc   %time %alloc
    
    MAIN        MAIN                     44           0  100.0   98.9   100.0  100.0
     CAF        GHC.IO.Handle.FD         85           0    0.0    1.0     0.0    1.0
     CAF        GHC.Conc.Signal          82           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding          80           0    0.0    0.1     0.0    0.1
     CAF        GHC.IO.FD                79           0    0.0    0.0     0.0    0.0
     CAF        GHC.IO.Encoding.Iconv    71           0    0.0    0.0     0.0    0.0
    

    请特别注意分配成本的巨大差异…

    最初的问题让我认为这是关于在提供的确切代码中查找性能问题。由于评论“我希望采用更惯用的/高级的”Haskell解决方案”与该假设相矛盾,我将给出性能合理的惯用Haskell解决方案

    我希望任何熟悉Haskell的随机程序员都能用Lazy ByTestring来解决这个问题。这使得程序员可以简单地指定读取输入和输出的任务,同时让编译器担心缓冲和循环结构的混乱

    模块主要在哪里

    import System.IO
    import System.Environment
    import Data.ByteString.Lazy as BS
    
    import Control.Monad
    
    main :: IO ()
    main = do
      file    <- fmap Prelude.head getArgs
      handle  <- openFile file ReadMode
      buf     <- BS.hGetContents handle
      hPut stdout buf
    
    也就是说,lazy-bytestring解决方案比
    cat
    慢21%。将
    cat
    放在优先缓存行为的最后会导致59毫秒的运行时间,使Haskell解决方案的速度降低51%

    编辑:Dons建议使用内存映射IO可以更准确地模拟猫的行为。我不确定这句话有多准确,但mmap几乎总能带来更好的性能,这种情况当然也不例外:

    timing memory mapped lazy bytestring with GHC -O2
    
    real    0m0.008s
    user    0m0.004s
    sys     0m0.003s
    
    由以下机构制作:

    module Main where
    
    import System.IO (stdout)
    import System.Environment
    import System.IO.Posix.MMap.Lazy
    import Data.ByteString.Lazy (hPut)
    
    import Control.Monad
    
    main :: IO ()
    main = do
      file    <- fmap Prelude.head getArgs
      buf     <- unsafeMMapFile file
      hPut stdout buf
    
    modulemain其中
    导入System.IO(标准输出)
    导入系统。环境
    导入System.IO.Posix.MMap.Lazy
    导入Data.ByteString.Lazy(hPut)
    进口管制
    main::IO()
    main=do
    文件节日后备注:

    我不确定现在的问题是什么,因为人们已经把它推了一点。我想看看
    bytestring mmap
    到底出了什么问题,所以我制作了一个pipes版本来“纠正”它的惰性bytestring模块。因此,我使用
    sibi
    s测试方法组装了所有这些程序。在中,只有两个模块使用了奇特的显式缓冲区管理

    无论如何,这里有一些结果:当我们向右移动时,文件大小增加了10*。有趣的是,看看不同文件大小的程序有多大的不同。不使用
    mmap
    的程序仅在420M处开始将其字符显示为“文件长度的线性”。在这一点上,以及之后,它们几乎完全相同,这表明在较小的尺寸上,相当不同的行为不能太认真。
    mmap
    文件的行为都很相似(彼此之间),有一些奇怪之处(我复制了它们),所有这些都是在OSX上实现的

    4200000           42000000          420000000         4200000000
    
    timing 'cat'
    
    real  0m0.006s    real  0m0.013s    real  0m0.919s    real  0m8.154s
    user  0m0.002s    user  0m0.002s    user  0m0.005s    user  0m0.028s
    sys   0m0.003s    sys   0m0.009s    sys   0m0.223s    sys   0m2.179s
    
    
    timing lazy bytestring - idiomatic Haskell (following Thomas M. DuBuisson) 
    
    real  0m0.009s    real  0m0.025s    real  0m0.894s    real  0m9.146s
    user  0m0.002s    user  0m0.006s    user  0m0.078s    user  0m0.787s
    sys   0m0.005s    sys   0m0.016s    sys   0m0.288s    sys   0m3.001s
    
    
    timing fancy buffering following statusfailed
    
    real  0m0.014s    real  0m0.066s    real  0m0.876s    real  0m8.686s
    user  0m0.005s    user  0m0.028s    user  0m0.278s    user  0m2.724s
    sys   0m0.007s    sys   0m0.035s    sys   0m0.424s    sys   0m4.232s
    
    
    timing fancier use of GHC.Buf following bmk
    
    real  0m0.011s    real  0m0.018s    real  0m0.831s    real  0m8.218s
    user  0m0.002s    user  0m0.003s    user  0m0.034s    user  0m0.289s
    sys   0m0.006s    sys   0m0.013s    sys   0m0.236s    sys   0m2.447s
    
    
    timing Pipes.ByteString following sibi
    
    real  0m0.012s    real  0m0.020s    real  0m0.845s    real  0m8.241s
    user  0m0.003s    user  0m0.004s    user  0m0.020s    user  0m0.175s
    sys   0m0.007s    sys   0m0.014s    sys   0m0.239s    sys   0m2.509s
    
    然后使用
    mmap

    timing Lazy.MMap following dons and Thomas M. DuBuisson 
    
    real  0m0.006s    real  0m0.006s    real  0m0.037s    real  0m0.133s
    user  0m0.002s    user  0m0.002s    user  0m0.006s    user  0m0.051s
    sys   0m0.003s    sys   0m0.003s    sys   0m0.013s    sys   0m0.061
    
    timing Pipes.ByteString.MMap with SafeT machinery
    
    real  0m0.006s    real  0m0.010s    real  0m0.051s    real  0m0.196s
    user  0m0.002s    user  0m0.004s    user  0m0.012s    user  0m0.099s
    sys   0m0.003s    sys   0m0.005s    sys   0m0.016s    sys   0m0.072s
    
    
    timing Pipes.ByteString.MMap 'withFile' style
    
    real  0m0.008s    real  0m0.008s    real  0m0.142s    real  0m0.134s
    user  0m0.002s    user  0m0.002s    user  0m0.007s    user  0m0.046s
    sys   0m0.004s    sys   0m0.004s    sys   0m0.016s    sys   0m0.066s
    

    很高兴看到您还添加了-O2优化。您也可以在此处发布到分析报告的链接。与其使用
    hGetSome
    不如使用
    hGetNonBlocking
    ,它实质上提高了性能通知缓存会显著影响计时。如果我在shell脚本“benchmark”中首先运行
    cat
    ,而不是第二次,它在我的系统上的执行速度将降低约60%。
    -prof
    如果没有任何成本中心,将不会非常有用;试着用
    -fprof auto
    -fprof cafs
    以及
    -prof-prof
    @statusfailed a运行得非常接近,通过testring
    编译它(以及整个
    包,如果可以的话(可能在阴谋集团的沙箱中))。我还在代码中附加了基准测试。请注意,每个Haskell程序也有大约10毫秒的固定开销,因此如果从Haskell计时中减去它,它就更接近于
    cat
    。我知道我在问题中没有具体说明这一点,但我希望使用更惯用的/“高级”Haskell解决方案。虽然投票结果不错。mmap-bytestring在行为上应该更接近猫,使用操作系统进行惰性分配。@DonStewart mmap始终是记住wrt性能的好方法,但这是一种将猫从水里赶出来的wrt性能的方法。我不确定我的gnu猫是否使用mmap。。。事实上,Strues显示了大量的读写。“唐斯沃特,我实际上尝试了TETESPLY MMAP,它和猫差不多。好建议!尽管如此,它还是让我感到不舒服,因为它感觉不地道(尽管Thomas M.DuBuisson对我的自相矛盾说得很对)@ThomasM.DuBuisson Re:self-conflication-我希望能更全面地探索解决方案的空间,这样我就可以选择一个满足我“地道”概念的解决方案,但感谢您指出这一点:-)
    module Main where
    
    import System.IO (stdout)
    import System.Environment
    import System.IO.Posix.MMap.Lazy
    import Data.ByteString.Lazy (hPut)
    
    import Control.Monad
    
    main :: IO ()
    main = do
      file    <- fmap Prelude.head getArgs
      buf     <- unsafeMMapFile file
      hPut stdout buf
    
    4200000           42000000          420000000         4200000000
    
    timing 'cat'
    
    real  0m0.006s    real  0m0.013s    real  0m0.919s    real  0m8.154s
    user  0m0.002s    user  0m0.002s    user  0m0.005s    user  0m0.028s
    sys   0m0.003s    sys   0m0.009s    sys   0m0.223s    sys   0m2.179s
    
    
    timing lazy bytestring - idiomatic Haskell (following Thomas M. DuBuisson) 
    
    real  0m0.009s    real  0m0.025s    real  0m0.894s    real  0m9.146s
    user  0m0.002s    user  0m0.006s    user  0m0.078s    user  0m0.787s
    sys   0m0.005s    sys   0m0.016s    sys   0m0.288s    sys   0m3.001s
    
    
    timing fancy buffering following statusfailed
    
    real  0m0.014s    real  0m0.066s    real  0m0.876s    real  0m8.686s
    user  0m0.005s    user  0m0.028s    user  0m0.278s    user  0m2.724s
    sys   0m0.007s    sys   0m0.035s    sys   0m0.424s    sys   0m4.232s
    
    
    timing fancier use of GHC.Buf following bmk
    
    real  0m0.011s    real  0m0.018s    real  0m0.831s    real  0m8.218s
    user  0m0.002s    user  0m0.003s    user  0m0.034s    user  0m0.289s
    sys   0m0.006s    sys   0m0.013s    sys   0m0.236s    sys   0m2.447s
    
    
    timing Pipes.ByteString following sibi
    
    real  0m0.012s    real  0m0.020s    real  0m0.845s    real  0m8.241s
    user  0m0.003s    user  0m0.004s    user  0m0.020s    user  0m0.175s
    sys   0m0.007s    sys   0m0.014s    sys   0m0.239s    sys   0m2.509s
    
    timing Lazy.MMap following dons and Thomas M. DuBuisson 
    
    real  0m0.006s    real  0m0.006s    real  0m0.037s    real  0m0.133s
    user  0m0.002s    user  0m0.002s    user  0m0.006s    user  0m0.051s
    sys   0m0.003s    sys   0m0.003s    sys   0m0.013s    sys   0m0.061
    
    timing Pipes.ByteString.MMap with SafeT machinery
    
    real  0m0.006s    real  0m0.010s    real  0m0.051s    real  0m0.196s
    user  0m0.002s    user  0m0.004s    user  0m0.012s    user  0m0.099s
    sys   0m0.003s    sys   0m0.005s    sys   0m0.016s    sys   0m0.072s
    
    
    timing Pipes.ByteString.MMap 'withFile' style
    
    real  0m0.008s    real  0m0.008s    real  0m0.142s    real  0m0.134s
    user  0m0.002s    user  0m0.002s    user  0m0.007s    user  0m0.046s
    sys   0m0.004s    sys   0m0.004s    sys   0m0.016s    sys   0m0.066s