Как списки реализованы в Haskell (GHC)? - PullRequest
40 голосов
/ 22 апреля 2010

Мне просто было любопытно узнать о некоторых точных деталях реализации списков в Haskell (ответы, специфичные для GHC, хороши) - это наивные связанные списки, или у них есть какие-то специальные оптимизации? Более конкретно:

  1. Должны ли length и (!!) (например) перебирать список?
  2. Если это так, кэшируются ли их значения каким-либо образом (т. Е. Если я дважды вызываю length, придется ли это повторять оба раза)?
  3. Включает ли доступ к задней части списка итерацию по всему списку?
  4. Запоминаются ли бесконечные списки и их списки? (т. е. для fib = 1:1:zipWith (+) fib (tail fib) будет ли каждое значение вычисляться рекурсивно или оно будет зависеть от предыдущего вычисленного значения?)

Любые другие интересные детали реализации приветствуются. Заранее спасибо!

Ответы [ 3 ]

34 голосов
/ 22 апреля 2010

Списки не имеют специальной оперативной обработки в Haskell.Они определены так:

data List a = Nil | Cons a (List a)

Просто с некоторыми специальными обозначениями: [a] для List a, [] для Nil и (:) для Cons.Если бы вы определили одно и то же и переопределили все операции, вы получите одинаковую производительность.

Таким образом, списки Haskell являются односвязными.Из-за лени они часто используются как итераторы.sum [1..n] работает в постоянном пространстве, поскольку неиспользуемые префиксы этого списка отбираются мусором по мере увеличения суммы, а хвосты не генерируются до тех пор, пока они не потребуются.

Что касается # 4: all значения в Haskell запоминаются, за исключением того, что функции не сохраняют таблицу памятки для своих аргументов.Поэтому, когда вы определяете fib так, как вы это сделали, результаты будут кэшироваться, и к n-му числу Фибоначчи будет обращаться за O (n) время.Однако, если вы определили это, по-видимому, эквивалентным образом:

-- Simulate infinite lists as functions from Integer
type List a = Int -> a

cons :: a -> List a -> List a
cons x xs n | n == 0    = x
            | otherwise = xs (n-1)

tailF :: List a -> List a
tailF xs n = xs (n+1)

fib :: List Integer
fib = 1 `cons` (1 `cons` (\n -> fib n + tailF fib n))

(Найдите время, чтобы заметить сходство с вашим определением)

Тогда результаты не будут разделены, и n-е число Фибоначчибудут доступны в O (FIB N) (экспоненциальный) времени.Вы можете убедить функции для совместного использования с библиотекой напоминаний, например data-memocombinators .

10 голосов
/ 22 апреля 2010

Если это так, кэшируются ли их значения каким-либо образом (т. Е. Если я дважды вызову length, придется ли его повторять оба раза)?

GHC не выполняет полное устранение общей субэкспрессии . Например:

{-# NOINLINE aaaaaaaaa #-}
aaaaaaaaa :: [a] -> Int
aaaaaaaaa x = length x + length x

{-# NOINLINE bbbbbbbbb #-}
bbbbbbbbb :: [a] -> Int
bbbbbbbbb x = l + l where l = length x

main = bbbbbbbbb [1..2000000] `seq` aaaaaaaaa [1..2000000] `seq` return ()

Дает -ddump-simpl:

Main.aaaaaaaaa [NEVER Nothing] :: forall a_adp.
                                  [a_adp] -> GHC.Types.Int
GblId
[Arity 1
 NoCafRefs
 Str: DmdType Sm]
Main.aaaaaaaaa =
  \ (@ a_ahc) (x_adq :: [a_ahc]) ->
    case GHC.List.$wlen @ a_ahc x_adq 0 of ww_anf { __DEFAULT ->
    case GHC.List.$wlen @ a_ahc x_adq 0 of ww1_Xnw { __DEFAULT ->
    GHC.Types.I# (GHC.Prim.+# ww_anf ww1_Xnw)
    }
    }

Main.bbbbbbbbb [NEVER Nothing] :: forall a_ado.
                                  [a_ado] -> GHC.Types.Int
GblId
[Arity 1
 NoCafRefs
 Str: DmdType Sm]
Main.bbbbbbbbb =
  \ (@ a_adE) (x_adr :: [a_adE]) ->
    case GHC.List.$wlen @ a_adE x_adr 0 of ww_anf { __DEFAULT ->
    GHC.Types.I# (GHC.Prim.+# ww_anf ww_anf)
    }

Обратите внимание, что aaaaaaaaa вызывает GHC.List.$wlen дважды.

(Фактически, поскольку x необходимо сохранить в aaaaaaaaa, это более чем в 2 раза медленнее, чем bbbbbbbbb.)

10 голосов
/ 22 апреля 2010

Насколько я знаю (я не знаю, насколько это зависит от GHC)

  1. length и (!!) НЕОБХОДИМО перебирать список.

  2. Я не думаю, что есть какие-либо специальные оптимизации для списков, но есть метод, который применяется ко всем типам данных.

    Если у вас есть что-то вроде

    foo xs = bar (length xs) ++ baz (length xs)
    

    , тогда length xs будет вычислено дважды.

    Но если вместо этого у вас есть

    foo xs = bar len ++ baz len
      where len = length xs
    

    тогда он будет вычислен только один раз.

  3. Да.

  4. Да, после вычисления части именованного значения оно сохраняется до тех пор, пока имя не выйдет из области видимости. (Язык не требует этого, но я так понимаю, что реализации ведут себя.)

...