Почему добавление INLINE замедляет мою программу

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

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

Поэтому я написал следующую функцию:

foldlp :: (b -> a -> b) -> (b -> Bool) -> b -> [a] -> b
foldlp f p = go where
    go b [] = b
    go b (x : xs) 
        | p b = go (f b x) xs
        | otherwise = b

И проверил это с помощью моей пользовательской функции умножения короткого замыкания:

 mult :: Integer -> Integer -> Integer
 mult 0 _ = 0
 mult x y = x * y

 main :: IO ()
 main = print . <test_function>

Результаты, которые я получил с -prof -fprof-auto -O2, +RTS -p, были следующими:

foldlp mult (/= 0) 1 $ replicate (10 ^ 7) 1
total time = 0.40 secs
total alloc = 480,049,336 bytes

foldlp mult (`seq` True) 1 $ replicate (10 ^ 7) 1
total time = 0.37 secs
total alloc = 480,049,336 bytes

foldl' mult 1 $ replicate (10 ^ 7) 1
total time = 0.37 secs
total alloc = 480,049,352 bytes

foldl mult 1 $ replicate (10 ^ 7) 1
total time = 0.74 secs
total alloc = 880,049,352 bytes

foldr mult 1 $ replicate (10 ^ 7) 1
total time = 0.87 secs
total alloc = 880,049,336 bytes

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

Первый пример завершится, как только достигнет 0, как и foldr, но foldr намного медленнее.

Это позволяет избежать таких проблем, как переходы внутри кортежей, поскольку ((1 + 2) + 3, (10 + 20) + 30) технически находится в WHNF, нарушая foldl'.

Вы можете повторно получить foldl с помощью flip foldl (const True) и foldl' с flip foldl (seqTrue). И рабочие характеристики исходных ограниченных функций, по-видимому, получаются при этом заново.

Так что в качестве примечания я думаю, что foldlp было бы достойным дополнением к Foldable.

Но мой настоящий вопрос заключался в том, почему, когда я добавил {-# INLINE foldlp #-}, производительность функций значительно снизилась, что дало мне:

foldlp mult (/= 0) 1 $ replicate (10 ^ 7) 1
total time = 0.67 secs
total alloc = 800,049,336 bytes

Поэтому мой реальный вопрос заключается в том, почему это происходит. Я думал, что недостатком встраивания является раздувание кода, а не значительное негативное влияние на производительность во время выполнения и увеличение использования памяти.


person semicolon    schedule 12.10.2016    source источник
comment
Похоже, что принудительное встраивание раньше приводит к тому, что некоторые другие правила не срабатывают должным образом...   -  person Alec    schedule 12.10.2016


Ответы (1)


Согласно документам GHC, прагма INLINE предотвращает другие оптимизации компилятора, чтобы правила перезаписи продолжали действовать.

Поэтому я предполагаю, что с помощью INLINE вы удаляете некоторую оптимизацию, которую GHC применил бы, чтобы сделать ваш код быстрее.

Немного поковырявшись в ядре (используя -ddump-simpl в компиляции) я нашел оптимизацию, которую выполняет GHC. Для этого я взглянул на ядро ​​для foldlp с встраиванием и без встраивания:

Встроенный:

foldlp =
  \ (@ b_a10N)
    (@ a_a10O)
    (eta_B2 :: b_a10N -> a_a10O -> b_a10N)
    (eta1_B1 :: b_a10N -> Bool)
    (eta2_X3 :: b_a10N)
    (eta3_X5 :: [a_a10O]) ->
    letrec {
      go_s1Ao [Occ=LoopBreaker] :: b_a10N -> [a_a10O] -> b_a10N
      [LclId, Arity=2, Str=DmdType <L,U><S,1*U>]
      go_s1Ao =
        \ (b1_avT :: b_a10N) (ds_d1xQ :: [a_a10O]) ->
        -- Removed the actual definition of go for brevity,
        -- it's the same in both cases
          }; } in
    go_s1Ao eta2_X3 eta3_X5

Не встроенный:

foldlp =
  \ (@ b_a10N)
    (@ a_a10O)
    (f_avQ :: b_a10N -> a_a10O -> b_a10N)
    (p_avR :: b_a10N -> Bool) ->
    letrec {
      go_s1Am [Occ=LoopBreaker] :: b_a10N -> [a_a10O] -> b_a10N
      [LclId, Arity=2, Str=DmdType <L,U><S,1*U>]
      go_s1Am =
        \ (b1_avT :: b_a10N) (ds_d1xQ :: [a_a10O]) ->
        -- Removed the actual definition of go for brevity,
        -- it's the same in both cases
          }; } in
    go_s1Am

Соответствующее различие находится в самой последней строке. Оптимизатор убирает необходимость вызова foldlp для вызова go и просто создает функцию с двумя аргументами из foldlp, которая возвращает функцию с двумя аргументами. При инлайнинге эта оптимизация не выполняется и ядро ​​выглядит именно так, как написанный вами код.

Я проверил это, написав три варианта foldlp:

module Main where

foldlp :: (b -> a -> b) -> (b -> Bool) -> b -> [a] -> b
foldlp f p = go where
      go b [] = b
      go b (x : xs)
        | p b = go (f b x) xs
        | otherwise = b

{-# INLINE foldlpInline #-}
foldlpInline :: (b -> a -> b) -> (b -> Bool) -> b -> [a] -> b
foldlpInline f p = go where
      go b [] = b
      go b (x : xs)
        | p b = go (f b x) xs
        | otherwise = b


{-# INLINE foldlp' #-} -- So that the code is not optimized
foldlp' b [] = b
foldlp' b (x : xs)
        | (/= 0) b = foldlp' (mult b x) xs
        | otherwise = b

mult :: Integer -> Integer -> Integer
mult 0 _ = 0
mult x y = x * y

--main = print $ foldlp mult (/= 0) 1 $ replicate (10 ^ 7) 1
--main = print $ foldlpInline mult (/= 0) 1 $ replicate (10 ^ 7) 1
main = print $ foldlp' 1 $ replicate (10 ^ 7) 1

Результаты:

Первый случай (обычный не встроенный):

./test  0,42s user 0,01s system 96% cpu 0,446 total

Второй случай (встроенный):

./test  0,83s user 0,02s system 98% cpu 0,862 total

Третий случай (что выдает компилятор для не встроенных)

./test  0,42s user 0,01s system 99% cpu 0,432 total
person Christof Schramm    schedule 12.10.2016
comment
Ок классно спасибо! В этом случае я всегда должен оставлять foldlp не встроенным? Или это зависит от ситуации (например, за пределами модуля). Есть ли способ заставить GHC встроиться, но сначала попробовать другие оптимизации? INLINABLE возможно? - person semicolon; 12.10.2016
comment
Насколько я понимаю документы, GHC автоматически определяет, когда выгодно встроить функцию, и делает это. Поэтому я бы рекомендовал не использовать прагму INLINE, если у вас нет конкретной причины. - person Christof Schramm; 13.10.2016