【发布时间】:2019-04-29 11:55:41
【问题描述】:
我正在分析 where 子句对 Haskell 程序性能的影响。
在Haskell, The craft of functional programming, Thomspson,第 20.4 章中,我找到了以下示例:
exam1 :: Int -> [Int]
exam1 n = [1 .. n] ++ [1 .. n]
exam2 :: Int -> [Int]
exam2 n = list ++ list
where list = [1 .. n]
而且,我引用,
计算 [exam1] 所需的时间将是
O(n),使用的空间将是O(1),但我们将不得不计算表达式[1 .. n]两次。...
[exam2] 的作用是计算一次列表
[1 .. n],这样我们计算后保存它的值,以便再次使用。...
如果我们通过在
where子句中引用它来保存某些东西,我们必须为它占用的空间付出代价。
所以我发疯了,认为-O2 标志必须处理这个问题并为我选择最佳行为。我使用 Criterion 分析这两个示例的时间复杂度。
import Criterion.Main
exam1 :: Int -> [Int]
exam1 n = [1 .. n] ++ [1 .. n]
exam2 :: Int -> [Int]
exam2 n = list ++ list
where list = [1 .. n]
m :: Int
m = 1000000
main :: IO ()
main = defaultMain [ bench "exam1" $ nf exam1 m
, bench "exam2" $ nf exam2 m
]
我用-O2编译,发现:
benchmarking exam1
time 15.11 ms (15.03 ms .. 15.16 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 15.11 ms (15.08 ms .. 15.14 ms)
std dev 83.20 μs (53.18 μs .. 122.6 μs)
benchmarking exam2
time 76.27 ms (72.84 ms .. 82.75 ms)
0.987 R² (0.963 R² .. 0.997 R²)
mean 74.79 ms (70.20 ms .. 77.70 ms)
std dev 6.204 ms (3.871 ms .. 9.233 ms)
variance introduced by outliers: 26% (moderately inflated)
有什么不同!为什么会这样?我认为exam2 应该更快但内存效率低(根据上面的引用)。但是不,它实际上要慢得多(并且可能内存效率更低,但需要测试)。
也许它比较慢,因为[1 .. 1e6] 必须存储在内存中,这需要很多时间。你怎么看?
PS:我找到了a possibly related question,但不是真的。
【问题讨论】:
-
看起来在第二个示例中编译器未能内联
list所以它实际上计算它,存储在内存中,然后从内存中读取它。 -
@talex Inlining
let x = expensiveComputation in f x x在一般情况下可能是有害的,因为它可能导致x被计算两次。使用where我们可能会遇到类似的问题。我认为 GHC 在这种情况下对内联非常保守,因为它可能会导致性能灾难(例如,进行两次递归调用而不是一次可能会导致指数级爆炸)。 -
@chi 是的,但在这种情况下,内联根本不会导致计算。它将即时计算。
-
@talex 我同意,但我不希望 GHC 理解在这种情况下,它可以安全地内联。检测什么时候可以内联,什么时候不能内联看起来并不简单。
标签: performance haskell time-complexity space-complexity