为什么ghc会因优化标志而改变评估方式?

时间:2014-09-21 09:59:07

标签: haskell ghc

您好,我遇到过ghc优化标志的有线行为。优化标志似乎改变了评估方式。总之,

  • 我编写了一个包含primesisPrime的代码,这些代码是通过引用彼此来定义的。
  • 我发现该程序适用于ghc -O3,但我无法使用runhaskell来获取结果。这花费了太多时间。
  • 我注意到当我使用ghc -O1时,结果会立即显示为-O3,但ghc -O0编译的可执行文件无法在一分钟内计算结果。
  • 我使用Debug.Trace.trace查找primes每次调用isPrime时都会从其开始进行评估。
  • 我将primesisPrime的定义移到了另一个文件Prime.hs。在主文件中,我导入了Prime库。不幸的是,ghc -O3编译的可执行文件不会在一分钟内计算出结果。

以下是说明。请参阅以下代码。

main :: IO ()
main = print $ length $ filter isPrime [100000..1000000]

primes :: Integral a => [a]
primes = 2 : filter isPrime [3,5..]

isPrime :: Integral a => a -> Bool
isPrime n = n > 1 && foldr (\p r -> p * p > n || (n `mod` p /= 0 && r)) True primes

当我使用ghc -O3编译代码时,可执行文件会在2秒内计算出正确的结果68906

 $ ghc -O3 test.hs
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...
 $ time ./test
68906
./test  1.24s user 0.02s system 79% cpu 1.574 total

然而,当我使用-O0时,我无法在一分钟内得到结果。请务必提前删除生成的文件。

 $ rm -f ./test ./test.o ./test.hi
 $ ghc -O0 test.hs
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...
 $ time ./test
^C
./test  64.34s user 0.94s system 94% cpu 1:08.90 total
我流产了。标记-O1-O3的效果相同。

让我们深入调查。我用了Debug.Trace.trace。我追溯了isPrime的论点。

import Debug.Trace

main :: IO ()
main = print $ length $ filter isPrime [10..30]

primes :: (Show a, Integral a) => [a]
primes = 2 : filter isPrime [3,5..]

isPrime :: (Show a, Integral a) => a -> Bool
isPrime n = trace (show n) $ n > 1 && foldr (\p r -> p * p > n || (n `mod` p /= 0 && r)) True primes

当优化标志为-O3,(或-O1)时,输出如下:

10
11
3
5
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
7
30
6

这个结果是合理的(注意最后一行打印素数的数量; 11,13,17,19,23,29)。

这是-O0(或runhaskell

的结果
10
11
3
5
3
12
13
3
5
3
14
15
3
16
17
3
5
3
18
19
3
5
3
20
21
3
22
23
3
5
3
24
25
3
5
3
26
27
3
28
29
3
5
3
7
3
30
6

这个结果很有意思。 2已经安排在primes的头部。如果isPrime一次又一次地检查3和5。调用isPrime 11时,如果是素数,则检查3,并且还检查5,再次调用isPrime 3。同样,对于几乎每个奇数,isPrime 3isPrime 5会被一次又一次地调用。

因此我认为,当我使用-O0时,primes每次调用[2]时都不会从isPrime缓存和构建。-O0所以第一个问题是-O1-O0改变评估行为的原因。

这是另一个问题。好的,好的,但我很少使用-O2标志。在大多数情况下,我使用-O3primes优化标记,因此我认为上述问题并未出现在许多用例中。

但是当我将代码移动到另一个文件时,问题又出现了。我刚刚将isPrimeimport Prime main :: IO () main = print $ length $ filter isPrime [100000..1000000] 移至Prime.hs。

test.hs:

module Prime where

primes :: Integral a => [a]
primes = 2 : filter isPrime [3,5..]

isPrime :: Integral a => a -> Bool
isPrime n = n > 1 && foldr (\p r -> p * p > n || (n `mod` p /= 0 && r)) True primes

Prime.hs:

-O1

在这段时间内,我无法使用-O3标志或 $ ghc -O3 test.hs [1 of 2] Compiling Prime ( Prime.hs, Prime.o ) [2 of 2] Compiling Main ( test.hs, test.o ) Linking test ... $ time ./test ^C ./test 62.41s user 0.88s system 92% cpu 1:08.23 total 标志获取结果。

-O3
嗯,我再次流产了。我不知道这种方式是否会对结果产生影响,我事先用Debug.Trace.trace预编译Prime.hs,但是徒劳无功。我特此使用-O3,我一次又一次地看到2和3 primes标志。简而言之,我无法创建Prime库,因为当isPrime-O3移动到模块中时评估方式会发生变化(这让我感到惊讶)并且-O3无法使其工作。

所以第二个问题是,尽管有-O0标志,为什么模块中的东西被评估为Data.Numbers.Primes标志编译?

我终于厌倦了调查这种有线行为。我总结说我不应该在模块中使用交叉引用的定义。我放弃了创建Prime库并开始使用{{1}}。

提前致谢。

1 个答案:

答案 0 :(得分:10)

此处发生的事情如下:

primes :: Integral a => [a]

类型类可以防止primes被天真地记忆。 primes :: [Int]primes :: [Integer]不同。并且不能共享任何计算,因为GHC不能假设Num的所有实例都遵循相同的逻辑。因此,primes的每次使用最终都会重新计算所选类型的列表。

但是当你启用优化时,GHC会变得更加智能。当primes的唯一用途与定义在同一模块中时,GHC可以将其优化为它所用的具体类型。然后在列表的使用中共享计算。

但它只在模块边界内执行此操作。单独的模块编译意味着如果导出primes,它不能专门用于具体类型 - GHC永远不知道它将编译的下一个模块是否可以使用不同类型的primes

解决此问题的最简单方法是给primes一个具体类型。然后即使天真地使用它也会记忆。