为什么添加 INLINE 会减慢我的程序
Why does adding INLINE slow down my program
我正在考虑制作一个适用于无限列表的 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
,用flip foldl (
seqTrue)
重新获得foldl'
。而原来受限函数的性能特征,似乎也因此重新获得了。
因此,作为旁注,我认为 foldlp
是对 Foldable
的一个有价值的补充。
但我真正的问题是为什么当我添加 {-# INLINE foldlp #-}
函数性能显着下降,给我:
foldlp mult (/= 0) 1 $ replicate (10 ^ 7) 1
total time = 0.67 secs
total alloc = 800,049,336 bytes
所以我真正的问题是为什么会这样。我认为内联的缺点是代码膨胀,对运行时性能和内存使用量增加没有显着的负面影响。
根据 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 中创建一个带有两个参数的函数,而 returns 是一个带有两个参数的函数。通过内联,不会执行此优化,核心看起来与您编写的代码完全一样。
我通过编写 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
我正在考虑制作一个适用于无限列表的 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
,用flip foldl (
seqTrue)
重新获得foldl'
。而原来受限函数的性能特征,似乎也因此重新获得了。
因此,作为旁注,我认为 foldlp
是对 Foldable
的一个有价值的补充。
但我真正的问题是为什么当我添加 {-# INLINE foldlp #-}
函数性能显着下降,给我:
foldlp mult (/= 0) 1 $ replicate (10 ^ 7) 1
total time = 0.67 secs
total alloc = 800,049,336 bytes
所以我真正的问题是为什么会这样。我认为内联的缺点是代码膨胀,对运行时性能和内存使用量增加没有显着的负面影响。
根据 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 中创建一个带有两个参数的函数,而 returns 是一个带有两个参数的函数。通过内联,不会执行此优化,核心看起来与您编写的代码完全一样。
我通过编写 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