Реализация Data.STM.LinkedList - PullRequest
1 голос
/ 01 мая 2020

Я смотрю на реализацию Data.STM.LinkedList для высокопроизводительного связанного списка. Глядя на документацию, функция длины запускается в O (n) - почему это так? Была ли какая-то реальная проблема для его реализации в O (1)?

Вот исходный код https://hackage.haskell.org/package/stm-linkedlist-0.1.0.0/docs/src/Data-STM-LinkedList-Internal.html#length

Возможно ли реализовать его в O (1) ? я новичок в Haskell, поэтому я не уверен, что хранение некоторых метаданных о списке проблематично c.

Спасибо!

1 Ответ

2 голосов
/ 06 мая 2020

В первом приближении Haskell является достаточно выразительным языком, поэтому любой алгоритм, реализованный на другом языке общего назначения, также может быть реализован в Haskell при сохранении асимптотических характеристик производительности c. (Это довольно низкая планка. Большинство языков общего назначения являются такими выразительными.)

В частности, хотя Haskell наиболее естественно поддерживает неизменяемые структуры данных, он имеет достаточную поддержку изменяемых данных, что изменяемые структуры данных и их алгоритмы обычно могут быть напрямую переведены в Haskell код. Могут быть некоторые издержки (часто существенные накладные расходы), и изменяемые структуры данных могут быть значительно более неудобными для использования, чем их неизменные кузены, но это все же возможно.

На практике, однако, соответствие Фактическая (в отличие от асимптотики c) производительность C ++ реализации изменяемой структуры данных, вероятно, окажется чрезвычайно сложной, если не невозможной. Может быть разумно получить в 2-3 раза производительность C ++, а получить в 5-10 раз довольно легко (см. Ниже). Однако, если вам нужно соответствовать производительности C ++, вам, вероятно, было бы лучше написать высокопроизводительный мутирующий код на C ++ и использовать FFI (интерфейс внешних функций) для взаимодействия с этим кодом.

В любом случае, «умеренный» Производительность "двусвязный список с O (1) length безусловно возможна, и нет никаких фундаментальных трудностей с поддержкой изменяемых метаданных всего списка. Причина, по которой stm-linkedlist не обеспечивает O (1) length, вероятно, та же самая причина, по которой C ++ гарантировал только O (n) std::list<>::size производительность до C ++ 11 . А именно, во многих практических случаях использования списков с двойной связью не нужно вызывать length / size, а обеспечение производительности O (1) сопряжено с дополнительными расходами на ведение бухгалтерского учета.

В качестве подтверждения концепции Для реализации полностью изменяемого двусвязного списка с функцией длины O (1) достаточно следующих типов данных. Здесь типы и идентификаторы, заканчивающиеся подчеркиванием, предназначены только для внутреннего использования. Список строг в своих указателях (поэтому нет бесконечных списков!), Но ленив в своих значениях.

data List a = List
  { headNode_ :: !(IORef (Node_ a))
  , length_ :: !(IORef Int) }
data Node_ a = Node_
  { prev_ :: !(IORef (Node_ a))
  , next_ :: !(IORef (Node_ a))
  , value_ :: a }

Тип List содержит указатель (т. Е. IORef) на неполный headNode это указывает на начало и конец списка (или на себя для пустого списка), но имеет неопределенное поле значения. Это делает это значение небезопасного узла, поэтому он никогда не должен быть напрямую доступен конечному пользователю. List также содержит указатель на значение длины списка.

Дополнительный тип Node (без подчеркивания) используется для украшения указателя узла соответствующим списком (например, «итератор» из комментариев). ), чтобы сделать метаданные списка доступными для функций, которые в них нуждаются:

data Node a = Node
  { node_ :: !(IORef (Node_ a))
  , list_ :: !(List a) }

Обратите внимание, что List и Node - это типы данных для работы со списками, ориентированные на пользователя.

Вы создаете список empty следующим образом:

empty :: IO (List a)
empty = mdo
  n <- newIORef (Node_ n n undefined)
  List n <$> newIORef 0

Вставка до и после данного узла работает следующим образом. Вот где небезопасное представление головного узла окупается, поскольку алгоритм может рассматривать вставку в начале и конце списка как особые случаи вставки между головным узлом и фактическим узлом списка.

insertBefore :: a -> Node a -> IO (Node a)
insertBefore x Node{node_=rnode2, list_} = do
  Node_{prev_=rnode1} <- readIORef rnode2
  insertBetween_ x list_ rnode1 rnode2

insertAfter :: a -> Node a -> IO (Node a)
insertAfter x Node{node_=rnode1, list_} = do
  Node_{next_=rnode2} <- readIORef rnode1
  insertBetween_ x list_ rnode1 rnode2

insertBetween_ :: a -> List a -> IORef (Node_ a) -> IORef (Node_ a) -> IO (Node a)
insertBetween_ x l rnode1 rnode2 = do
  modifyIORef' (length_ l) succ
  newnode <- newIORef (Node_ rnode1 rnode2 x)
  modifyIORef' rnode1 (\n -> n{next_=newnode})
  modifyIORef' rnode2 (\n -> n{prev_=newnode})
  return $ Node newnode l

Поскольку пользователю не разрешено «иметь» головной узел, нам нужны дополнительные пользовательские функции для вставки в начало и конец списка:

prepend :: a -> List a -> IO (Node a)
prepend x l = insertAfter x (Node (headNode_ l) l)

append :: a -> List a -> IO (Node a)
append x l = insertBefore x (Node (headNode_ l) l)

Обратите внимание, что все вставки go - insertBetween_ который отвечает за увеличение значения длины.

Удаление является простым и равномерным, будь то внутренний узел или узел в начале или конце. Все удаления go с помощью этой функции delete, которая отвечает за уменьшение значения длины.

delete :: Node a -> IO ()
delete Node{node_,list_} = do
  modifyIORef' (length_ list_) pred
  Node_{next_, prev_} <- readIORef node_
  modifyIORef' prev_ (\n -> n{next_=next_})
  modifyIORef' next_ (\n -> n{prev_=prev_})

Удаление головного узла может привести к катастрофе, но пользователи не могут иметь такой Node, поэтому мы в безопасности.

Если у пользователя есть Node, он может перемещаться по списку:

prev :: Node a -> IO (Maybe (Node a))
prev Node{node_, list_} = do
  Node_{prev_} <- readIORef node_
  return $ maybeNode_ prev_ list_

next :: Node a -> IO (Maybe (Node a))
next Node{node_, list_} = do
  Node_{next_} <- readIORef node_
  return $ maybeNode_ next_ list_

maybeNode_ :: IORef (Node_ a) -> List a -> Maybe (Node a)
maybeNode_ n l =
  if n == headNode_ l
  then Nothing
  else Just (Node n l)

Обратите внимание, что мы должны позаботиться о том, чтобы никогда не давать пользователю головной узел, поэтому maybeNode_ здесь проверяет его и возвращает вместо него Nothing.

Для начала пользователь может получить старт или конец List с использованием следующих функций (которые используют prev или next на запрещенном главном узле):

start :: List a -> IO (Maybe (Node a))
start l = next $ Node (headNode_ l) l

end :: List a -> IO (Maybe (Node a))
end l = prev $ Node (headNode_ l) l

Все, чего не хватает, - это несколько разных функций запроса:

value :: Node a -> IO a
value = fmap value_ . readIORef . node_

null :: List a -> IO Bool
null l = (==0) <$> length l

length :: List a -> IO Int
length = readIORef . length_

некоторые утилиты для преобразования в простые списки:

toList :: List a -> IO [a]
toList = toList_ next_

toListRev :: List a -> IO [a]
toListRev = toList_ prev_

toList_ :: (Node_ a -> IORef (Node_ a)) -> List a -> IO [a]
toList_ dir l = go =<< readIORef h
  where h = headNode_ l
        go n = do
          if dir n == h then return []
            else do
            n' <- readIORef (dir n)
            (value_ n':) <$> go n'

и Show экземпляр для отладки:

instance (Show a) => Show (List a) where
  showsPrec d lst = showParen (d > 10) $ showString "fromList " . showsPrec 11 (unsafePerformIO $ toList lst)

ПРЕДУПРЕЖДЕНИЕ: This Show Экземпляр небезопасен, если список видоизменен до полной оценки сгенерированной строки, поэтому его следует использовать только для отладки (и, вероятно, удаления из рабочей версии).

Кроме того, хотя это не является строго обязательным, поскольку мы можем удалить и вставить заново, ни одна уважающая себя изменчивая структура не будет полной без изменения элементов на месте:

modify :: (a -> a) -> Node a -> IO ()
modify f Node{node_} = modifyIORef' node_ (\n -> n { value_ = f (value_ n) })

Вот полный код. (См. Определение ex1 для примера использования.) Вы можете использовать его в качестве отправной точки для своей собственной реализации. Он не проверен и не соответствует стандартам, за исключением нескольких быстрых тестов, которые, вероятно, примерно в 5-10 раз медленнее, чем реализация C ++.

{-# LANGUAGE NamedFieldPuns, RecursiveDo #-}

module LinkedList
  ( List, Node
  , value, null, length
  , empty, prepend, append, insertBefore, insertAfter, delete, modify
  , prev, next, start, end
  , toList, toListRev
  ) where

import System.IO.Unsafe
import Control.Monad
import Prelude hiding (null, length)

import Data.IORef

data List a = List
  { headNode_ :: !(IORef (Node_ a))
  , length_ :: !(IORef Int) }
data Node a = Node
  { node_ :: !(IORef (Node_ a))
  , list_ :: !(List a) }
data Node_ a = Node_
  { prev_ :: !(IORef (Node_ a))
  , next_ :: !(IORef (Node_ a))
  , value_ :: a }

-- unsafe show instance: remove from production version
instance (Show a) => Show (List a) where
  showsPrec d lst = showParen (d > 10) $ showString "fromList " . showsPrec 11 (unsafePerformIO $ toList lst)

value :: Node a -> IO a
value = fmap value_ . readIORef . node_

null :: List a -> IO Bool
null l = (==0) <$> length l

length :: List a -> IO Int
length = readIORef . length_

empty :: IO (List a)
empty = mdo
  n <- newIORef (Node_ n n undefined)
  List n <$> newIORef 0

prepend :: a -> List a -> IO (Node a)
prepend x l = insertAfter x (Node (headNode_ l) l)

append :: a -> List a -> IO (Node a)
append x l = insertBefore x (Node (headNode_ l) l)

insertBefore :: a -> Node a -> IO (Node a)
insertBefore x Node{node_=rnode2, list_} = do
  Node_{prev_=rnode1} <- readIORef rnode2
  insertBetween_ x list_ rnode1 rnode2

insertAfter :: a -> Node a -> IO (Node a)
insertAfter x Node{node_=rnode1, list_} = do
  Node_{next_=rnode2} <- readIORef rnode1
  insertBetween_ x list_ rnode1 rnode2

insertBetween_ :: a -> List a -> IORef (Node_ a) -> IORef (Node_ a) -> IO (Node a)
insertBetween_ x l rnode1 rnode2 = do
  modifyIORef' (length_ l) succ
  newnode <- newIORef (Node_ rnode1 rnode2 x)
  modifyIORef' rnode1 (\n -> n{next_=newnode})
  modifyIORef' rnode2 (\n -> n{prev_=newnode})
  return $ Node newnode l

delete :: Node a -> IO ()
delete Node{node_,list_} = do
  modifyIORef' (length_ list_) pred
  Node_{next_, prev_} <- readIORef node_
  modifyIORef' prev_ (\n -> n{next_=next_})
  modifyIORef' next_ (\n -> n{prev_=prev_})

modify :: (a -> a) -> Node a -> IO ()
modify f Node{node_} = modifyIORef' node_ (\n -> n { value_ = f (value_ n) })

prev :: Node a -> IO (Maybe (Node a))
prev Node{node_, list_} = do
  Node_{prev_} <- readIORef node_
  return $ maybeNode_ prev_ list_

next :: Node a -> IO (Maybe (Node a))
next Node{node_, list_} = do
  Node_{next_} <- readIORef node_
  return $ maybeNode_ next_ list_

maybeNode_ :: IORef (Node_ a) -> List a -> Maybe (Node a)
maybeNode_ n l =
  if n == headNode_ l
  then Nothing
  else Just (Node n l)

start :: List a -> IO (Maybe (Node a))
start l = next $ Node (headNode_ l) l

end :: List a -> IO (Maybe (Node a))
end l = prev $ Node (headNode_ l) l

toList :: List a -> IO [a]
toList = toList_ next_

toListRev :: List a -> IO [a]
toListRev = toList_ prev_

toList_ :: (Node_ a -> IORef (Node_ a)) -> List a -> IO [a]
toList_ dir l = go =<< readIORef h
  where h = headNode_ l
        go n = do
          if dir n == h then return []
            else do
            n' <- readIORef (dir n)
            (value_ n':) <$> go n'

ex1 :: IO (List Int)
ex1 = do
  t <- empty
  mapM_ (flip prepend t) [10,9..1]
  mapM_ (flip append t) [11..20]
  return t
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...