【问题标题】:Traversing ByteStrings遍历字节串
【发布时间】:2014-04-03 22:59:37
【问题描述】:

我正在阅读some random blog,其中有人试图在 Haskell 中执行一个简单的字符串处理操作并且得到相当慢的代码。他的一些问题(最终,在页面下方)代码:

  1. 立即读入整个文件。
  2. 他使用相对昂贵的isSpace,然后将生成的程序与只考虑简单空格和换行符的 C 代码进行比较。
  3. 他使用 scanl 的方式看起来对管道非常不友好,在不需要时使用计算字符作为每个步骤的输入。

我认为最自然的方法是使用懒惰的ByteStrings(正如他早期的一些尝试所做的那样)并废弃scanl 以支持zipWith',将字符串拉上并移动字符串一:zipWith f s (cons ' ' s)

问题

用自身的移位版本压缩一个懒惰的ByteString 并没有利用两个字符串之间的关系。它对块尾和字符串尾执行许多不必要的检查。我确信我可以编写一个专门的函数来遍历带有两个字符“窗口”的ByteString,并且我确信一个比我可以编写一个更好的程序员来利用块表示的细节,但是我更愿意找到一种更易于访问的方法。有什么想法吗?

编辑添加:另一种方法可能是使用foldr 生成ByteString 构建器,遵循相同的通用方法,但使用(希望未装箱的)元组以避免数据依赖性;我不确定我是否完全了解那些建设者或他们的效率。

【问题讨论】:

    标签: string haskell traversal


    【解决方案1】:

    我将使用以下导入。

    import Data.Char 
    import Data.List           
    import qualified Data.Text.Lazy as T                      
    
    import Criterion.Main
    import Test.QuickCheck
    

    与博客文章中的此参考实现相比,我设法获得了惊人的速度:

    capitalize :: T.Text -> T.Text
    capitalize = T.tail . T.scanl (\a b -> if isSpace a then toUpper b else b) ' '
    

    使用mapAccumL 要快得多。这是StringText 版本。

    {-# INLINE f #-}
    f a b = (b, if isSpace a then toUpper b else b)
    
    string :: String -> String
    string = snd . mapAccumL f ' '
    
    text :: T.Text -> T.Text
    text = snd . T.mapAccumL f ' '
    

    首先,让我们确保优化是有效的

    λ. quickCheck $ \xs -> 
        capitalize (T.pack xs) == text (T.pack xs)
    +++ OK, passed 100 tests.
    

    现在获取来自criterion 的一些基准测试结果,在 3.2 M 的 Lorem Ipsum 文件上运行每个函数。这是我们的参考速度。

    benchmarking reference
    collecting 100 samples, 1 iterations each, in estimated 56.19690 s
    mean: 126.4616 ms, lb 126.0039 ms, ub 128.6617 ms, ci 0.950
    std dev: 4.432843 ms, lb 224.7290 us, ub 10.55986 ms, ci 0.950
    

    String 仅比优化参考 Text 版本慢 30% 左右,而使用 TextmapAccumL 版本几乎快两倍!

    benchmarking string
    collecting 100 samples, 1 iterations each, in estimated 16.45751 s
    mean: 165.1451 ms, lb 165.0927 ms, ub 165.2112 ms, ci 0.950
    std dev: 301.0338 us, lb 250.2601 us, ub 370.2991 us, ci 0.950
    
    benchmarking text
    collecting 100 samples, 1 iterations each, in estimated 16.88929 s
    mean: 67.67978 ms, lb 67.65432 ms, ub 67.72081 ms, ci 0.950
    std dev: 162.8791 us, lb 114.9346 us, ub 246.0348 us, ci 0.950
    

    但还有更容易获得的收益。 Data.Char.isSpace 以其性能问题而闻名,所以让我们试试快速的Data.Attoparsec.Char8.isSpace。我们的quickcheck 测试不会通过,但性能很棒。

    benchmarking string/atto
    collecting 100 samples, 1 iterations each, in estimated 12.91881 s
    mean: 129.2176 ms, lb 129.1328 ms, ub 129.4941 ms, ci 0.950
    std dev: 705.3433 us, lb 238.2757 us, ub 1.568524 ms, ci 0.950
    
    benchmarking text/atto
    collecting 100 samples, 1 iterations each, in estimated 15.76300 s
    mean: 38.63183 ms, lb 38.62850 ms, ub 38.63730 ms, ci 0.950
    std dev: 21.41514 us, lb 15.27777 us, ub 33.98801 us, ci 0.950
    

    我们现在比原始参考快了大约3x。为了比较,非常快的python代码(只是调用C),

    print open('lorem.txt').read().title()
    

    翻阅30ms中的文本文件。

    【讨论】:

      【解决方案2】:

      延迟 I/O 可能是个问题,但它是处理这个小任务的最简单方法。

      import Data.Text.Lazy (toTitle)
      import Data.Text.Lazy.IO (readFile, putStr)
      import Prelude hiding (readFile, putStr)
      
      main = readFile "file" >>= putStr . toTitle
      

      它实际上会花时间正确地处理 Unicode(分词和标题大小写),但这可能是您想要的。如果你想避免 Lazy I/O,pipes-text 包应该产生一些不大的东西。

      如果您真的想将所有内容都视为 ASCII 并假设所有单词都以字母开头,我仍然认为惰性 I/O 在这里是一个胜利,但它有点复杂。

      import Data.Bits (.&.)
      import Data.ByteString.Lazy (ByteString, cons', putStrLn, readFile, uncons)
      import Data.ByteString.Lazy.Char8 (lines, unlines, unwords, words)
      import Data.Word (Word8)
      import Prelude hiding (putStrLn, readFile, lines, unlines, unwords, words)
      
      capitalize :: ByteString -> ByteString
      capitalize word = case uncons word of
        Just (h, t) -> cons' (h .|. complement 32) t
        Nothing     -> word
      
      main = readFile "file"
         >>= putStrLn . unlines
                      . map (unwords . map capitalize . words)
                      . lines
      

      同样,避免惰性 I/O 就像使用管道字节串一样简单。

      还有一个关于该帖子here 的reddit 线程,它们似乎从Builder 抽象中获得了出色的性能,以及更好的大写方式。构建器抽象可能会比我的 bytestring hack 更快,因为它会在写入输出数据之前更好地分块。

      【讨论】:

      • 使用wordsunwords 会丢失标签等,并且胡乱检查空字确实看起来像一个丑陋的黑客。部分问题是各种“拆分”功能通常会消耗分隔符,这并不总是正确的。半相关地,在行/非行中嵌套单词/非单词在这里是丑陋的,因为在问题描述中空格的处理和换行符的处理之间没有真正的区别。
      猜你喜欢
      • 1970-01-01
      • 2018-05-15
      • 1970-01-01
      • 2011-08-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多