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

#haskell #inline

#haskell #встроенный

Вопрос:

Я изучал возможность создания a 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
  

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

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

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

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

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

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

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

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

Комментарии:

1. Похоже, что принудительное выполнение встраивания раньше приводит к тому, что некоторые другие правила не срабатывают должным образом…

Ответ №1:

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

Так что я предполагаю, что, используя 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
  

Комментарии:

1. Хорошо, потрясающее спасибо! В этом случае я должен всегда оставлять foldlp не встроенным? Или это зависит от ситуации (например, от границ модуля). Есть ли способ указать GHC на inline, но сначала попробуйте другие оптимизации? INLINABLE возможно?

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