您好,我遇到过ghc优化标志的有线行为。优化标志似乎改变了评估方式。总之,
primes
和isPrime
的代码,这些代码是通过引用彼此来定义的。ghc -O3
,但我无法使用runhaskell
来获取结果。这花费了太多时间。ghc -O1
时,结果会立即显示为-O3
,但ghc -O0
编译的可执行文件无法在一分钟内计算结果。Debug.Trace.trace
查找primes
每次调用isPrime
时都会从其开始进行评估。primes
和isPrime
的定义移到了另一个文件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 3
和isPrime 5
会被一次又一次地调用。
因此我认为,当我使用-O0
时,primes
每次调用[2]
时都不会从isPrime
缓存和构建。-O0
所以第一个问题是-O1
和-O0
改变评估行为的原因。
这是另一个问题。好的,好的,但我很少使用-O2
标志。在大多数情况下,我使用-O3
或primes
优化标记,因此我认为上述问题并未出现在许多用例中。
但是当我将代码移动到另一个文件时,问题又出现了。我刚刚将isPrime
和import 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}}。
提前致谢。
答案 0 :(得分:10)
此处发生的事情如下:
primes :: Integral a => [a]
类型类可以防止primes
被天真地记忆。 primes :: [Int]
与primes :: [Integer]
不同。并且不能共享任何计算,因为GHC不能假设Num
的所有实例都遵循相同的逻辑。因此,primes
的每次使用最终都会重新计算所选类型的列表。
但是当你启用优化时,GHC会变得更加智能。当primes
的唯一用途与定义在同一模块中时,GHC可以将其优化为它所用的具体类型。然后在列表的使用中共享计算。
但它只在模块边界内执行此操作。单独的模块编译意味着如果导出primes
,它不能专门用于具体类型 - GHC永远不知道它将编译的下一个模块是否可以使用不同类型的primes
。
解决此问题的最简单方法是给primes
一个具体类型。然后即使天真地使用它也会记忆。