Как лениво Haskell `++`?
Мне интересно, как мне следует улучшить производительность подпрограммы 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 предоставляет операции на месте, возможно, просто манипулируя некоторыми индексами в исходных данных. На самом деле мне не нужно беспокоиться о том, чтобы отслеживать индексы, чтобы избежать избыточного копирования, верно?
Мне любопытно, как ++
влияет на оптимизацию. Я бы предположил, что это создает ленивый струнный тон, который никогда не добавляет, пока строка не будет прочитана так далеко. Ergo, a
никогда не должен быть добавлен к b
, когда минимум может устранить эту строку раньше, например, потому что она начинается с более поздней буквы. Правильно ли это?
Ответы
Ответ 1
xs ++ ys
добавляет некоторые издержки во всех ячейках списка из xs
, но как только он достигает конца xs
, он освобождается - он просто возвращает ys
.
Глядя на определение (++)
, можно понять, почему:
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
i.e., он должен "перестроить" весь первый список по мере прохождения результата. Эта статья очень полезна для понимания того, как рассуждать о ленивом коде таким образом.
Ключом к пониманию является то, что добавление не выполняется сразу; новый связанный список поэтапно строится, сначала пройдя через все xs
, а затем помещая ys
, где будет []
.
Итак, вам не нужно беспокоиться о достижении конца b
и внезапном возникновении одноразовой стоимости "добавления" a
; стоимость распределяется по всем элементам b
.
Векторы - это совсем другое дело; они строги по своей структуре, поэтому даже рассмотрение только первого элемента xs V.++ ys
несет на себе все накладные расходы, связанные с распределением нового вектора и копированием xs
и ys
на него - как на строгом языке. То же самое относится к изменяемым векторам (за исключением того, что стоимость возникает при выполнении операции, а не при форсировании результирующего вектора), хотя я думаю, что вам придется писать свою собственную операцию добавления с ними в любом случае. Вы можете представить кучу добавленных (неизменяемых) векторов как [Vector a]
или похожих, если это проблема для вас, но это просто перемещает накладные расходы, когда вы сплющиваете его обратно в один вектор, и это звучит так, будто вы больше интересующихся изменчивыми векторами.
Ответ 2
Try
minimumrotation :: Ord a => [a] -> [a]
minimumrotation xs = minimum . take len . map (take len) $ tails (cycle xs)
where
len = length xs
Я ожидаю, что это будет быстрее, чем у вас, хотя индексный жонглирование на unboxed Vector
или UArray
, вероятно, будет еще быстрее. Но, действительно ли это узкое место?
Ответ 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