Подтвердить что ты не робот

STUArray s я e - космическая утечка только тогда, когда я == Int?

Меня смущает поведение следующего:

import Data.Int
import Data.Array.ST
import Control.Monad.ST

{-# INLINE fib #-}
fib _ 0 = return 0
fib _ 1 = return 1
fib c n = do
  f1 <- memo c (fib c) (n-1)
  f2 <- memo c (fib c) (n-2)
  return (f1+f2)

newtype C a = C a

{-# INLINE memo #-}
memo (C a) f k = do
  e <- readArray a k
  if e == (-1)
     then do
       v <- f k
       writeArray a k v
       return v
     else return e

evalFib :: Int -> Int
evalFib n = runST $ do
  a <- newArray (0,n) (-1) :: ST s (STUArray s Int Int)
  fib (C a) n

main = print $ evalFib 120000

Когда скомпилировано с -O2, он переполняет стек (показывает 20M используемой памяти). Запутанная часть состоит в том, что она работает нормально (без и используемой памяти 9M), если выполнено одно из следующих изменений:

  • Int64 используется вместо Int: (давая evalFib :: Int64 -> Int и STUArray s Int64 Int). Фактически любые Int* (Int32, Int16 и т.д.) Будут выполнять трюк, а также Word или Word*;
  • newtype C a удаляется из изображения;
  • data C a = C !a используется вместо newtype

Я пытаюсь понять это поведение: это ошибка в модуле GHC/array (он показывает одинаковое поведение в 7.4.2 и 7.6.2), или он должен работать таким образом?

PS Самое смешное, что, когда я пытаюсь скомпилировать его с помощью ghc array.hs -O2 -fext-core, чтобы увидеть различия в ядре, выпущенные как версии GHC, терпят неудачу с помощью "ghc: panic!" ( "невозможное" произошло) ". Не повезло и здесь.

4b9b3361

Ответ 1

Глядя на ядро ​​из 7.6.1, с -O2 и -dsuppress-uniques, функция, выполняющая работу, Main.main_$spoly_$wa является структурно (почти) идентичной, использую ли я int или Int64 как индекс тип. Поскольку ядро ​​длинное и сложное, здесь вывод diff:

$ diff Int_core.dump-simpl Int64_core.dump-simpl 
11,12c11,12
<           (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
<           (Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int))
---
>           (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)
>           (Main.C (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int))
26,27c26,27
<             (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
<             (Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)))
---
>             (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)
>             (Main.C (Data.Array.Base.STUArray s GHC.Int.Int64 GHC.Types.Int)))

Различные типы индексов, это, конечно, разные.

33,40d32
<           l :: GHC.Types.Int
<           [LclId]
<           l = GHC.Types.I# sc } in
<         let {
<           u :: GHC.Types.Int
<           [LclId]
<           u = GHC.Types.I# sc1 } in
<         let {

Для типа индекса int GHC создает несколько более информативные ошибки для индексов вне границ, для чего ему нужна нижняя и верхняя граница допустимых индексов. (Реализация index по умолчанию не переопределена в instance Ix Int64.)

45,46c37
<           GHC.Types.False ->
<             case poly_$w$j5 (GHC.Types.I# a) l u of wild2 { };
---
>           GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild1 { };

Различные ошибки, indexError и hopelessIndexError. Следующие различия также касаются только ошибок индекса.

49,50c40
<               GHC.Types.False ->
<                 case poly_$w$j5 (GHC.Types.I# a) l u of wild2 { };
---
>               GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild2 { };
58c48
<                     case poly_$w$j4 y (GHC.Types.I# sc2) of wild3 { };
---
>                     case poly_$w$j3 y (GHC.Types.I# sc2) of wild4 { };
62c52
<                         case poly_$w$j4 y (GHC.Types.I# sc2) of wild5 { };
---
>                         case poly_$w$j3 y (GHC.Types.I# sc2) of wild5 { };
77,78c67
<                                 GHC.Types.False ->
<                                   case poly_$w$j3 (GHC.Types.I# a1) l u of wild6 { };
---
>                                 GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild6 { };
81,82c70
<                                     GHC.Types.False ->
<                                       case poly_$w$j3 (GHC.Types.I# a1) l u of wild7 { };
---
>                                     GHC.Types.False -> case GHC.Arr.hopelessIndexError of wild7 { };

Теперь еще раз разный тип индекса:

110c98
<                                                                       GHC.Types.Int
---
>                                                                       GHC.Int.Int64
152c140
<                                                 s GHC.Types.Int GHC.Types.Int>)>)
---
>                                                 s GHC.Int.Int64 GHC.Types.Int>)>)

И, наконец, 0 и 1 получили разные имена верхнего уровня.

177,178c165,166
<       0 -> (# sc5, lvl5 #);
<       1 -> (# sc5, lvl6 #)
---
>       0 -> (# sc5, lvl #);
>       1 -> (# sc5, lvl1 #)

Таким образом, весь код, который выполняет фактическую работу, идентичен. Тем не менее, один вызывает переполнение стека (хотя достаточно только -K9M [-K8731K здесь, -K8730K not]), а другой нет.

Разница действительно вызвана ошибками индекса. Код с индексами int выделяет два вложенных в квадрат int в каждом рекурсивном вызове, который не выделяет код Int64, потому что

Main.main_$spoly_$wa [Occ=LoopBreaker]
  :: forall s.
     GHC.Prim.Int#
     -> GHC.Prim.Int#
     -> GHC.Prim.Int#
     -> GHC.Prim.MutableByteArray# s
     -> (GHC.Prim.~#)
          *
          (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int)
          (Main.C (Data.Array.Base.STUArray s GHC.Types.Int GHC.Types.Int))
     -> GHC.Prim.Int#
     -> GHC.Prim.State# s
     -> (# GHC.Prim.State# s, GHC.Types.Int #)

содержит две ссылки на массив.

Это использует больше стека, и эти бокс-теги int должны быть собраны в мусор, что приводит к значительно большим значениям GC. Кроме того, thunk для индексной ошибки немного больше, чем hopelessIndexError thunk.

Теперь, если вы поможете компилятору

  • удаление оболочки newtype
  • делает функцию строгой в массиве (с помощью шаблонов bang или data C a = C !a)

или некоторые другие способы, он создает лучший код, который управляет без для данного аргумента, поскольку в рабочем документе имеется только одна ссылка на массив, и, следовательно, распределение помеченных int для границ isn Не нужно.

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

Ответ 2

Ваша первоначальная программа, как вы говорите, переполняет стек:

$ ghc -O2 A.hs --make -rtsopts
$ ./A +RTS -s
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
      21,213,928 bytes allocated in the heap
       3,121,864 bytes copied during GC
       5,400,592 bytes maximum residency (4 sample(s))
          20,464 bytes maximum slop
              20 MB total memory in use (0 MB lost due to fragmentation)

Пока мы переходим к Int64, он отлично работает:

$ time ./A
-2092835058118682368
./A  0.03s user 0.01s system 92% cpu 0.050 total

Итак, где утечка?

Просто визуально проверяя свой код, есть очевидная проблема:

  • Фиб ленив в аргументе массива

Вы также можете вывести это из поведения, которое вы видели:

newtype C a is removed from the picture;

data C a = C !a is used instead of newtype

Все, на каком сервере выставлять массив анализатору строгости.

Если вы создаете фибр в массиве:

{-# LANGUAGE BangPatterns #-}

{-# INLINE fib #-}
fib !_ 0 = return 0
fib !_ 1 = return 1
fib !c n = do
  f1 <- memo c (fib c) (n-1)
  f2 <- memo c (fib c) (n-2)
  return (f1+f2)

Затем он просто работает:

$ time ./A
-2092835058118682368
./A  0.03s user 0.01s system 89% cpu 0.052 total

Итак, почему он протекает с одним типом, но не с другим? Думаю, вам повезло с Int64. Но я считаю это "ошибкой", возможно, в правилах перезаписи для различных типов num.

Если посмотреть на выход упрощения, мы получим совсем другой запуск правил перезаписи, запущенных в случае Int64, против случая Int. Поскольку базовые функции часто индексируются Int, вы в конечном итоге выполняете различные оптимизации, используя общий тип Int. В этом случае достаточно путать анализатор строгости.

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