Runhaskell性能异常

8

我正在尝试理解在runhaskell下运行程序时观察到的性能异常。

涉及的程序是:

isFactor n = (0 ==) . (mod n)
factors x = filter (isFactor x) [2..x]
main = putStrLn $ show $ sum $ factors 10000000

当我运行这个程序时,它需要1.18秒的时间。
然而,如果我重新定义isFactor为:
isFactor n f = (0 ==) (mod n f)

然后程序需要花费17.7秒的时间。

这是性能上的巨大差异,而我本来期望这两个程序是等效的。有人知道我错在哪里了吗?

注:使用GHC编译时不会出现这种情况。


1
我的猜测是,由于runhaskell只执行了一些优化,第二个受到了某些严格性问题的影响。 - fuz
2个回答

9
尽管这两个函数的功能应该是相同的,但它们的应用方式有所不同。第一个定义中,在调用isFactor x时完全应用了isFactor。在第二个定义中,由于isFactor现在显式地接受了两个参数,因此它没有被完全应用。
即使是最小的优化也足以让GHC看穿这一点,并为两者创建相同的代码,但是如果您使用-O0 -ddump-simpl编译,则可以确定,如果没有任何优化,这将产生差异(至少在ghc-7.2.1中,其他版本可能会有所不同)。
对于第一个isFactor,GHC创建一个作为“GHC.List.Filter”谓词传递的单个函数,其中mod 10000000(==)的调用被内联。对于第二个定义,相反,isFactor中的大多数调用都是类函数的let-bound引用,并且在isFactor的多个调用之间不共享。因此,存在完全不必要的字典开销。
这几乎从未成为问题,因为即使默认的编译器设置也会将其优化掉,然而runhaskell明显甚至没有做到这一点。即便如此,我有时也会将代码结构化为someFun x y = \z ->,因为我知道someFun将被部分应用,这是保留调用之间共享的唯一方法(即GHC的优化器不够聪明)。

5
据我理解,runhaskell 很少或几乎不进行优化。它的设计是为了快速加载和运行代码。如果它进行更多的优化,那么你的代码启动时间就会更长。当然,在这种情况下,使用优化后,代码运行更快。
据我了解,如果已经存在编译版本的代码,则 runhaskell 会使用它。因此,如果性能很重要,请确保首先启用了优化进行编译。(我认为甚至可以向 runhaskell 传递开启优化的开关,你需要查看文档...)

谢谢您的回答!我明白runhaskell并不执行太多优化,但是在我的理解中,foo x = bar xfoo = bar应该生成完全相同的代码。这就是我的困惑所在。即使没有优化,我也只是想了解为什么这两个会有不同的表现。 - Steve
1
我想象中,根据函数定义的方式不同,thunk 的结构可能会有微小的差异。比如说,一个版本与所有调用共享一个 thunk,而另一个版本则为每个调用创建一个副本,导致更多的分配和释放发生。这只是我的猜测;你需要向 GHC 开发人员询问“真正”的原因。显然,在启用优化的情况下,两种方式最终都应该产生相同的代码。只是在没有优化的情况下,这种情况就不会发生。 - MathematicalOrchid
看起来是一个合理的猜测。当编译(到目标代码)时,两个版本无论优化级别如何都会产生相同的代码,所以这是字节码生成器的一个怪癖。 - Daniel Fischer
@JohnL 承认,我只尝试了7.4.1版本,在那里得到了相同的核心。 - Daniel Fischer
@MathematicalOrchid 你是正确的。我很惊讶即使是字面上的 0 在使用缓慢版本时也不会在多次调用之间共享。 - John L
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接