穿越ByteStrings

时间:2014-04-02 20:58:14

标签: string haskell traversal

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

  1. 立即读入整个文件。
  2. 他使用相对昂贵的isSpace,然后将生成的程序与仅考虑简单空格和换行符的C代码进行比较。
  3. 他使用scanl的方式看起来非常不管道,在没有必要的情况下使用计算字符作为每个步骤的输入。
  4. 我认为,最自然的做法是使用懒惰ByteString s(正如他之前的一些尝试所做的那样)并废弃scanl而转而使用zipWith',字符串移位超过一个字符串:zipWith f s (cons ' ' s)

    问题

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

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

2 个答案:

答案 0 :(得分:2)

我将使用以下导入。

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的一些基准测试结果,在Lorem Ipsum的3.2 M文件上运行每个函数。这是我们的参考速度。

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%,使用mapAccumL的{​​{1}}版本几乎快两倍!

Text

但是还有更轻松的收获。 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 。为了比较,非常快的python代码(它只是调用C),

3x

翻阅print open('lorem.txt').read().title() 中的文字文件。

答案 1 :(得分:0)

懒惰的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就像使用pipes-bytestring一样简单。

还有一篇关于该帖here的reddit帖子,他们似乎从Builder抽象中获得了很好的表现,再加上一种更好的上层框架。构建器抽象可能会比我的bytestring hack更快,因为它会在写入之前更好地对输出数据进行分块。