【问题标题】:Why does adding INLINE slow down my program为什么添加 INLINE 会减慢我的程序
【发布时间】:2016-10-12 05:48:45
【问题描述】:

我正在考虑制作一个适用于无限列表的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 的速度要慢得多。

它避免了元组内的 thunk 等问题,因为((1 + 2) + 3, (10 + 20) + 30) 在技术上属于 WHNF,从而破坏了foldl'

您可以通过flip foldl (const True)foldl' 重新获得foldlflip 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

所以我真正的问题是为什么会这样。我认为内联的缺点是代码膨胀,对运行时性能和内存使用量增加没有显着的负面影响。

【问题讨论】:

  • 看起来强制内联提前发生会导致其他一些规则无法正常触发......

标签: haskell inline


【解决方案1】:

根据the GHC docs 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

【讨论】:

  • 好的,谢谢!在那种情况下,我应该总是让 foldlp 不内联吗?还是取决于情况(例如跨模块边界)。有没有办法告诉 GHC 内联但先尝试其他优化? INLINABLE 或许?
  • 根据我对文档的理解,GHC 会自动检测何时内联函数有利,并这样做。所以我建议不要使用INLINE pragma,除非你有特定的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-25
  • 2019-05-29
  • 1970-01-01
  • 2017-01-16
相关资源
最近更新 更多