String 哈斯克尔有多懒;s`++;`?

String 哈斯克尔有多懒;s`++;`?,string,optimization,haskell,lazy-evaluation,String,Optimization,Haskell,Lazy Evaluation,我很好奇我应该如何改进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而不是list,因为Data.Vector提供了就地操作,可能只是在原始数据中

我很好奇我应该如何改进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而不是list,因为Data.Vector提供了就地操作,可能只是在原始数据中操纵一些索引。我其实不需要亲自跟踪索引以避免过度复制,对吗


我很好奇
++
是如何影响优化的。我可以想象它会产生一个懒惰的字符串thunk,在字符串被读取到那么远之前,它不会进行追加。因此,
a
永远不应该实际附加到
b
上,只要minimum可以提前删除该字符串,比如因为它以某个非常晚的字母开头。这是否正确?

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

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

[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
i、 例如,它必须在遍历结果时“重新构建”整个第一个列表。这对于理解如何以这种方式推理惰性代码非常有帮助

要认识到的关键是,追加并不是一下子完成的;通过首先遍历所有
xs
,然后将
ys
放在
[]
的位置,以增量方式构建一个新的链表

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

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

试试看

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

我希望这会比你拥有的更快,尽管在未绑定的
向量
UArray
上进行索引杂耍可能会更快。但是,这真的是一个瓶颈吗?

如果您对快速串联和快速
拆分感兴趣,请使用

我对您的代码做了一些风格上的修改,使其看起来更像惯用的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
导入符合条件的数据。顺序为S
导入符合条件的数据。可折叠为F
最小旋转::Ord a=>[a]->[a]
最小旋转xs=F.toList
. F.最低限度
. fmap(`swapAt`xs')
. S.elemIndicesL(F.最小x')
$xs'
其中xs'=S.fromList xs
斯瓦帕特n=f。S.splitAt n
其中f(a,b)=b S.>
很好,但可能会切换到
数据。Vector
解决了这个问题,是吗?还是会造成单独的复制惩罚?在这种情况下,我应该简单地创建我自己的
twovectors
类型或其他什么?@JeffBurdges:我已经扩展了我的答案,涵盖了Vectors:)谢谢!另一个小问题:如果我写
minimumrotation x=minimum$map f$elemIndices(minimum x)x其中f I=take(length x)$drop I(x++x)
。当去除
f
时,
长度x
x++x
是否只计算一次?@JeffBurdges:可能,但我;GHC对这种优化持保守态度。您可能应该给
length x
一个名称(与
f
的定义在同一where块中);我不会担心
(x++x)
部分。(请注意,
f
本身已经处于弱头部正常形式,因此永远不会被强迫(“脱驼”);对于不同的
i
值,
f i
将被强迫)@JeffBurdges:这没有帮助;必须将表达式提升到lambda-expression之外。循环是否比
xs++xs
快?我会先假设是的。我认为交换两个
take
s不会影响性能,因为所有这些thunk都必须进行计算,对吗?
cycle xs
只是
fix(xs++)
,所以如果有什么
xs++xs
会更便宜,但我不会担心它;开销将是微乎其微的。交换
take len
map(take len)
不会有任何效果。如果
xs++xs
cycle xs
之间有任何性能差异,如果它不是很小,我会感到惊讶。我不认为交换
take len
map(take len)
会产生可测量的差异,但我还没有对其进行基准测试。@LightnessRacesinOrbit:很明显,你从来没有在游戏中见过Haskell程序!我的幽默和善意的评论被删除了(想想看。啊,这里有几个巧妙的技巧,包括中缀
swapAt
)。lol@JeffBurdges-另一个选项是
(翻转swapAt xs')
,但我个人更喜欢中缀部分。当然,最好始终使用序列,这样
toList
fromList
就不会占用程序很多时间