Почему бессмысленная версия моей функции использует гораздо больше памяти

Я работал над проблемой Project Euler и получил файл на Haskell, который включал функцию, которая выглядела следующим образом:

matches :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
matches f cs = foldr (\(cs', n) a -> fromBool (f cs cs') * n + a) 0

С fromBool импортировано из Foreign.Marshal.Utils просто быстро конвертировать True в 1 а также False в 0,

Я пытался получить немного больше скорости от своего решения, поэтому я попытался переключиться с foldr в foldl' (переключение аргументов в процессе), как я предполагал foldr не имеет особого смысла использовать на числах.

Переключение с foldr в foldl' заставил меня выделить более чем в два раза больше памяти в соответствии с профайлером GHC.

Ради интереса я также решил заменить лямбду на версию функции без точек:

matches :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
matches f cs = foldr ((+) . uncurry ((*) . fromBool . f cs)) 0

Это привело к увеличению моего выделения памяти в 20 раз от foldr версия.

Теперь это не так уж важно, поскольку даже в 20-кратном случае общее выделение памяти составляло всего около 135Mb и время выполнения программы было относительно незатронутым, во всяком случае, версии с более высоким распределением памяти работали немного быстрее.

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

РЕДАКТИРОВАТЬ:

GHC версия 7.10.2, скомпилированная с -O2 -prof -fprof-auto, Выполнено с +RTS -p,

РЕДАКТИРОВАТЬ 2:

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

СПОЙЛЕРЫ НИЖЕ:

{-# LANGUAGE NoMonomorphismRestriction #-}

import Control.Monad
import Data.List
import Foreign.Marshal.Utils

data Color = Red | Green | Blue deriving (Eq, Enum, Bounded, Show)

colors :: [Color]
colors = [Red ..]

matches :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
matches f x = foldr ((+) . uncurry ((*) . fromBool . f x)) 0
-- matches f x = foldr (\(y, n) a -> fromBool (f x y) * n + a) 0
-- matches f x = foldl' (\a (y, n) -> fromBool (f x y) * n + a) 0

invert :: [([Color], Int)] -> [([Color], Int)]
invert rs = (\cs -> (cs, matches valid cs rs)) <$> choices
  where
    len = maximum $ length . fst <$> rs
    choices = replicateM len colors
    valid (x : xs) (y : ys) = x /= y && valid xs ys
    valid _ _ = True

expand :: [([Color], Int)] -> [([Color], Int)]
expand rs = (\cs -> (cs, matches valid cs rs)) <$> choices
  where
    len = maximum $ length . fst <$> rs
    choices = replicateM (len + 1) colors
    valid (x1 : x2 : xs) (y : ys) = x1 /= y && x2 /= y && valid (x2 : xs) ys
    valid _ _ = True

getRow :: Int -> [([Color], Int)]
getRow 1 = flip (,) 1 . pure <$> colors
getRow n = expand . invert $ getRow (n - 1)

result :: Int -> Int
result n = sum $ snd <$> getRow n

main :: IO ()
main = print $ result 8

1 ответ

Решение

Примечание: этот пост написан на грамотном языке Haskell. Скопируйте его в файл, сохраните как *.lhs и скомпилируйте / загрузите в GHC(i). Кроме того, я начал писать этот ответ до того, как вы отредактировали свой код, но урок остался прежним.

TL; DR

Prelude функция uncurry слишком ленив, в то время как ваш шаблон достаточно строг.

Слово предостережения и отказ от ответственности

Мы входим в волшебное, странное место. Осторожно. Кроме того, мои основные способности являются зачаточными. Теперь, когда я потерял весь свой авторитет, давайте начнем.

Протестированный код

Чтобы узнать, где мы получаем дополнительные требования к памяти, полезно иметь более двух функций.

> import Control.Monad (forM_)

Это ваш оригинальный, неочевидный вариант:

> matches :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matches    f cs = foldr (\(cs', n) a -> fromEnum (f cs cs') * n + a) 0

Это вариант, который только немного бессмысленно, параметр a Это сокращено.

> matchesPF' :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matchesPF' f cs = foldr (\(cs', n) -> (+) (fromEnum (f cs cs') * n)) 0

Это вариант, который встраивает uncurry рукой.

> matchesPFI :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matchesPFI f cs = foldr ((+) . (\(cs', n) -> fromEnum (f cs cs') * n)) 0

Это ваша точечная версия.

> matchesPF :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matchesPF  f cs = foldr ((+) . uncurry  ((*) . fromEnum . f cs)) 0

Это вариант, который использует пользовательский uncurry, увидеть ниже.

> matchesPFU :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matchesPFU f cs = foldr ((+) . uncurryI ((*) . fromEnum . f cs)) 0

Это вариант, который использует пользовательский ленивый uncurry, увидеть ниже.

> matchesPFL :: (a -> a -> Bool) -> a -> [(a, Int)] -> Int
> matchesPFL f cs = foldr ((+) . uncurryL ((*) . fromEnum . f cs)) 0

Чтобы легко проверить функции, мы используем список:

> funcs = [matches, matchesPF', matchesPF, matchesPFL, matchesPFU, matchesPFI]

Наш самописец uncurry:

> uncurryI :: (a -> b -> c) -> (a, b) -> c
> uncurryI f (a,b) = f a b

Ленивый uncurry:

> uncurryL :: (a -> b -> c) -> (a, b) -> c
> uncurryL f p = f (fst p) (snd p)

Ленивый вариант uncurryL имеет ту же семантику, что и вариант в Prelude например,

uncurry (\_ _ -> 0) undefined == 0 == uncurryL (\_ _ -> 0) undefined

в то время как uncurryI строг в позвоночнике пары.

> main = do
>   let f a b = a < b
>   forM_ [1..10] $ \i ->
>     forM_ funcs $ \m ->
>       print $ m f i (zip (cycle [1..10]) [1..i*100000])

Список [1..i*100000] зависит от i намеренно, чтобы мы не вводили CAF и не искажали наш профиль распределения.

Обезвоженный код

Прежде чем мы углубимся в профиль, давайте посмотрим на desugared код каждой функции:

==================== Desugar (after optimization) ====================
Result size of Desugar (after optimization)
  = {terms: 221, types: 419, coercions: 0}

uncurryL
uncurryL = \ @ a @ b @ c f p -> f (fst p) (snd p)

uncurryI
uncurryI = \ @ a @ b @ c f ds -> case ds of _ { (a, b) -> f a b }

-- uncurried inlined by hand
matchesPFI =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (. (+ $fNumInt)
         (\ ds ->
            case ds of _ { (cs', n) ->
            * $fNumInt (fromEnum $fEnumBool (f cs cs')) n
            }))
      (I# 0)

-- lazy uncurry
matchesPFL =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (. (+ $fNumInt)
         (uncurryL (. (* $fNumInt) (. (fromEnum $fEnumBool) (f cs)))))
      (I# 0)

-- stricter uncurry
matchesPFU =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (. (+ $fNumInt)
         (uncurryI (. (* $fNumInt) (. (fromEnum $fEnumBool) (f cs)))))
      (I# 0)

-- normal uncurry
matchesPF =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (. (+ $fNumInt)
         (uncurry (. (* $fNumInt) (. (fromEnum $fEnumBool) (f cs)))))
      (I# 0)

-- eta-reduced a
matchesPF' =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (\ ds ->
         case ds of _ { (cs', n) ->
         + $fNumInt (* $fNumInt (fromEnum $fEnumBool (f cs cs')) n)
         })
      (I# 0)

-- non-point-free
matches =
  \ @ a f cs ->
    foldr
      $fFoldable[]
      (\ ds a ->
         case ds of _ { (cs', n) ->
         + $fNumInt (* $fNumInt (fromEnum $fEnumBool (f cs cs')) n) a
         })
      (I# 0)

Пока все вроде хорошо. В этом нет ничего удивительного. Функции классов типов заменяются их словарными вариантами, например foldr становится foldr $fFoldable[]`, так как мы вызываем его в списке.

Профиль

   Пн Июл 18 15:47 Отчет по профилированию времени и распределения (окончательный)

       Prof +RTS -s -p -RTS общее время =        1,45 с (1446 тиков на 1000 долл. США, 1 процессор) общее выделение =        1 144 197 200 байт (исключая накладные расходы на профилирование) COST МОДУЛЬ ЦЕНТРА% time% alloc matchPF 'Main 13.6    0.0 совпадений PF Main 13.3   11.5 main. \. \ Main 11.8 76.9 main.f Main 10.9    0.0 uncurryL Main 9.5 11.5 совпадений PFU Main 8.9    0.0 совпадений PFI Main 7.3 0.0 совпадений Main 6.9    0.0 совпадений PFL Main 6.3 0.0 uncurryI Main 5.3 0.0 совпадений PF '. \ Main 2.6    0.0 совпадений PFI. \ Main 2.0 0.0 совпадений. \ Main 1.5 0.0 отдельный унаследованный МОДУЛЬ COST CENTER no. записи%time %alloc   %time %alloc

MAIN               MAIN                     44           0    0.0    0.0   100.0  100.0
 main              Main                     89           0    0.0    0.0   100.0  100.0
  main.\           Main                     90          10    0.0    0.0   100.0  100.0
   main.\.\        Main                     92          60   11.8   76.9   100.0  100.0
    funcs          Main                     93           0    0.0    0.0    88.2   23.1 совпадений PFI    Main                    110          10    7.3    0.0    11.7    0.0 совпадений PFI.\ Main                    111     5500000    2.0    0.0     4.4    0.0
       main.f      Main                    112     5500000    2.4    0.0     2.4    0.0 совпадений PFU    Main                    107          10    8.9    0.0    15,3.3 uncurryI Main 108 5500000 5.3 0.0     6.4    0.0
       main.f Основной 109     5500000    1,1 0,0 1,1 0,0 совпадений PFL Основной 104          10    6.3    0,0    17,7   11,5
      uncurryL Основной 105     5500000    9,5 11,5 11,4   11,5 main.f Основной 106     5500000    1,9    0,0 1,9    0,0 совпадений PF Основной 102          10   13,3   11,5 15,4   11,5 main.f Основной 103     5500000    2,1 0,0    2.1    0,0 совпадений PF'    Main                     99          10   13,6    0,0    17,2 0,0 совпадений PF'.\ Main                    100     5500000    2,6    0,0 3,6    0,0 main.f Main 101 5500000 1,0 0,0 1,0 0,0 совпадений Main                     94          10    6,9    0,0 10,9    0,0 совпадений.\    Main                     97     5500000    1,5 0,0 4,0 0,0
       main.f      Main                     98     5500000    2,5 0,0 2,5 0,0
 CAF               Main                     87           0    0,0 0,0 0,0 0,0 func Main                     91           1    0,0 0,0 0,0 0,0
  main             Main                     88           1    0,0 0. 0 0,0 0,0
   main.\          Main                     95           0    0,0 0,0 0,0 0,0
    main.\.\       Main                     96           0    0,0 0,0 0,0 0,0
 CAF               GHC.IO.Handle.FD         84           0    0,0 0,0 0,0 0,0
 CAF               GHC.Conc.Signal          78           0    0,0 0,0 0,0 0,0
 CAF               GHC.IO.Encoding          76           0    0,0 0,0 0,0 0,0
 CAF               GHC.IO.Handle.Text       75           0    0,0 0,0 0,0 0,0
 CAF               GHC.IO.Encoding.Iconv    59           0    0,0 0,0 0,0 0,0 0,0 

Игнорировать main\.\. шум, это просто список. Тем не менее, есть один момент, на который следует обратить внимание: matchesPF а также uncurryL использовать то же самое alloc%:

matchesPF    Main       13.3   11.5
uncurryL     Main        9.5   11.5

Как добраться до ядра

Теперь пришло время проверить полученный ЯДЕР (ghc -ddump-simpl). Мы заметим, что большинство функций были преобразованы в рабочие оболочки, и они выглядят более или менее одинаково (-dsuppress-all -dsuppress-uniques):

$wa5
$wa5 =
  \ @ a1 w w1 w2 ->
    letrec {
      $wgo
      $wgo =
        \ w3 ->
          case w3 of _ {
            [] -> 0;
            : y ys ->
              case y of _ { (cs', n) ->
              case $wgo ys of ww { __DEFAULT ->
              case w w1 cs' of _ {
                False -> case n of _ { I# y1 -> ww };
                True -> case n of _ { I# y1 -> +# y1 ww }
              }
              }
              }
          }; } in
    $wgo w2

Это твой обычный рабочий-фантик. $wgo берет список, проверяет, пусто ли, строго в голове (case y of _ { (cs', n) ->…) и ленив в рекурсивном результате $wgo ys of ww,

Все функции выглядят одинаково. Ну все кроме matchesPF (твой вариант)

-- matchesPF
$wa3 =
  \ @ a1 w w1 w2 ->
    letrec {
      $wgo =
        \ w3 ->
          case w3 of _ {
            [] -> 0;
            : y ys ->
              case $wgo ys of ww { __DEFAULT ->
              case let {
                     x = case y of _ { (x1, ds) -> x1 } } in
                   case w w1 x of _ {
                     False ->
                       case y of _ { (ds, y1) -> case y1 of _ { I# y2 -> main13 } };
                              -- main13 is just #I 0
                     True -> case y of _ { (ds, y1) -> y1 }
                   }
              of _ { I# x ->
              +# x ww
              }
              }
          }; } in
    $wgo w2

а также matchesPFL (вариант, который использует ленивый uncurryL)

-- matchesPFL
$wa2
$wa2 =
  \ @ a1 w w1 w2 ->
    letrec {
      $wgo =
        \ w3 ->
          case w3 of _ {
            [] -> 0;
            : y ys ->
              case $wgo ys of ww { __DEFAULT ->
              case snd y of ww1 { I# ww2 ->
              case let {
                     x = fst y } in
                   case w w1 x of _ {
                     False -> main13;
                     True -> ww1
                   }
              of _ { I# x ->
              +# x ww
              }
              }
              }
          }; } in
    $wgo w2

Они практически одинаковы. И оба они содержат let привязки. Это создаст громоотвод и, как правило, приведет к ухудшению требований к пространству.

Решение

Я думаю, что виновник в этом пункте ясен. это uncurry, GHC хочет обеспечить правильную семантику

uncurry (const (const 0)) undefined

Тем не менее, это добавляет лени и дополнительных громов. Ваш вариант без использования точек не представляет такого поведения, поскольку вы сопоставляете шаблон с парой:

foldr (\(cs', n) a -> …)

Все еще не доверяете мне? Используйте ленивый образец

foldr (\ ~(cs', n) a -> …)

и вы заметите, что matches будет вести себя так же, как matchesPF, Поэтому используйте более строгий вариант uncurry, uncurryI достаточно дать подсказку анализатора строгости.

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

Другие вопросы по тегам