【问题标题】:How lazy is Haskell's `++`?Haskell 的 `++` 有多懒?
【发布时间】:2012-01-15 19:38:41
【问题描述】:

我很好奇应该如何提高 Haskell 例程的性能,该例程可以找到字符串的字典顺序最小循环旋转。

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 上,只要 minimum 可以尽早消除该字符串,例如因为它以一些非常晚的字母开头。这是正确的吗?

【问题讨论】:

  • @LightnessRacesinOrbit:显然你从未在benchmarks game 中看到过 Haskell 程序!
  • 我幽默善意的评论被删除了。 :( 去图。

标签: string optimization haskell lazy-evaluation


【解决方案1】:

xs ++ ys 在来自xs 的所有列表单元格中增加了一些开销,但一旦到达xs 的末尾,它是免费的——它只是返回ys

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

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

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

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

因此,您不必担心到达b 的末尾并突然产生“附加”a 的一次性成本;成本分布在b 的所有元素上。

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

【讨论】:

  • 很好,但大概切换到Data.Vector 可以解决这个问题,是吗?还是会产生单独的复制惩罚?在这种情况下,我应该简单地创建自己的twovectors 类型或其他什么?
  • @JeffBurdges:我的答案已经扩展到了 Vectors :)
  • 谢谢!另一个小问题:如果我写了minimumrotation x = minimum $ map f $ elemIndices (minimum x) x where f i = take (length x) $ drop i (x++x)length xx++x 是否仅在 f 被取消时被评估一次?
  • @JeffBurdges:也许吧,但我wouldn't count on it; GHC 对这种优化持保守态度。您可能应该给length x 一个名称(在与f 的定义相同的 where 块中);我不会担心(x++x) 部分。 (请注意,f 本身已经处于弱头正常形式,因此永远不会被强制(“dethunked”);对于不同的 i 值,f i 将被强制。)
  • @JeffBurdges:这无济于事;您必须将表达式提升到 lambda 表达式之外。
【解决方案2】:

试试

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

我希望它比您拥有的更快,尽管在未装箱的 VectorUArray 上进行索引处理可能会更快。但是,真的是瓶颈吗?

【讨论】:

  • 循环比xs++xs快吗?我会假设是先验的。我是否正确交换两个 takes 不应该影响性能,因为无论如何都必须计算所有这些 thunk?
  • cycle xs 只是fix (xs ++),所以如果有的话xs ++ xs 会更便宜,但我不会担心;开销将是微不足道的。交换take lenmap (take len) 将没有效果。
  • 如果xs ++ xscycle xs 在这里有任何性能差异,如果不是微不足道的话,我会感到惊讶。我不认为交换 take lenmap (take len) 会产生可衡量的差异,但我没有对其进行基准测试。
【解决方案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

【讨论】:

  • 啊,有几个巧妙的技巧,包括中缀swapAt。大声笑
  • @JeffBurdges - 另一种选择是(flip swapAt xs'),但我个人更喜欢中缀部分。
  • 当然最好一直使用序列,这样toListfromList 就不会占用程序太多时间
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
  • 2015-09-06
  • 1970-01-01
  • 2015-09-06
  • 2016-12-14
  • 2014-03-23
  • 1970-01-01
相关资源
最近更新 更多