哈斯克尔的“++”多么懒惰?

时间:2012-01-15 19:38:41

标签: string optimization haskell lazy-evaluation

我很好奇我应该如何改进Has​​kell例程的性能,该例程找到字典的字典最小循环旋转。

import Data.List
swapAt n = f . splitAt n where f (a,b) = b++a
minimumrotation x = minimum $ map (\i -> swapAt i x) $ elemIndices (minimum x) x

我想我应该使用Data.Vector而不是列表,因为Data.Vector提供了就地操作,可能只是将一些索引操作到原始数据中。我自己实际上不需要费心去追踪索引,以避免过多的复制,对吗?

我很好奇++如何影响优化。我想它会产生一个懒惰的字符串thunk,直到字符串被读取到远处才会附加。因此,a实际上永远不会被附加到b,只要最小可以提前消除该字符串,就像因为它以一些非常晚的字母开头。这是对的吗?

3 个答案:

答案 0 :(得分:10)

xs ++ ysxs的所有列表单元格中增加了一些开销,但是一旦它到达xs的结尾它就是免费的 - 它只返回ys

查看(++)的定义有助于了解原因:

[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

即,它必须在遍历结果时“重新构建”整个第一个列表。 This article非常有助于理解如何以这种方式推理惰性代码。

要意识到的关键是不是一次性完成追加;首先遍历所有xs,然后将ys放在[]所在的位置,逐步构建新的链接列表。

所以,你不必担心到达b的末尾并突然产生“追加”a的一次性费用;费用分散在b的所有要素上。

矢量完全是另一回事;他们的结构很严格,所以即使只检查xs V.++ ys的第一个元素,也会产生分配新向量并将xsys复制到其中的全部开销 - 就像在严格的语言。这同样适用于可变向量(除了在执行操作时产生成本,而不是强制生成向量时),尽管我认为你必须用这些来编写自己的追加操作。你可以将一堆附加的(不可变的)向量表示为[Vector a]或类似的,如果这对你来说是一个问题,但只是将开销移到你将它展平成一个Vector时,它听起来像你'对可变载体更感兴趣。

答案 1 :(得分:5)

尝试

minimumrotation :: Ord a => [a] -> [a]
minimumrotation xs = minimum . take len . map (take len) $ tails (cycle xs)
  where
    len = length xs

我希望它比你拥有的更快,尽管未装箱的VectorUArray上的索引玩杂耍可能会更快。但是,它真的是一个瓶颈吗?

答案 2 :(得分:3)

如果您对快速连接和快速splitAt感兴趣,请使用Data.Sequence

我对你的代码进行了一些风格修改,使它看起来更像是惯用的Haskell,但逻辑完全相同,除了Seq之间的一些转换:

import qualified Data.Sequence as S
import qualified Data.Foldable as F

minimumRotation :: Ord a => [a] -> [a]
minimumRotation xs = F.toList
                   . F.minimum
                   . fmap (`swapAt` xs')
                   . S.elemIndicesL (F.minimum xs')
                   $ xs'
  where xs' = S.fromList xs
        swapAt n = f . S.splitAt n
          where f (a,b) = b S.>< a