String 通过测试环遍历
我在读一篇文章,有人试图在Haskell中执行一个简单的字符串处理操作,但得到的代码相当慢。他的(最终,下一页a方式)代码的一些问题:String 通过测试环遍历,string,haskell,traversal,String,Haskell,Traversal,我在读一篇文章,有人试图在Haskell中执行一个简单的字符串处理操作,但得到的代码相当慢。他的(最终,下一页a方式)代码的一些问题: 整个文件一次读入 他使用相对昂贵的isSpace,然后将生成的程序与只考虑简单空格和换行符的C代码进行比较 他使用scanl的方式看起来非常不友好,在不必要的情况下使用计算字符作为每个步骤的输入 我认为,最自然的方法是使用lazyByteStrings(就像他以前的一些尝试那样),并放弃scanl,转而使用zipWith',将字符串压缩到一个字符串上:zipWi
isSpace
,然后将生成的程序与只考虑简单空格和换行符的C代码进行比较scanl
的方式看起来非常不友好,在不必要的情况下使用计算字符作为每个步骤的输入ByteString
s(就像他以前的一些尝试那样),并放弃scanl
,转而使用zipWith'
,将字符串压缩到一个字符串上:zipWith f s(cons)
问题
通过testring压缩一个懒惰的本身的移位版本不会利用两个字符串之间的关系。它对块的结尾和字符串的结尾执行许多不必要的检查。我确信我可以编写一个专门的函数,用两个字符的“窗口”遍历ByteString
,我确信我是一个比我更好的程序员,可以编写一个利用块表示细节的函数,但我更愿意找到一种更易访问的方法。有什么想法吗
编辑添加:另一种方法可能是使用foldr
生成ByteString
builder,遵循相同的一般方法,但使用(希望是未装箱的)元组来避免数据依赖性;我不确定我是否完全理解这些构建器或它们的效率。惰性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(分词和标题大小写),但这可能是您想要的。如果您想避免懒惰的I/O,那么pipes文本包应该生成一些不大的内容
如果你真的想把所有的东西都当作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就像使用管道bytestring一样简单
这篇文章还有一个reddit线程,他们似乎从构建器抽象中获得了很好的性能,加上一种更好的上框方式。构建器抽象可能会比我的bytestring hack更快,因为它在写入输出数据之前会更好地将其分块。我将使用以下导入
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
要快得多。以下是字符串
和文本
版本
{-# 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.
现在,对于来自标准的一些基准测试结果
,在Lorem Ipsum的3.2m文件上运行每个函数。这是我们的参考速度
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%,而使用Text
的mapAccumL
版本的速度几乎是优化后的两倍
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
我们现在比原始参考快了约3倍。相比之下,非常快的python代码(只是调用C)
使用words
和unwords
在30ms
中撕开文本文件,丢失制表符等等,到处检查空单词确实看起来像一个丑陋的黑客。问题的一部分是,各种“split-on”函数通常使用分隔符,这并不总是正确的。半相关的是,在行/未行中嵌套单词/未用词在这里很难看,因为在问题描述中对空格的处理和对新行的处理之间没有真正的区别。
print open('lorem.txt').read().title()